TonieToolbox 0.6.1__py3-none-any.whl → 0.6.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1212 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ GUI Player Module for TonieToolbox
4
+
5
+ This module provides a minimal tkinter-based GUI for playing TAF (Tonie Audio Format) files.
6
+ It wraps the existing TAFPlayer functionality in a user-friendly graphical interface.
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import time
12
+ import threading
13
+ from typing import Optional, Dict, Any
14
+ from pathlib import Path
15
+
16
+ from .logger import get_logger
17
+ from .player import TAFPlayer, TAFPlayerError
18
+ from .constants import ICON_BASE64
19
+ from . import __version__
20
+ from .tonie_analysis import split_to_opus_files, extract_to_mp3_files, extract_full_audio_to_mp3
21
+
22
+ try:
23
+ import tkinter as tk
24
+ from tkinter import ttk, messagebox, filedialog
25
+ import base64
26
+ import io
27
+ TKINTER_AVAILABLE = True
28
+ except ImportError as e:
29
+ TKINTER_AVAILABLE = False
30
+ TKINTER_ERROR = str(e)
31
+
32
+
33
+ logger = get_logger(__name__)
34
+
35
+
36
+ class TAFPlayerGUI:
37
+ """
38
+ A minimal GUI player for TAF files using tkinter.
39
+
40
+ This player provides a simple graphical interface with basic playback controls,
41
+ progress tracking, and file information display.
42
+ """
43
+
44
+ def __init__(self):
45
+ """Initialize the GUI player."""
46
+ self.root = tk.Tk()
47
+ self.root.title("🎵 TonieToolbox - TAF Player")
48
+ optimal_width = 1024
49
+ optimal_height = 1500
50
+
51
+ self.root.geometry(f"{optimal_width}x{optimal_height}")
52
+ self.root.resizable(True, True)
53
+ self.root.minsize(1024, 768)
54
+ self._center_window(optimal_width, optimal_height)
55
+ self.root.lift()
56
+ self.root.attributes('-topmost', True)
57
+ self.root.after_idle(lambda: self.root.attributes('-topmost', False))
58
+ self.colors = {
59
+ 'bg_dark': '#2b2b2b',
60
+ 'bg_medium': '#3c3c3c',
61
+ 'bg_light': '#4a4a4a',
62
+ 'accent': '#ff6b35',
63
+ 'accent_hover': '#ff8c5a',
64
+ 'text_light': '#ffffff',
65
+ 'text_medium': '#cccccc',
66
+ 'success': '#4ade80',
67
+ 'warning': '#fbbf24',
68
+ 'error': '#f87171'
69
+ }
70
+
71
+ self.root.configure(bg=self.colors['bg_dark'])
72
+ self._set_window_icon()
73
+ self._configure_styles()
74
+ self.player: Optional[TAFPlayer] = None
75
+ self.current_file: Optional[Path] = None
76
+ self.is_playing = False
77
+ self.is_paused = False
78
+ self.update_thread: Optional[threading.Thread] = None
79
+ self.stop_updates = threading.Event()
80
+ self.progress_var = tk.DoubleVar()
81
+ self.current_time_var = tk.StringVar(value="00:00")
82
+ self.total_time_var = tk.StringVar(value="00:00")
83
+ self.file_name_var = tk.StringVar(value="No file loaded")
84
+ self.status_var = tk.StringVar(value="🔄 Ready to load TAF file")
85
+ self.selected_input_file: Optional[str] = None
86
+ self._create_widgets()
87
+ self._setup_layout()
88
+ self.root.protocol("WM_DELETE_WINDOW", self._on_close)
89
+
90
+ logger.info("TAF Player GUI initialized")
91
+
92
+ def _center_window(self, width: int, height: int):
93
+ """Center the window on the screen."""
94
+ try:
95
+ screen_width = self.root.winfo_screenwidth()
96
+ screen_height = self.root.winfo_screenheight()
97
+ x = (screen_width - width) // 2
98
+ y = (screen_height - height) // 2
99
+ x = max(0, x)
100
+ y = max(0, y)
101
+ self.root.geometry(f"{width}x{height}+{x}+{y}")
102
+ logger.debug(f"Window centered at {x},{y} with size {width}x{height}")
103
+
104
+ except Exception as e:
105
+ logger.debug(f"Could not center window: {e}")
106
+
107
+ def _configure_styles(self):
108
+ """Configure modern dark theme styles for ttk widgets."""
109
+ style = ttk.Style()
110
+ style.theme_use('clam')
111
+ style.configure('Dark.TFrame',
112
+ background=self.colors['bg_dark'],
113
+ borderwidth=0)
114
+ style.configure('Dark.TLabelframe',
115
+ background=self.colors['bg_medium'],
116
+ borderwidth=2,
117
+ relief='raised')
118
+ style.configure('Dark.TLabelframe.Label',
119
+ background=self.colors['bg_medium'],
120
+ foreground=self.colors['text_light'],
121
+ font=('Arial', 11, 'bold'))
122
+ style.configure('Accent.TButton',
123
+ background=self.colors['accent'],
124
+ foreground=self.colors['text_light'],
125
+ borderwidth=0,
126
+ focuscolor='none',
127
+ padding=(20, 10),
128
+ font=('Arial', 10, 'bold'))
129
+ style.map('Accent.TButton',
130
+ background=[('active', self.colors['accent_hover']),
131
+ ('pressed', self.colors['accent'])])
132
+ style.configure('Large.TButton',
133
+ background=self.colors['bg_light'],
134
+ foreground=self.colors['text_light'],
135
+ borderwidth=1,
136
+ relief='raised',
137
+ focuscolor='none',
138
+ padding=(25, 15),
139
+ font=('Arial', 12, 'bold'))
140
+ style.map('Large.TButton',
141
+ background=[('active', self.colors['accent']),
142
+ ('pressed', self.colors['bg_medium']),
143
+ ('disabled', '#666666')])
144
+ style.configure('Title.TLabel',
145
+ background=self.colors['bg_dark'],
146
+ foreground=self.colors['text_light'],
147
+ font=('Arial', 14, 'bold'))
148
+ style.configure('Text.TLabel',
149
+ background=self.colors['bg_dark'],
150
+ foreground=self.colors['text_light'])
151
+ style.configure('Info.TLabel',
152
+ background=self.colors['bg_medium'],
153
+ foreground='#ffffff',
154
+ font=('Arial', 10))
155
+ style.configure('Time.TLabel',
156
+ background=self.colors['bg_dark'],
157
+ foreground=self.colors['accent'],
158
+ font=('Consolas', 11, 'bold'))
159
+ style.configure('Status.TLabel',
160
+ background=self.colors['bg_dark'],
161
+ foreground=self.colors['success'],
162
+ font=('Arial', 9))
163
+ style.configure('Title.TLabel',
164
+ background=self.colors['bg_dark'],
165
+ foreground=self.colors['text_light'],
166
+ font=('Arial', 32, 'bold'))
167
+ style.configure('Modern.Horizontal.TScale',
168
+ background=self.colors['bg_medium'],
169
+ troughcolor=self.colors['bg_light'],
170
+ borderwidth=0,
171
+ lightcolor=self.colors['accent'],
172
+ darkcolor=self.colors['accent'])
173
+ style.configure('Dark.TNotebook',
174
+ background=self.colors['bg_dark'],
175
+ borderwidth=0)
176
+ style.configure('Dark.TNotebook.Tab',
177
+ background=self.colors['bg_medium'],
178
+ foreground=self.colors['text_medium'],
179
+ padding=(20, 10),
180
+ font=('Arial', 10, 'bold'))
181
+ style.map('Dark.TNotebook.Tab',
182
+ background=[('selected', self.colors['bg_light']),
183
+ ('active', self.colors['accent'])],
184
+ foreground=[('selected', self.colors['text_light']),
185
+ ('active', self.colors['text_light'])])
186
+ style.configure('Dark.TRadiobutton',
187
+ background=self.colors['bg_medium'],
188
+ foreground=self.colors['text_medium'],
189
+ font=('Arial', 10),
190
+ focuscolor='none')
191
+ style.map('Dark.TRadiobutton',
192
+ background=[('active', self.colors['bg_light'])],
193
+ foreground=[('active', self.colors['text_light'])])
194
+ style.configure('Dark.TEntry',
195
+ background='#2d2d2d',
196
+ foreground='#ffffff',
197
+ borderwidth=2,
198
+ relief='solid',
199
+ insertcolor='#ffffff',
200
+ selectbackground=self.colors['accent'],
201
+ selectforeground='#ffffff')
202
+ style.map('Dark.TEntry',
203
+ focuscolor=[('focus', self.colors['accent'])],
204
+ bordercolor=[('focus', self.colors['accent'])])
205
+ style.configure('Modern.Horizontal.TProgressbar',
206
+ background=self.colors['accent'],
207
+ troughcolor=self.colors['bg_light'],
208
+ borderwidth=0,
209
+ lightcolor=self.colors['accent'],
210
+ darkcolor=self.colors['accent'])
211
+
212
+ def _set_window_icon(self):
213
+ """Set the window icon using the base64 encoded icon from constants."""
214
+ try:
215
+ icon_data = base64.b64decode(ICON_BASE64)
216
+ logger.debug(f"Decoded icon data: {len(icon_data)} bytes")
217
+ is_ico = icon_data.startswith(b'\x00\x00\x01\x00')
218
+ success = False
219
+ if is_ico and not success:
220
+ success = self._try_ico_to_png_icon(icon_data)
221
+ if not success:
222
+ success = self._try_photoimage_icon()
223
+ if not success and is_ico:
224
+ success = self._try_iconbitmap_icon(icon_data)
225
+ if success:
226
+ logger.info("Successfully set window icon")
227
+ else:
228
+ logger.debug("Could not set window icon - using system default")
229
+
230
+ except Exception as e:
231
+ logger.debug(f"Failed to set window icon: {e}")
232
+
233
+ def _try_photoimage_icon(self):
234
+ """Try setting icon using PhotoImage (for PNG/GIF formats)."""
235
+ try:
236
+ icon_image = tk.PhotoImage(data=ICON_BASE64)
237
+ self.root.iconphoto(True, icon_image)
238
+ logger.debug("Icon set using PhotoImage method")
239
+ return True
240
+ except tk.TclError as e:
241
+ logger.debug(f"PhotoImage method failed: {e}")
242
+ return False
243
+
244
+ def _try_iconbitmap_icon(self, icon_data):
245
+ """Try setting icon using iconbitmap with temporary file."""
246
+ try:
247
+ import tempfile
248
+ with tempfile.NamedTemporaryFile(suffix='.ico', delete=False) as tmp_file:
249
+ tmp_file.write(icon_data)
250
+ tmp_file.flush()
251
+ self.root.iconbitmap(tmp_file.name)
252
+ logger.debug("Icon set using iconbitmap method")
253
+ os.unlink(tmp_file.name)
254
+ return True
255
+ except Exception as e:
256
+ logger.debug(f"iconbitmap method failed: {e}")
257
+ return False
258
+
259
+ def _try_ico_to_png_icon(self, icon_data):
260
+ """Try extracting PNG from ICO and setting as PhotoImage."""
261
+ try:
262
+ png_start = icon_data.find(b'\x89PNG\r\n\x1a\n')
263
+ if png_start > 0:
264
+ png_data = icon_data[png_start:]
265
+ png_end = png_data.find(b'IEND') + 8
266
+ if png_end > 8:
267
+ png_data = png_data[:png_end]
268
+ png_b64 = base64.b64encode(png_data).decode('ascii')
269
+ icon_image = tk.PhotoImage(data=png_b64)
270
+ self.root.iconphoto(True, icon_image)
271
+ logger.debug("Icon set using PNG extraction from ICO")
272
+ return True
273
+ except Exception as e:
274
+ logger.debug(f"ICO to PNG extraction failed: {e}")
275
+ return False
276
+
277
+ def _create_widgets(self):
278
+ """Create all GUI widgets with modern styling and tabbed interface."""
279
+ self.main_frame = ttk.Frame(self.root, style='Dark.TFrame', padding="20")
280
+ self.header_frame = ttk.Frame(self.main_frame, style='Dark.TFrame')
281
+ self.title_label = ttk.Label(
282
+ self.header_frame,
283
+ text="🎵 TonieToolbox TAF Player",
284
+ style='Text.TLabel'
285
+ )
286
+
287
+ self.notebook = ttk.Notebook(self.main_frame, style='Dark.TNotebook')
288
+
289
+ self._create_player_tab()
290
+ self._create_tools_tab()
291
+ self._create_about_tab()
292
+
293
+ def _create_player_tab(self):
294
+ """Create the main player tab with all playback controls."""
295
+ self.player_tab = ttk.Frame(self.notebook, style='Dark.TFrame', padding="15")
296
+
297
+ self.file_frame = ttk.LabelFrame(
298
+ self.player_tab,
299
+ text="📁 Audio File",
300
+ style='Dark.TLabelframe',
301
+ padding="20"
302
+ )
303
+ self.file_display_frame = ttk.Frame(self.file_frame, style='Dark.TFrame')
304
+ self.file_icon_label = ttk.Label(
305
+ self.file_display_frame,
306
+ text="🎵",
307
+ style='Text.TLabel'
308
+ )
309
+ self.file_label = ttk.Label(
310
+ self.file_display_frame,
311
+ textvariable=self.file_name_var,
312
+ style='Text.TLabel'
313
+ )
314
+ self.load_button = ttk.Button(
315
+ self.file_frame,
316
+ text="📂 Load TAF File",
317
+ command=self._load_file,
318
+ style='Accent.TButton'
319
+ )
320
+ self.details_frame = ttk.Frame(self.file_frame, style='Dark.TFrame')
321
+ self.details_text = tk.Text(
322
+ self.details_frame,
323
+ height=10,
324
+ width=90,
325
+ state=tk.DISABLED,
326
+ wrap=tk.WORD,
327
+ bg=self.colors['bg_light'],
328
+ fg=self.colors['text_medium'],
329
+ font=('Consolas', 10),
330
+ borderwidth=0,
331
+ highlightthickness=0,
332
+ padx=15,
333
+ pady=10
334
+ )
335
+ self.details_scrollbar = ttk.Scrollbar(
336
+ self.details_frame,
337
+ orient=tk.VERTICAL,
338
+ command=self.details_text.yview
339
+ )
340
+ self.details_text.config(yscrollcommand=self.details_scrollbar.set)
341
+ self.controls_frame = ttk.LabelFrame(
342
+ self.player_tab,
343
+ text="🎮 Player Controls",
344
+ style='Dark.TLabelframe',
345
+ padding="20"
346
+ )
347
+ self.button_frame = ttk.Frame(self.controls_frame, style='Dark.TFrame')
348
+ self.play_button = ttk.Button(
349
+ self.button_frame,
350
+ text="▶️ Play",
351
+ command=self._toggle_play,
352
+ state=tk.DISABLED,
353
+ style='Large.TButton'
354
+ )
355
+ self.stop_button = ttk.Button(
356
+ self.button_frame,
357
+ text="⏹️ Stop",
358
+ command=self._stop_playback,
359
+ state=tk.DISABLED,
360
+ style='Large.TButton'
361
+ )
362
+ self.chapter_frame = ttk.Frame(self.controls_frame, style='Dark.TFrame')
363
+ self.prev_chapter_button = ttk.Button(
364
+ self.chapter_frame,
365
+ text="⏮️ Previous",
366
+ command=self._prev_chapter,
367
+ state=tk.DISABLED,
368
+ style='Accent.TButton'
369
+ )
370
+ self.next_chapter_button = ttk.Button(
371
+ self.chapter_frame,
372
+ text="⏭️ Next",
373
+ command=self._next_chapter,
374
+ state=tk.DISABLED,
375
+ style='Accent.TButton'
376
+ )
377
+ self.progress_section = ttk.Frame(self.controls_frame, style='Dark.TFrame')
378
+ self.time_frame = ttk.Frame(self.progress_section, style='Dark.TFrame')
379
+ self.current_time_label = ttk.Label(
380
+ self.time_frame,
381
+ textvariable=self.current_time_var,
382
+ style='Time.TLabel'
383
+ )
384
+ self.time_separator = ttk.Label(
385
+ self.time_frame,
386
+ text=" / ",
387
+ style='Time.TLabel'
388
+ )
389
+ self.total_time_label = ttk.Label(
390
+ self.time_frame,
391
+ textvariable=self.total_time_var,
392
+ style='Time.TLabel'
393
+ )
394
+ self.progress_bar = ttk.Scale(
395
+ self.progress_section,
396
+ from_=0,
397
+ to=100,
398
+ orient=tk.HORIZONTAL,
399
+ variable=self.progress_var,
400
+ command=self._on_seek,
401
+ style='Modern.Horizontal.TScale',
402
+ length=800
403
+ )
404
+ self.status_frame = ttk.Frame(self.player_tab, style='Dark.TFrame')
405
+ self.status_icon = ttk.Label(
406
+ self.status_frame,
407
+ text="ℹ️",
408
+ style='Status.TLabel'
409
+ )
410
+ self.status_label = ttk.Label(
411
+ self.status_frame,
412
+ textvariable=self.status_var,
413
+ style='Status.TLabel'
414
+ )
415
+ self.notebook.add(self.player_tab, text='🎵 Player')
416
+
417
+ def _create_about_tab(self):
418
+ """Create the About tab with TonieToolbox information."""
419
+ self.about_tab = ttk.Frame(self.notebook, style='Dark.TFrame', padding="15")
420
+ self.notebook.add(self.about_tab, text='ℹ️ About')
421
+ self.about_container = ttk.Frame(self.about_tab, style='Dark.TFrame')
422
+ self.about_container.pack(fill=tk.BOTH, expand=True, padx=40, pady=40)
423
+ self.about_title = ttk.Label(
424
+ self.about_container,
425
+ text="TonieToolbox",
426
+ style='Title.TLabel',
427
+ font=('Arial', 32, 'bold')
428
+ )
429
+ self.logo_frame = ttk.Frame(self.about_container, style='Dark.TFrame')
430
+ try:
431
+ icon_data = base64.b64decode(ICON_BASE64)
432
+ logo_image = None
433
+ try:
434
+ logo_image = tk.PhotoImage(data=ICON_BASE64)
435
+ logo_image = logo_image.subsample(2, 2)
436
+ except tk.TclError:
437
+ png_start = icon_data.find(b'\x89PNG\r\n\x1a\n')
438
+ if png_start > 0:
439
+ png_data = icon_data[png_start:]
440
+ png_end = png_data.find(b'IEND') + 8
441
+ if png_end > 8:
442
+ png_data = png_data[:png_end]
443
+ png_b64 = base64.b64encode(png_data).decode('ascii')
444
+ logo_image = tk.PhotoImage(data=png_b64)
445
+ logo_image = logo_image.subsample(2, 2)
446
+
447
+ if logo_image:
448
+ self.logo_label = ttk.Label(
449
+ self.logo_frame,
450
+ image=logo_image,
451
+ style='Text.TLabel'
452
+ )
453
+ self.logo_label.image = logo_image
454
+ else:
455
+ self.logo_label = ttk.Label(
456
+ self.logo_frame,
457
+ text="🦜",
458
+ style='Text.TLabel',
459
+ font=('Arial', 48)
460
+ )
461
+
462
+ except Exception as e:
463
+ logger.debug(f"Failed to load logo: {e}")
464
+ self.logo_label = ttk.Label(
465
+ self.logo_frame,
466
+ text="🦜",
467
+ style='Text.TLabel',
468
+ font=('Arial', 48)
469
+ )
470
+
471
+ self.version_frame = ttk.Frame(self.about_container, style='Dark.TFrame')
472
+ self.version_label = ttk.Label(
473
+ self.version_frame,
474
+ text=f"📋 Version: {__version__}",
475
+ style='Text.TLabel',
476
+ font=('Arial', 14)
477
+ )
478
+ self.author_frame = ttk.Frame(self.about_container, style='Dark.TFrame')
479
+ self.author_label = ttk.Label(
480
+ self.author_frame,
481
+ text="👨‍💻 Author: Quentendo64",
482
+ style='Text.TLabel',
483
+ font=('Arial', 14)
484
+ )
485
+ self.desc_frame = ttk.Frame(self.about_container, style='Dark.TFrame')
486
+ description_text = (
487
+ "A comprehensive tool for working with Tonie audio files (TAF).\n"
488
+ "Features audio playback, file analysis, and media conversion."
489
+ )
490
+ self.desc_label = ttk.Label(
491
+ self.desc_frame,
492
+ text=description_text,
493
+ style='Text.TLabel',
494
+ font=('Arial', 12),
495
+ justify=tk.CENTER,
496
+ wraplength=600
497
+ )
498
+ self.repo_frame = ttk.Frame(self.about_container, style='Dark.TFrame')
499
+ self.repo_label = ttk.Label(
500
+ self.repo_frame,
501
+ text="🔗 Repository:",
502
+ style='Text.TLabel',
503
+ font=('Arial', 14)
504
+ )
505
+ self.repo_button = ttk.Button(
506
+ self.repo_frame,
507
+ text="🌐 GitHub Repository",
508
+ command=self._open_repository,
509
+ style='Accent.TButton'
510
+ )
511
+ self.pypi_button = ttk.Button(
512
+ self.repo_frame,
513
+ text="📦 PyPI Page",
514
+ command=self._open_pypi,
515
+ style='Accent.TButton'
516
+ )
517
+ self.license_frame = ttk.Frame(self.about_container, style='Dark.TFrame')
518
+ self.license_label = ttk.Label(
519
+ self.license_frame,
520
+ text="📄 Licensed under GPL3 - see repository for details",
521
+ style='Text.TLabel',
522
+ font=('Arial', 10),
523
+ foreground=self.colors['text_medium']
524
+ )
525
+ self.attribution_frame = ttk.Frame(self.about_container, style='Dark.TFrame')
526
+ self.attribution_label = ttk.Label(
527
+ self.attribution_frame,
528
+ text="Parrot Icon created by Freepik - Flaticon",
529
+ style='Title.TLabel',
530
+ font=('Arial', 8),
531
+ foreground=self.colors['text_medium']
532
+ )
533
+ self.attribution_button = ttk.Button(
534
+ self.attribution_frame,
535
+ text="🎨 Icon Attribution",
536
+ command=self._open_attribution_link,
537
+ style='Accent.TButton'
538
+ )
539
+ self.about_title.pack(pady=(0, 20))
540
+ self.logo_frame.pack(pady=(0, 30))
541
+ self.logo_label.pack()
542
+
543
+ self.version_frame.pack(pady=10, fill=tk.X)
544
+ self.version_label.pack()
545
+
546
+ self.author_frame.pack(pady=10, fill=tk.X)
547
+ self.author_label.pack()
548
+
549
+ self.desc_frame.pack(pady=20, fill=tk.X)
550
+ self.desc_label.pack()
551
+
552
+ self.repo_frame.pack(pady=20, fill=tk.X)
553
+ self.repo_label.pack(pady=(0, 10))
554
+ self.repo_button.pack(pady=10)
555
+ self.pypi_button.pack(pady=10)
556
+ self.license_frame.pack(pady=30, fill=tk.X)
557
+ self.license_label.pack()
558
+ self.attribution_frame.pack(pady=20, fill=tk.X)
559
+ self.attribution_label.pack(pady=(0, 5))
560
+ self.attribution_button.pack()
561
+
562
+ def _open_repository(self):
563
+ """Open the TonieToolbox repository in web browser."""
564
+ import webbrowser
565
+ try:
566
+ webbrowser.open('https://github.com/Quentendo64/TonieToolbox')
567
+ self.status_var.set("🌐 Opening repository in browser...")
568
+ except Exception as e:
569
+ self.status_var.set(f"❌ Could not open browser: {str(e)}")
570
+ def _open_pypi(self):
571
+ """Open the PyPI page for TonieToolbox in web browser."""
572
+ import webbrowser
573
+ try:
574
+ webbrowser.open('https://pypi.org/project/TonieToolbox/')
575
+ self.status_var.set("🌐 Opening PyPI page in browser...")
576
+ except Exception as e:
577
+ self.status_var.set(f"❌ Could not open browser: {str(e)}")
578
+
579
+ def _open_attribution_link(self):
580
+ """Open the Flaticon attribution page in web browser."""
581
+ import webbrowser
582
+ try:
583
+ webbrowser.open('https://www.flaticon.com/free-animated-icons/parrot')
584
+ self.status_var.set("🎨 Opening icon attribution page...")
585
+ except Exception as e:
586
+ self.status_var.set(f"❌ Could not open browser: {str(e)}")
587
+
588
+ def _create_tools_tab(self):
589
+ """Create the Tools tab with conversion and utility functions."""
590
+ self.tools_tab = ttk.Frame(self.notebook, style='Dark.TFrame', padding="15")
591
+ self.notebook.add(self.tools_tab, text='🔧 Tools')
592
+ self.tools_container = ttk.Frame(self.tools_tab, style='Dark.TFrame')
593
+ self.tools_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
594
+ self.tools_notebook = ttk.Notebook(self.tools_container, style='Dark.TNotebook')
595
+ self.tools_notebook.pack(fill=tk.BOTH, expand=True)
596
+ self._create_convert_tab()
597
+ self._create_analyze_tab()
598
+ if hasattr(self, 'selected_input_file') and self.selected_input_file:
599
+ self._analyze_selected_file()
600
+
601
+ def _create_convert_tab(self):
602
+ """Create the Convert sub-tab with conversion options."""
603
+ self.convert_tab = ttk.Frame(self.tools_notebook, style='Dark.TFrame', padding="20")
604
+ self.tools_notebook.add(self.convert_tab, text='🔄 Convert')
605
+ self.input_section = ttk.LabelFrame(
606
+ self.convert_tab,
607
+ text="📁 Input File",
608
+ style='Dark.TLabelframe',
609
+ padding="15"
610
+ )
611
+ self.input_section.pack(fill=tk.X, pady=(0, 15))
612
+ self.input_file_frame = ttk.Frame(self.input_section, style='Dark.TFrame')
613
+ self.input_file_frame.pack(fill=tk.X, pady=(0, 10))
614
+ self.input_file_var = tk.StringVar()
615
+ if self.current_file:
616
+ self.input_file_var.set(self.current_file.name)
617
+ self.selected_input_file = str(self.current_file)
618
+ else:
619
+ self.input_file_var.set("No file selected")
620
+ self.input_file_label = ttk.Label(
621
+ self.input_file_frame,
622
+ textvariable=self.input_file_var,
623
+ style='Info.TLabel'
624
+ )
625
+ self.input_file_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
626
+ self.browse_button = ttk.Button(
627
+ self.input_file_frame,
628
+ text="📂 Browse File",
629
+ command=self._browse_input_file,
630
+ style='Accent.TButton'
631
+ )
632
+ self.browse_button.pack(side=tk.RIGHT, padx=(10, 0))
633
+ self.convert_options = ttk.LabelFrame(
634
+ self.convert_tab,
635
+ text="⚙️ Conversion Options",
636
+ style='Dark.TLabelframe',
637
+ padding="15"
638
+ )
639
+ self.convert_options.pack(fill=tk.X, pady=(0, 15))
640
+ self.convert_type_frame = ttk.Frame(self.convert_options, style='Dark.TFrame')
641
+ self.convert_type_frame.pack(fill=tk.X, pady=(0, 15))
642
+ ttk.Label(
643
+ self.convert_type_frame,
644
+ text="Convert to:",
645
+ style='Text.TLabel'
646
+ ).pack(side=tk.LEFT)
647
+
648
+ self.convert_type_var = tk.StringVar(value="separate_mp3")
649
+ self.mp3_separate_radio = ttk.Radiobutton(
650
+ self.convert_type_frame,
651
+ text="🎵 Separate MP3 tracks",
652
+ variable=self.convert_type_var,
653
+ value="separate_mp3",
654
+ style='Dark.TRadiobutton'
655
+ )
656
+ self.mp3_separate_radio.pack(anchor=tk.W, pady=2)
657
+ self.mp3_single_radio = ttk.Radiobutton(
658
+ self.convert_type_frame,
659
+ text="🎵 Single MP3 file",
660
+ variable=self.convert_type_var,
661
+ value="single_mp3",
662
+ style='Dark.TRadiobutton'
663
+ )
664
+ self.mp3_single_radio.pack(anchor=tk.W, pady=2)
665
+ self.opus_radio = ttk.Radiobutton(
666
+ self.convert_type_frame,
667
+ text="🎶 Opus tracks",
668
+ variable=self.convert_type_var,
669
+ value="opus",
670
+ style='Dark.TRadiobutton'
671
+ )
672
+ self.opus_radio.pack(anchor=tk.W, pady=2)
673
+ self.output_dir_frame = ttk.Frame(self.convert_options, style='Dark.TFrame')
674
+ self.output_dir_frame.pack(fill=tk.X, pady=(0, 15))
675
+ ttk.Label(
676
+ self.output_dir_frame,
677
+ text="Output directory:",
678
+ style='Text.TLabel'
679
+ ).pack(anchor=tk.W)
680
+
681
+ self.output_dir_var = tk.StringVar()
682
+ self.output_dir_var.set(os.path.join(os.getcwd(), "output"))
683
+ self.output_dir_display_frame = ttk.Frame(self.output_dir_frame, style='Dark.TFrame')
684
+ self.output_dir_display_frame.pack(fill=tk.X, pady=(5, 0))
685
+ self.output_dir_label = ttk.Label(
686
+ self.output_dir_display_frame,
687
+ textvariable=self.output_dir_var,
688
+ style='Info.TLabel'
689
+ )
690
+ self.output_dir_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
691
+ self.output_browse_button = ttk.Button(
692
+ self.output_dir_display_frame,
693
+ text="📁 Browse Folder",
694
+ command=self._browse_output_dir,
695
+ style='Accent.TButton'
696
+ )
697
+ self.output_browse_button.pack(side=tk.RIGHT, padx=(5, 0))
698
+ self.convert_action_frame = ttk.Frame(self.convert_options, style='Dark.TFrame')
699
+ self.convert_action_frame.pack(fill=tk.X)
700
+ self.convert_button = ttk.Button(
701
+ self.convert_action_frame,
702
+ text="🔄 Start Conversion",
703
+ command=self._start_conversion,
704
+ style='Large.TButton'
705
+ )
706
+ self.convert_button.pack(pady=10)
707
+ self.convert_progress = ttk.Progressbar(
708
+ self.convert_action_frame,
709
+ mode='indeterminate',
710
+ style='Modern.Horizontal.TProgressbar'
711
+ )
712
+ self.convert_progress.pack(fill=tk.X, pady=(10, 0))
713
+ self.convert_status_var = tk.StringVar()
714
+ self.convert_status_var.set("Ready to convert")
715
+ self.convert_status_label = ttk.Label(
716
+ self.convert_action_frame,
717
+ textvariable=self.convert_status_var,
718
+ style='Status.TLabel'
719
+ )
720
+ self.convert_status_label.pack(pady=(5, 0))
721
+
722
+ def _create_analyze_tab(self):
723
+ """Create the Analyze sub-tab with file analysis tools."""
724
+ self.analyze_tab = ttk.Frame(self.tools_notebook, style='Dark.TFrame', padding="20")
725
+ self.tools_notebook.add(self.analyze_tab, text='🔍 Analyze')
726
+ self.analyze_info = ttk.LabelFrame(
727
+ self.analyze_tab,
728
+ text="📊 File Analysis",
729
+ style='Dark.TLabelframe',
730
+ padding="15"
731
+ )
732
+ self.analyze_info.pack(fill=tk.BOTH, expand=True)
733
+ self.analyze_text = tk.Text(
734
+ self.analyze_info,
735
+ height=20,
736
+ width=80,
737
+ state=tk.DISABLED,
738
+ wrap=tk.WORD,
739
+ bg=self.colors['bg_light'],
740
+ fg=self.colors['text_medium'],
741
+ font=('Consolas', 10),
742
+ borderwidth=0,
743
+ highlightthickness=0,
744
+ padx=15,
745
+ pady=10
746
+ )
747
+ self.analyze_scrollbar = ttk.Scrollbar(
748
+ self.analyze_info,
749
+ orient=tk.VERTICAL,
750
+ command=self.analyze_text.yview
751
+ )
752
+ self.analyze_text.config(yscrollcommand=self.analyze_scrollbar.set)
753
+ self.analyze_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
754
+ self.analyze_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
755
+ self._update_analyze_text("Select a TAF file to see detailed analysis information.")
756
+
757
+ def _browse_input_file(self):
758
+ """Browse for input TAF file."""
759
+ file_path = filedialog.askopenfilename(
760
+ title="Select TAF File for Conversion",
761
+ filetypes=[("TAF files", "*.taf"), ("All files", "*.*")],
762
+ initialdir=os.getcwd()
763
+ )
764
+
765
+ if file_path:
766
+ self.input_file_var.set(os.path.basename(file_path))
767
+ self.selected_input_file = file_path
768
+ self._analyze_selected_file()
769
+
770
+ def _update_tools_input_file(self, file_path: str):
771
+ """Update the Tools tab input file when a file is loaded in the Player tab."""
772
+ if hasattr(self, 'input_file_var') and hasattr(self, 'selected_input_file'):
773
+ self.input_file_var.set(os.path.basename(file_path))
774
+ self.selected_input_file = file_path
775
+ self._analyze_selected_file()
776
+
777
+ def _browse_output_dir(self):
778
+ """Browse for output directory."""
779
+ dir_path = filedialog.askdirectory(
780
+ title="Select Output Directory",
781
+ initialdir=self.output_dir_var.get()
782
+ )
783
+
784
+ if dir_path:
785
+ self.output_dir_var.set(dir_path)
786
+
787
+ def _start_conversion(self):
788
+ """Start the conversion process in a separate thread."""
789
+ if not hasattr(self, 'selected_input_file'):
790
+ messagebox.showerror("Error", "Please select an input TAF file first.")
791
+ return
792
+
793
+ if not os.path.exists(self.selected_input_file):
794
+ messagebox.showerror("Error", "Selected input file does not exist.")
795
+ return
796
+ output_dir = self.output_dir_var.get()
797
+ os.makedirs(output_dir, exist_ok=True)
798
+ self.convert_button.config(state=tk.DISABLED)
799
+ self.convert_progress.start()
800
+ self.convert_status_var.set("Converting...")
801
+ threading.Thread(
802
+ target=self._perform_conversion,
803
+ daemon=True
804
+ ).start()
805
+
806
+ def _perform_conversion(self):
807
+ """Perform the actual conversion."""
808
+ try:
809
+ convert_type = self.convert_type_var.get()
810
+ input_file = self.selected_input_file
811
+ output_dir = self.output_dir_var.get()
812
+ base_name = os.path.splitext(os.path.basename(input_file))[0]
813
+
814
+ if convert_type == "separate_mp3":
815
+ extract_to_mp3_files(input_file, output_dir)
816
+ self.root.after(0, lambda: self.convert_status_var.set("✅ Converted to separate MP3 files"))
817
+ elif convert_type == "single_mp3":
818
+ output_file = os.path.join(output_dir, f"{base_name}.mp3")
819
+ extract_full_audio_to_mp3(input_file, output_file)
820
+ self.root.after(0, lambda: self.convert_status_var.set("✅ Converted to single MP3 file"))
821
+ elif convert_type == "opus":
822
+ split_to_opus_files(input_file, output_dir)
823
+ self.root.after(0, lambda: self.convert_status_var.set("✅ Converted to Opus files"))
824
+
825
+ except Exception as e:
826
+ error_msg = f"❌ Conversion failed: {str(e)}"
827
+ self.root.after(0, lambda: self.convert_status_var.set(error_msg))
828
+ logger.error(f"Conversion error: {e}")
829
+
830
+ finally:
831
+ self.root.after(0, self._conversion_finished)
832
+
833
+ def _conversion_finished(self):
834
+ """Called when conversion is finished to update UI."""
835
+ self.convert_button.config(state=tk.NORMAL)
836
+ self.convert_progress.stop()
837
+
838
+ def _analyze_selected_file(self):
839
+ """Analyze the selected file and update the analyze tab."""
840
+ if hasattr(self, 'selected_input_file'):
841
+ try:
842
+ from .tonie_analysis import check_tonie_file
843
+ file_info = check_tonie_file(self.selected_input_file)
844
+ analysis_text = f"""File Analysis: {os.path.basename(self.selected_input_file)}
845
+ {'='*60}
846
+
847
+ File Path: {self.selected_input_file}
848
+ File Size: {os.path.getsize(self.selected_input_file):,} bytes
849
+
850
+ {file_info}
851
+
852
+ Analysis completed at: {time.strftime('%Y-%m-%d %H:%M:%S')}
853
+ """
854
+
855
+ self._update_analyze_text(analysis_text)
856
+
857
+ except Exception as e:
858
+ error_text = f"""Analysis Error
859
+ {'='*60}
860
+
861
+ Could not analyze file: {self.selected_input_file}
862
+ Error: {str(e)}
863
+
864
+ Please ensure the file is a valid TAF file.
865
+ """
866
+ self._update_analyze_text(error_text)
867
+ logger.error(f"Analysis error: {e}")
868
+
869
+ def _update_analyze_text(self, text):
870
+ """Update the analyze text widget with new content."""
871
+ self.analyze_text.config(state=tk.NORMAL)
872
+ self.analyze_text.delete(1.0, tk.END)
873
+ self.analyze_text.insert(1.0, text)
874
+ self.analyze_text.config(state=tk.DISABLED)
875
+
876
+ def _setup_layout(self):
877
+ """Setup the modern layout with tabbed interface."""
878
+ self.main_frame.pack(fill=tk.BOTH, expand=True)
879
+ self.header_frame.pack(fill=tk.X, pady=(0, 20))
880
+ #self.title_label.pack()
881
+ self.notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 20))
882
+ self._layout_player_tab()
883
+
884
+ def _layout_player_tab(self):
885
+ """Layout widgets within the player tab."""
886
+ self.file_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
887
+ self.file_display_frame.pack(fill=tk.X, pady=(0, 15))
888
+ self.file_icon_label.pack(side=tk.LEFT, padx=(0, 10))
889
+ self.file_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
890
+ self.load_button.pack(pady=(0, 15))
891
+ self.details_frame.pack(fill=tk.BOTH, expand=True)
892
+ self.details_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
893
+ self.details_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
894
+ self.controls_frame.pack(fill=tk.X, pady=(0, 15))
895
+ self.button_frame.pack(pady=(0, 20))
896
+ self.play_button.pack(side=tk.LEFT, padx=(0, 15))
897
+ self.stop_button.pack(side=tk.LEFT)
898
+ # Hide chapter buttons - feature not is implemented yet
899
+ # self.chapter_frame.pack(pady=(0, 20))
900
+ # self.prev_chapter_button.pack(side=tk.LEFT, padx=(0, 15))
901
+ # self.next_chapter_button.pack(side=tk.LEFT)
902
+ self.progress_section.pack(fill=tk.X)
903
+ self.time_frame.pack(pady=(0, 10))
904
+ self.current_time_label.pack(side=tk.LEFT)
905
+ self.time_separator.pack(side=tk.LEFT)
906
+ self.total_time_label.pack(side=tk.LEFT)
907
+ self.progress_bar.pack(fill=tk.X, padx=40)
908
+ self.status_frame.pack(fill=tk.X, pady=(20, 0), padx=10)
909
+ self.status_icon.pack(side=tk.LEFT, padx=(0, 5))
910
+ self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
911
+
912
+ def _load_file(self):
913
+ """Load a TAF file using file dialog."""
914
+ file_path = filedialog.askopenfilename(
915
+ title="Select TAF File",
916
+ filetypes=[("TAF files", "*.taf"), ("All files", "*.*")],
917
+ initialdir=os.getcwd()
918
+ )
919
+
920
+ if file_path:
921
+ self.load_taf_file(file_path)
922
+
923
+ def load_taf_file(self, file_path: str):
924
+ """
925
+ Load a TAF file for playback.
926
+
927
+ Args:
928
+ file_path: Path to the TAF file to load
929
+ """
930
+ try:
931
+ self.status_var.set("📂 Loading TAF file...")
932
+ self.root.update()
933
+ if self.player:
934
+ self._stop_playback()
935
+ self.player.cleanup()
936
+ self.player = TAFPlayer()
937
+ self.player.load(file_path)
938
+
939
+ self.current_file = Path(file_path)
940
+ self.file_name_var.set(self.current_file.name)
941
+ self._update_file_info()
942
+ self.play_button.config(state=tk.NORMAL)
943
+ self.stop_button.config(state=tk.NORMAL)
944
+ if self.player.taf_info and self.player.taf_info.get('chapters'):
945
+ self.prev_chapter_button.config(state=tk.NORMAL)
946
+ self.next_chapter_button.config(state=tk.NORMAL)
947
+ if self.player.total_duration > 0:
948
+ self.total_time_var.set(self._format_time(self.player.total_duration))
949
+ self.progress_bar.config(to=self.player.total_duration)
950
+
951
+ self.status_var.set(f"✅ Successfully loaded: {self.current_file.name}")
952
+ logger.info(f"Successfully loaded TAF file: {file_path}")
953
+ self._update_tools_input_file(file_path)
954
+
955
+ except TAFPlayerError as e:
956
+ messagebox.showerror("❌ Loading Error", f"Failed to load TAF file:\n\n{e}")
957
+ self.status_var.set("❌ Error loading file")
958
+ logger.error(f"Failed to load TAF file: {e}")
959
+ except Exception as e:
960
+ messagebox.showerror("❌ Unexpected Error", f"An unexpected error occurred:\n\n{e}")
961
+ self.status_var.set("❌ Unexpected error")
962
+ logger.error(f"Unexpected error loading TAF file: {e}")
963
+
964
+ def _update_file_info(self):
965
+ """Update the file information display with enhanced formatting."""
966
+ if not self.player or not self.player.taf_info:
967
+ return
968
+
969
+ info = self.player.taf_info
970
+ info_lines = []
971
+ info_lines.append("📋 FILE INFORMATION")
972
+ info_lines.append("─" * 50)
973
+
974
+ if 'sha1_hash' in info and info['sha1_hash']:
975
+ info_lines.append(f"🔐 SHA1 Hash: {info['sha1_hash']}")
976
+
977
+ if 'sample_rate' in info:
978
+ info_lines.append(f"🎵 Sample Rate: {info['sample_rate']:,} Hz")
979
+
980
+ if 'channels' in info:
981
+ channel_text = "Stereo" if info['channels'] == 2 else f"{info['channels']} Channels"
982
+ info_lines.append(f"🔊 Audio: {channel_text}")
983
+
984
+ if 'bitrate' in info:
985
+ info_lines.append(f"📊 Bitrate: {info['bitrate']} kbps")
986
+
987
+ if self.player.total_duration > 0:
988
+ info_lines.append(f"⏱️ Duration: {self._format_time(self.player.total_duration)}")
989
+
990
+ if 'file_size' in info:
991
+ file_size_mb = info['file_size'] / (1024 * 1024)
992
+ info_lines.append(f"💾 File Size: {file_size_mb:.2f} MB")
993
+ if 'chapters' in info and info['chapters']:
994
+ info_lines.append("")
995
+ info_lines.append("📚 CHAPTERS")
996
+ info_lines.append("─" * 50)
997
+ info_lines.append(f"Total Chapters: {len(info['chapters'])}")
998
+ info_lines.append("")
999
+
1000
+ for i, chapter in enumerate(info['chapters'][:8]):
1001
+ start_time = self._format_time(chapter.get('start', 0))
1002
+ title = chapter.get('title', f'Chapter {i+1}')
1003
+ info_lines.append(f" {i+1:2d}. {title:<30} [{start_time}]")
1004
+
1005
+ if len(info['chapters']) > 8:
1006
+ remaining = len(info['chapters']) - 8
1007
+ info_lines.append(f" ... and {remaining} more chapter{'s' if remaining > 1 else ''}")
1008
+ else:
1009
+ info_lines.append("")
1010
+ info_lines.append("📚 CHAPTERS")
1011
+ info_lines.append("─" * 50)
1012
+ info_lines.append("No chapter information available")
1013
+ self.details_text.config(state=tk.NORMAL)
1014
+ self.details_text.delete(1.0, tk.END)
1015
+ self.details_text.insert(1.0, '\n'.join(info_lines))
1016
+ self.details_text.config(state=tk.DISABLED)
1017
+
1018
+ def _toggle_play(self):
1019
+ """Toggle play/pause."""
1020
+ if not self.player:
1021
+ return
1022
+
1023
+ try:
1024
+ if not self.is_playing:
1025
+ current_pos = self.progress_var.get()
1026
+ self.player.play(start_time=current_pos)
1027
+ self.is_playing = True
1028
+ self.is_paused = False
1029
+ self.play_button.config(text="⏸️ Pause")
1030
+ self.status_var.set("▶️ Playing audio...")
1031
+ self._start_update_thread()
1032
+
1033
+ elif self.is_playing and not self.is_paused:
1034
+ self.player.pause()
1035
+ self.is_paused = True
1036
+ self.play_button.config(text="▶️ Resume")
1037
+ self.status_var.set("⏸️ Playback paused")
1038
+
1039
+ elif self.is_paused:
1040
+ self.player.resume()
1041
+ self.is_paused = False
1042
+ self.play_button.config(text="⏸️ Pause")
1043
+ self.status_var.set("▶️ Playing audio...")
1044
+
1045
+ except TAFPlayerError as e:
1046
+ messagebox.showerror("🚫 Playback Error", f"Unable to control playback:\n\n{e}")
1047
+ self.status_var.set("❌ Playback error occurred")
1048
+ logger.error(f"Playback error: {e}")
1049
+
1050
+ def _stop_playback(self):
1051
+ """Stop playback."""
1052
+ if not self.player:
1053
+ return
1054
+
1055
+ try:
1056
+ self.player.stop()
1057
+ self.is_playing = False
1058
+ self.is_paused = False
1059
+ self.play_button.config(text="▶️ Play")
1060
+ self.status_var.set("⏹️ Playback stopped")
1061
+ self._stop_update_thread()
1062
+ self.progress_var.set(0)
1063
+ self.current_time_var.set("00:00")
1064
+
1065
+ except TAFPlayerError as e:
1066
+ logger.error(f"Stop error: {e}")
1067
+
1068
+ def _on_seek(self, value):
1069
+ """Handle seek bar changes."""
1070
+ if not self.player or not self.is_playing:
1071
+ return
1072
+
1073
+ try:
1074
+ seek_time = float(value)
1075
+ self.player.seek(seek_time)
1076
+ except (TAFPlayerError, ValueError) as e:
1077
+ logger.error(f"Seek error: {e}")
1078
+
1079
+ def _prev_chapter(self):
1080
+ """Go to previous chapter."""
1081
+ # Implementation would depend on chapter navigation support in TAFPlayer
1082
+ pass
1083
+
1084
+ def _next_chapter(self):
1085
+ """Go to next chapter."""
1086
+ # Implementation would depend on chapter navigation support in TAFPlayer
1087
+ pass
1088
+
1089
+ def _start_update_thread(self):
1090
+ """Start the progress update thread."""
1091
+ if self.update_thread and self.update_thread.is_alive():
1092
+ return
1093
+
1094
+ self.stop_updates.clear()
1095
+ self.update_thread = threading.Thread(target=self._update_progress, daemon=True)
1096
+ self.update_thread.start()
1097
+
1098
+ def _stop_update_thread(self):
1099
+ """Stop the progress update thread."""
1100
+ self.stop_updates.set()
1101
+ if self.update_thread and self.update_thread.is_alive():
1102
+ self.update_thread.join(timeout=1.0)
1103
+
1104
+ def _update_progress(self):
1105
+ """Update progress bar and time display (runs in separate thread)."""
1106
+ while not self.stop_updates.is_set() and self.is_playing:
1107
+ try:
1108
+ if self.player:
1109
+ status = self.player.get_status()
1110
+ current_time = status.get('current_position', 0)
1111
+ self.root.after(0, self._update_progress_gui, current_time)
1112
+
1113
+ time.sleep(0.1)
1114
+
1115
+ except Exception as e:
1116
+ logger.error(f"Progress update error: {e}")
1117
+ break
1118
+
1119
+ def _update_progress_gui(self, current_time: float):
1120
+ """Update GUI progress elements (called from main thread)."""
1121
+ self.progress_var.set(current_time)
1122
+ self.current_time_var.set(self._format_time(current_time))
1123
+ if self.player and current_time >= self.player.total_duration:
1124
+ self._stop_playback()
1125
+
1126
+ def _format_time(self, seconds: float) -> str:
1127
+ """Format time in MM:SS or HH:MM:SS format."""
1128
+ if seconds < 0:
1129
+ return "00:00"
1130
+
1131
+ hours = int(seconds // 3600)
1132
+ minutes = int((seconds % 3600) // 60)
1133
+ secs = int(seconds % 60)
1134
+
1135
+ if hours > 0:
1136
+ return f"{hours:02d}:{minutes:02d}:{secs:02d}"
1137
+ else:
1138
+ return f"{minutes:02d}:{secs:02d}"
1139
+
1140
+ def _on_close(self):
1141
+ """Handle window close event."""
1142
+ try:
1143
+ self._stop_playback()
1144
+ self._stop_update_thread()
1145
+
1146
+ if self.player:
1147
+ self.player.cleanup()
1148
+
1149
+ self.root.destroy()
1150
+
1151
+ except Exception as e:
1152
+ logger.error(f"Error during cleanup: {e}")
1153
+ self.root.destroy()
1154
+
1155
+ def run(self, taf_file_path: Optional[str] = None):
1156
+ """
1157
+ Start the GUI player.
1158
+
1159
+ Args:
1160
+ taf_file_path: Optional path to a TAF file to load on startup
1161
+ """
1162
+ if taf_file_path:
1163
+ self.root.after(100, lambda: self.load_taf_file(taf_file_path))
1164
+
1165
+ try:
1166
+ self.root.mainloop()
1167
+ except KeyboardInterrupt:
1168
+ logger.info("GUI player interrupted by user")
1169
+ except Exception as e:
1170
+ logger.error(f"GUI player error: {e}")
1171
+ raise
1172
+
1173
+
1174
+ def gui_player(taf_file_path: Optional[str] = None) -> None:
1175
+ """
1176
+ Start the GUI TAF player.
1177
+
1178
+ Args:
1179
+ taf_file_path: Optional path to the TAF file to play. If None, user can load via dialog.
1180
+ """
1181
+
1182
+ if not TKINTER_AVAILABLE:
1183
+ print("Error: GUI player requires tkinter, which is not available on this system.")
1184
+ print(f"Tkinter error: {TKINTER_ERROR}")
1185
+ print("\nTo use the GUI player, you need to install tkinter support:")
1186
+ print(" - On Ubuntu/Debian: sudo apt-get install python3-tk")
1187
+ print(" - On Fedora/RHEL: sudo dnf install tkinter")
1188
+ print(" - On Arch Linux: sudo pacman -S tk")
1189
+ print(" - On macOS: tkinter should be included with Python")
1190
+ print(" - On Windows: tkinter should be included with Python")
1191
+ print("\nAlternatively, use the command-line player with: --play")
1192
+ return
1193
+
1194
+ try:
1195
+ logger.info(f"Starting GUI player for: {taf_file_path}")
1196
+ if not os.path.exists(taf_file_path):
1197
+ print(f"Error: TAF file not found: {taf_file_path}")
1198
+ return
1199
+ player_gui = TAFPlayerGUI()
1200
+ player_gui.run(taf_file_path)
1201
+
1202
+ except Exception as e:
1203
+ logger.error(f"Failed to start GUI player: {e}")
1204
+ print(f"Error starting GUI player: {e}")
1205
+
1206
+
1207
+ if __name__ == "__main__":
1208
+ if len(sys.argv) > 1:
1209
+ gui_player(sys.argv[1])
1210
+ else:
1211
+ player_gui = TAFPlayerGUI()
1212
+ player_gui.run()