plex-generate-previews 2.0.0__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.
- plex_generate_previews/__init__.py +10 -0
- plex_generate_previews/__main__.py +11 -0
- plex_generate_previews/cli.py +474 -0
- plex_generate_previews/config.py +479 -0
- plex_generate_previews/gpu_detection.py +541 -0
- plex_generate_previews/media_processing.py +439 -0
- plex_generate_previews/plex_client.py +211 -0
- plex_generate_previews/utils.py +135 -0
- plex_generate_previews/version_check.py +178 -0
- plex_generate_previews/worker.py +478 -0
- plex_generate_previews-2.0.0.dist-info/METADATA +728 -0
- plex_generate_previews-2.0.0.dist-info/RECORD +15 -0
- plex_generate_previews-2.0.0.dist-info/WHEEL +5 -0
- plex_generate_previews-2.0.0.dist-info/entry_points.txt +2 -0
- plex_generate_previews-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,10 @@
|
|
1
|
+
"""
|
2
|
+
Plex Video Preview Generator
|
3
|
+
|
4
|
+
A tool for generating video preview thumbnails for Plex Media Server.
|
5
|
+
Supports GPU acceleration (NVIDIA, AMD, Intel, WSL2) and CPU processing.
|
6
|
+
"""
|
7
|
+
|
8
|
+
__version__ = "2.0.0"
|
9
|
+
__author__ = "stevezau"
|
10
|
+
__description__ = "Generate video preview thumbnails for Plex Media Server"
|
@@ -0,0 +1,474 @@
|
|
1
|
+
"""
|
2
|
+
Command-line interface for Plex Video Preview Generator.
|
3
|
+
|
4
|
+
Main entry point that orchestrates all components: configuration,
|
5
|
+
GPU detection, Plex connection, and worker pool management.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
import sys
|
10
|
+
import shutil
|
11
|
+
import signal
|
12
|
+
import argparse
|
13
|
+
from loguru import logger
|
14
|
+
from rich.console import Console
|
15
|
+
from rich.progress import Progress, SpinnerColumn, MofNCompleteColumn, ProgressColumn, BarColumn, TextColumn, TimeElapsedColumn
|
16
|
+
from rich.live import Live
|
17
|
+
from rich.console import Group
|
18
|
+
from rich.text import Text
|
19
|
+
|
20
|
+
from .config import Config, load_config
|
21
|
+
from .gpu_detection import detect_all_gpus, format_gpu_info
|
22
|
+
from .plex_client import plex_server, get_library_sections
|
23
|
+
from .worker import WorkerPool
|
24
|
+
from .utils import calculate_title_width, setup_working_directory as create_working_directory
|
25
|
+
from .version_check import check_for_updates
|
26
|
+
|
27
|
+
# Shared console for coordinated logging and progress output
|
28
|
+
console = Console()
|
29
|
+
|
30
|
+
|
31
|
+
class ApplicationState:
|
32
|
+
"""Global application state for signal handling and cleanup."""
|
33
|
+
def __init__(self):
|
34
|
+
self.config = None
|
35
|
+
self.console = console
|
36
|
+
|
37
|
+
def set_config(self, config):
|
38
|
+
"""Set the configuration object."""
|
39
|
+
self.config = config
|
40
|
+
|
41
|
+
def cleanup(self):
|
42
|
+
"""Perform cleanup operations."""
|
43
|
+
# Restore terminal cursor visibility
|
44
|
+
if self.console:
|
45
|
+
self.console.show_cursor(True)
|
46
|
+
|
47
|
+
# Clean up working tmp folder if it exists
|
48
|
+
try:
|
49
|
+
if self.config and hasattr(self.config, 'working_tmp_folder'):
|
50
|
+
if os.path.isdir(self.config.working_tmp_folder):
|
51
|
+
shutil.rmtree(self.config.working_tmp_folder)
|
52
|
+
logger.debug(f"Cleaned up working temp folder: {self.config.working_tmp_folder}")
|
53
|
+
except Exception as cleanup_error:
|
54
|
+
logger.warning(f"Failed to clean up working temp folder during interrupt: {cleanup_error}")
|
55
|
+
|
56
|
+
|
57
|
+
# Global application state
|
58
|
+
app_state = ApplicationState()
|
59
|
+
|
60
|
+
|
61
|
+
class AnimatedBarColumn(BarColumn):
|
62
|
+
"""Custom animated progress bar with scrolling red bars."""
|
63
|
+
|
64
|
+
def __init__(self, bar_width=None, style="green", complete_style="red", finished_style="green"):
|
65
|
+
super().__init__(bar_width=bar_width, style=style)
|
66
|
+
self.complete_style = complete_style
|
67
|
+
self.finished_style = finished_style
|
68
|
+
self._animation_offset = 0
|
69
|
+
|
70
|
+
def render(self, task):
|
71
|
+
"""Render the animated progress bar."""
|
72
|
+
if task.total is None or task.total == 0:
|
73
|
+
return Text("", style=self.style)
|
74
|
+
|
75
|
+
# Calculate progress
|
76
|
+
progress = task.completed / task.total
|
77
|
+
completed_width = int(progress * (self.bar_width or 40))
|
78
|
+
|
79
|
+
# Create the base bar
|
80
|
+
bar_text = "█" * completed_width + "░" * ((self.bar_width or 40) - completed_width)
|
81
|
+
|
82
|
+
# Add animated red bars for incomplete portion
|
83
|
+
if completed_width < (self.bar_width or 40):
|
84
|
+
# Create scrolling red bars effect
|
85
|
+
remaining_width = (self.bar_width or 40) - completed_width
|
86
|
+
red_bars = "█" * min(3, remaining_width) # 3-character red bars
|
87
|
+
|
88
|
+
# Animate the red bars position
|
89
|
+
self._animation_offset = (self._animation_offset + 1) % max(1, remaining_width - 2)
|
90
|
+
|
91
|
+
# Insert red bars at animated position
|
92
|
+
if remaining_width > 3:
|
93
|
+
bar_list = list(bar_text)
|
94
|
+
start_pos = completed_width + self._animation_offset
|
95
|
+
for i, char in enumerate(red_bars):
|
96
|
+
pos = start_pos + i
|
97
|
+
if pos < len(bar_list) and bar_list[pos] == "░":
|
98
|
+
bar_list[pos] = char
|
99
|
+
bar_text = "".join(bar_list)
|
100
|
+
|
101
|
+
# Apply styling
|
102
|
+
if task.finished:
|
103
|
+
style = self.finished_style
|
104
|
+
else:
|
105
|
+
style = self.complete_style if completed_width > 0 else self.style
|
106
|
+
|
107
|
+
return Text(bar_text, style=style)
|
108
|
+
|
109
|
+
|
110
|
+
class FFmpegDataColumn(ProgressColumn):
|
111
|
+
"""Custom column to display FFmpeg data for worker progress bars."""
|
112
|
+
|
113
|
+
def render(self, task):
|
114
|
+
# Get FFmpeg data from task fields
|
115
|
+
frame = task.fields.get("frame", 0)
|
116
|
+
fps = task.fields.get("fps", 0)
|
117
|
+
time_str = task.fields.get("time_str", "00:00:00.00")
|
118
|
+
speed = task.fields.get("speed", "0.0x")
|
119
|
+
|
120
|
+
# Create simplified FFmpeg-style output with only essential info
|
121
|
+
if frame > 0 or fps > 0:
|
122
|
+
ffmpeg_data = f"frame={frame:4d} fps={fps:4.1f} time={time_str} speed={speed}"
|
123
|
+
return Text(ffmpeg_data, style="dim")
|
124
|
+
else:
|
125
|
+
return Text("Waiting for FFmpeg data...", style="dim")
|
126
|
+
|
127
|
+
|
128
|
+
def parse_arguments() -> argparse.Namespace:
|
129
|
+
"""Parse command-line arguments."""
|
130
|
+
parser = argparse.ArgumentParser(
|
131
|
+
description='Generate video preview thumbnails for Plex Media Server'
|
132
|
+
)
|
133
|
+
|
134
|
+
# Plex server configuration
|
135
|
+
parser.add_argument('--plex-url', help='Plex server URL (e.g., http://localhost:32400)')
|
136
|
+
parser.add_argument('--plex-token', help='Plex authentication token (get from https://support.plex.tv/articles/204059436/)')
|
137
|
+
parser.add_argument('--plex-timeout', type=int, help='Plex API timeout in seconds (default: 60)')
|
138
|
+
parser.add_argument('--plex-libraries', help='Comma-separated list of library names (e.g., "Movies, TV Shows")')
|
139
|
+
|
140
|
+
# Media paths
|
141
|
+
parser.add_argument('--plex-config-folder', help='Path to Plex Media Server configuration folder (e.g., /path_to/plex/Library/Application Support/Plex Media Server)')
|
142
|
+
parser.add_argument('--plex-local-videos-path-mapping', help='Local videos path mapping (e.g., /path/this/script/sees/to/video/library)')
|
143
|
+
parser.add_argument('--plex-videos-path-mapping', help='Plex videos path mapping (e.g., /path/plex/sees/to/video/library)')
|
144
|
+
|
145
|
+
# Processing configuration
|
146
|
+
parser.add_argument('--plex-bif-frame-interval', type=int, help='Interval between preview images in seconds (default: 5)')
|
147
|
+
parser.add_argument('--thumbnail-quality', type=int, help='Preview image quality 1-10 (default: 4, 2=highest quality, 10=lowest quality)')
|
148
|
+
parser.add_argument('--regenerate-thumbnails', action='store_true', help='Regenerate existing thumbnails (default: false)')
|
149
|
+
|
150
|
+
# Threading configuration
|
151
|
+
parser.add_argument('--gpu-threads', type=int, help='Number of GPU worker threads (default: 4)')
|
152
|
+
parser.add_argument('--cpu-threads', type=int, help='Number of CPU worker threads (default: 4)')
|
153
|
+
parser.add_argument('--gpu-selection', help='GPU selection: "all" or comma-separated indices like "0,1,2" (default: all)')
|
154
|
+
parser.add_argument('--list-gpus', action='store_true', help='List detected GPUs and exit')
|
155
|
+
|
156
|
+
# System paths
|
157
|
+
parser.add_argument('--tmp-folder', help='Temporary folder for processing (default: /tmp/plex_generate_previews)')
|
158
|
+
|
159
|
+
# Logging
|
160
|
+
parser.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'debug', 'info', 'warning', 'error'], help='Logging level (default: INFO)')
|
161
|
+
|
162
|
+
# Version check
|
163
|
+
parser.add_argument('--skip-version-check', action='store_true', help='Skip checking for newer versions on startup')
|
164
|
+
|
165
|
+
return parser.parse_args()
|
166
|
+
|
167
|
+
|
168
|
+
def setup_logging(log_level: str = 'INFO') -> None:
|
169
|
+
"""Set up logging configuration with shared Rich console."""
|
170
|
+
logger.remove()
|
171
|
+
logger.add(
|
172
|
+
lambda msg: console.print(msg, end=''),
|
173
|
+
level=log_level,
|
174
|
+
format='<green>{time:YYYY/MM/DD HH:mm:ss}</green> | {level.icon} - <level>{message}</level>',
|
175
|
+
enqueue=True
|
176
|
+
)
|
177
|
+
|
178
|
+
|
179
|
+
def signal_handler(signum, frame):
|
180
|
+
"""Handle interrupt signals gracefully."""
|
181
|
+
logger.info("Received interrupt signal, shutting down gracefully...")
|
182
|
+
|
183
|
+
# Perform cleanup using global application state
|
184
|
+
app_state.cleanup()
|
185
|
+
|
186
|
+
sys.exit(0)
|
187
|
+
|
188
|
+
|
189
|
+
def list_gpus() -> None:
|
190
|
+
"""List detected GPUs and exit."""
|
191
|
+
logger.info('🔍 Detecting available GPUs...')
|
192
|
+
|
193
|
+
detected_gpus = detect_all_gpus()
|
194
|
+
|
195
|
+
if not detected_gpus:
|
196
|
+
logger.info('❌ No GPUs detected')
|
197
|
+
logger.info('💡 Use --cpu-threads to run with CPU-only processing')
|
198
|
+
return
|
199
|
+
|
200
|
+
logger.info(f'✅ Found {len(detected_gpus)} GPU(s):')
|
201
|
+
for i, (gpu_type, gpu_device, gpu_info) in enumerate(detected_gpus):
|
202
|
+
gpu_name = gpu_info.get('name', f'{gpu_type} GPU')
|
203
|
+
gpu_desc = format_gpu_info(gpu_type, gpu_device, gpu_name)
|
204
|
+
logger.info(f' [{i}] {gpu_desc}')
|
205
|
+
|
206
|
+
logger.info('')
|
207
|
+
logger.info('💡 Use --gpu-selection "0,1" to select specific GPUs')
|
208
|
+
logger.info('💡 Use --gpu-selection "all" to use all detected GPUs (default)')
|
209
|
+
|
210
|
+
|
211
|
+
def setup_application() -> tuple:
|
212
|
+
"""Set up logging, parse arguments, and handle special flags."""
|
213
|
+
# Set up logging with default level first
|
214
|
+
setup_logging()
|
215
|
+
|
216
|
+
# Parse command-line arguments
|
217
|
+
args = parse_arguments()
|
218
|
+
|
219
|
+
# Handle --list-gpus flag
|
220
|
+
if args.list_gpus:
|
221
|
+
list_gpus()
|
222
|
+
return None, None
|
223
|
+
|
224
|
+
logger.info('This project has been completely rewritten for better performance and reliability.')
|
225
|
+
logger.info('Please report any issues at https://github.com/stevezau/plex_generate_vid_previews/issues')
|
226
|
+
|
227
|
+
# Check for updates (non-blocking, fails gracefully)
|
228
|
+
check_for_updates(skip_check=args.skip_version_check)
|
229
|
+
|
230
|
+
# Set up signal handlers for graceful shutdown
|
231
|
+
signal.signal(signal.SIGINT, signal_handler)
|
232
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
233
|
+
|
234
|
+
# Load and validate configuration (CLI args take precedence over env vars)
|
235
|
+
# Note: Basic logging is already set up, so config validation errors will be logged properly
|
236
|
+
config = load_config(args)
|
237
|
+
|
238
|
+
# Exit if configuration validation failed
|
239
|
+
if config is None:
|
240
|
+
sys.exit(1)
|
241
|
+
|
242
|
+
# Store config in global application state for cleanup
|
243
|
+
app_state.set_config(config)
|
244
|
+
|
245
|
+
# Update logging level from config (in case it wasn't set in load_config)
|
246
|
+
setup_logging(config.log_level)
|
247
|
+
|
248
|
+
return args, config
|
249
|
+
|
250
|
+
|
251
|
+
def setup_working_directory(config) -> None:
|
252
|
+
"""Create and set up the working temporary directory."""
|
253
|
+
try:
|
254
|
+
config.working_tmp_folder = create_working_directory(config.tmp_folder)
|
255
|
+
logger.debug(f"Created working temp folder: {config.working_tmp_folder}")
|
256
|
+
except Exception as cleanup_error:
|
257
|
+
logger.error(f"Failed to create working temp folder: {cleanup_error}")
|
258
|
+
sys.exit(1)
|
259
|
+
|
260
|
+
|
261
|
+
def detect_and_select_gpus(config) -> list:
|
262
|
+
"""Detect available GPUs and select based on configuration."""
|
263
|
+
selected_gpus = []
|
264
|
+
if config.gpu_threads > 0:
|
265
|
+
# Detect all available GPUs
|
266
|
+
detected_gpus = detect_all_gpus()
|
267
|
+
|
268
|
+
if not detected_gpus:
|
269
|
+
logger.error('No GPUs detected.')
|
270
|
+
logger.error('Please set the GPU_THREADS environment variable to 0 to use CPU-only processing.')
|
271
|
+
logger.error('If you think this is an error please log an issue here https://github.com/stevezau/plex_generate_vid_previews/issues')
|
272
|
+
sys.exit(1)
|
273
|
+
|
274
|
+
# Display detected GPUs
|
275
|
+
logger.info(f'🔍 Detected {len(detected_gpus)} GPU(s):')
|
276
|
+
for i, (gpu_type, gpu_device, gpu_info) in enumerate(detected_gpus):
|
277
|
+
gpu_name = gpu_info.get('name', f'{gpu_type} GPU')
|
278
|
+
gpu_desc = format_gpu_info(gpu_type, gpu_device, gpu_name)
|
279
|
+
logger.info(f' [{i}] {gpu_desc}')
|
280
|
+
|
281
|
+
# Filter GPUs based on selection
|
282
|
+
if config.gpu_selection.lower() == 'all':
|
283
|
+
selected_gpus = detected_gpus
|
284
|
+
logger.info(f'✅ Using all {len(selected_gpus)} GPU(s)')
|
285
|
+
if len(detected_gpus) > 1:
|
286
|
+
logger.info(f'💡 To use specific GPUs only, use --gpu-selection "0" or --gpu-selection "0,1"')
|
287
|
+
else:
|
288
|
+
try:
|
289
|
+
# Parse GPU indices
|
290
|
+
gpu_indices = [int(x.strip()) for x in config.gpu_selection.split(',') if x.strip()]
|
291
|
+
selected_gpus = []
|
292
|
+
|
293
|
+
for idx in gpu_indices:
|
294
|
+
if 0 <= idx < len(detected_gpus):
|
295
|
+
selected_gpus.append(detected_gpus[idx])
|
296
|
+
else:
|
297
|
+
logger.error(f'❌ GPU {idx} not found. Available GPUs: 0-{len(detected_gpus)-1}')
|
298
|
+
logger.error('💡 Run with --list-gpus to see available GPUs')
|
299
|
+
sys.exit(1)
|
300
|
+
|
301
|
+
if not selected_gpus:
|
302
|
+
logger.error('❌ No valid GPUs selected')
|
303
|
+
sys.exit(1)
|
304
|
+
|
305
|
+
logger.info(f'✅ Using {len(selected_gpus)} selected GPU(s): {config.gpu_selection}')
|
306
|
+
|
307
|
+
except ValueError:
|
308
|
+
logger.error(f'❌ Invalid GPU selection format: {config.gpu_selection}')
|
309
|
+
logger.error('💡 Use "all" or comma-separated indices like "0,1,2"')
|
310
|
+
sys.exit(1)
|
311
|
+
else:
|
312
|
+
logger.debug("GPU threads set to 0 - using CPU-only processing")
|
313
|
+
|
314
|
+
return selected_gpus
|
315
|
+
|
316
|
+
|
317
|
+
def create_progress_displays():
|
318
|
+
"""Create progress display instances for different purposes."""
|
319
|
+
# Create separate Progress instances for different purposes
|
320
|
+
main_progress = Progress(
|
321
|
+
SpinnerColumn(),
|
322
|
+
TextColumn("[bold green]{task.description}"),
|
323
|
+
BarColumn(bar_width=None, style="green"),
|
324
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
325
|
+
MofNCompleteColumn(),
|
326
|
+
TimeElapsedColumn(),
|
327
|
+
console=console,
|
328
|
+
refresh_per_second=20
|
329
|
+
)
|
330
|
+
|
331
|
+
worker_progress = Progress(
|
332
|
+
TextColumn("[bold cyan]{task.description}"),
|
333
|
+
BarColumn(bar_width=None, style="cyan"),
|
334
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
335
|
+
FFmpegDataColumn(), # Show FFmpeg data instead of time
|
336
|
+
console=console,
|
337
|
+
refresh_per_second=20
|
338
|
+
)
|
339
|
+
|
340
|
+
# Special progress for querying library with animated bar
|
341
|
+
query_progress = Progress(
|
342
|
+
SpinnerColumn(),
|
343
|
+
TextColumn("[bold green]{task.description}"),
|
344
|
+
AnimatedBarColumn(bar_width=None, style="green", complete_style="red", finished_style="green"),
|
345
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
346
|
+
MofNCompleteColumn(),
|
347
|
+
TimeElapsedColumn(),
|
348
|
+
console=console,
|
349
|
+
refresh_per_second=20
|
350
|
+
)
|
351
|
+
|
352
|
+
return main_progress, worker_progress, query_progress
|
353
|
+
|
354
|
+
|
355
|
+
def run_processing(config, selected_gpus):
|
356
|
+
"""Run the main processing workflow."""
|
357
|
+
try:
|
358
|
+
# Get Plex server
|
359
|
+
plex = plex_server(config)
|
360
|
+
|
361
|
+
# Calculate title width for display formatting
|
362
|
+
title_max_width = calculate_title_width()
|
363
|
+
|
364
|
+
# Create worker pool
|
365
|
+
worker_pool = WorkerPool(
|
366
|
+
gpu_workers=config.gpu_threads,
|
367
|
+
cpu_workers=config.cpu_threads,
|
368
|
+
selected_gpus=selected_gpus
|
369
|
+
)
|
370
|
+
|
371
|
+
# Process all library sections
|
372
|
+
total_processed = 0
|
373
|
+
|
374
|
+
# Create progress displays
|
375
|
+
main_progress, worker_progress, query_progress = create_progress_displays()
|
376
|
+
|
377
|
+
# Create a dynamic group that can switch between query and processing displays
|
378
|
+
class DynamicGroup:
|
379
|
+
def __init__(self):
|
380
|
+
self.current_group = None
|
381
|
+
|
382
|
+
def set_query_mode(self):
|
383
|
+
self.current_group = Group(query_progress)
|
384
|
+
|
385
|
+
def set_processing_mode(self):
|
386
|
+
self.current_group = Group(main_progress, worker_progress)
|
387
|
+
|
388
|
+
def __rich_console__(self, console, options):
|
389
|
+
if self.current_group:
|
390
|
+
yield from self.current_group.__rich_console__(console, options)
|
391
|
+
|
392
|
+
dynamic_group = DynamicGroup()
|
393
|
+
|
394
|
+
with Live(dynamic_group, console=console, refresh_per_second=20):
|
395
|
+
# Start in query mode
|
396
|
+
dynamic_group.set_query_mode()
|
397
|
+
query_task = query_progress.add_task("Querying library...", total=1, completed=0)
|
398
|
+
|
399
|
+
# Get the generator for library sections
|
400
|
+
library_sections = get_library_sections(plex, config)
|
401
|
+
|
402
|
+
# Process all library sections
|
403
|
+
for section, media_items in library_sections:
|
404
|
+
if not media_items:
|
405
|
+
logger.info(f"No media items found in library '{section.title}', skipping")
|
406
|
+
continue
|
407
|
+
|
408
|
+
# Switch to processing mode
|
409
|
+
dynamic_group.set_processing_mode()
|
410
|
+
query_progress.remove_task(query_task)
|
411
|
+
|
412
|
+
main_task = main_progress.add_task(f"Processing {section.title}", total=len(media_items))
|
413
|
+
|
414
|
+
# Process items in this section with worker progress
|
415
|
+
worker_pool.process_items(media_items, config, plex, worker_progress, main_progress, main_task, title_max_width)
|
416
|
+
total_processed += len(media_items)
|
417
|
+
|
418
|
+
# Remove completed task
|
419
|
+
main_progress.remove_task(main_task)
|
420
|
+
|
421
|
+
# Switch back to query mode for next library
|
422
|
+
dynamic_group.set_query_mode()
|
423
|
+
query_task = query_progress.add_task("Querying library...", total=1, completed=0)
|
424
|
+
|
425
|
+
# Remove final query task
|
426
|
+
query_progress.remove_task(query_task)
|
427
|
+
|
428
|
+
logger.info(f'Successfully processed {total_processed} media items across all libraries')
|
429
|
+
|
430
|
+
except KeyboardInterrupt:
|
431
|
+
logger.info("Received interrupt signal, shutting down gracefully...")
|
432
|
+
except ConnectionError as e:
|
433
|
+
logger.error(f"Connection failed: {e}")
|
434
|
+
logger.error("Please fix the connection issue and try again.")
|
435
|
+
return 1
|
436
|
+
except Exception as e:
|
437
|
+
logger.error(f"Unexpected error in main execution: {e}")
|
438
|
+
raise
|
439
|
+
finally:
|
440
|
+
# Clean up worker pool
|
441
|
+
try:
|
442
|
+
if 'worker_pool' in locals():
|
443
|
+
worker_pool.shutdown()
|
444
|
+
except Exception as worker_error:
|
445
|
+
logger.warning(f"Failed to shutdown worker pool: {worker_error}")
|
446
|
+
|
447
|
+
# Clean up our working temp folder
|
448
|
+
try:
|
449
|
+
if os.path.isdir(config.working_tmp_folder):
|
450
|
+
shutil.rmtree(config.working_tmp_folder)
|
451
|
+
logger.debug(f"Cleaned up working temp folder: {config.working_tmp_folder}")
|
452
|
+
except Exception as cleanup_error:
|
453
|
+
logger.warning(f"Failed to clean up working temp folder {config.working_tmp_folder}: {cleanup_error}")
|
454
|
+
|
455
|
+
|
456
|
+
def main() -> None:
|
457
|
+
"""Main entry point for the application."""
|
458
|
+
# Set up application (logging, arguments, config)
|
459
|
+
args, config = setup_application()
|
460
|
+
if config is None: # Handled --list-gpus flag
|
461
|
+
return
|
462
|
+
|
463
|
+
# Set up working directory
|
464
|
+
setup_working_directory(config)
|
465
|
+
|
466
|
+
# Detect and select GPUs
|
467
|
+
selected_gpus = detect_and_select_gpus(config)
|
468
|
+
|
469
|
+
# Run the main processing workflow
|
470
|
+
run_processing(config, selected_gpus)
|
471
|
+
|
472
|
+
|
473
|
+
if __name__ == '__main__':
|
474
|
+
main()
|