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,479 @@
|
|
1
|
+
"""
|
2
|
+
Configuration management for Plex Video Preview Generator.
|
3
|
+
|
4
|
+
Handles environment variable loading, validation, and provides a centralized
|
5
|
+
configuration object for the entire application.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
import sys
|
10
|
+
import shutil
|
11
|
+
import subprocess
|
12
|
+
from dataclasses import dataclass
|
13
|
+
from typing import List, Optional
|
14
|
+
from dotenv import load_dotenv
|
15
|
+
from loguru import logger
|
16
|
+
|
17
|
+
from .utils import is_docker_environment
|
18
|
+
|
19
|
+
# Load environment variables from .env file
|
20
|
+
load_dotenv()
|
21
|
+
|
22
|
+
# Set default ROCM_PATH if not already set to prevent KeyError in AMD SMI
|
23
|
+
if 'ROCM_PATH' not in os.environ:
|
24
|
+
os.environ['ROCM_PATH'] = '/opt/rocm'
|
25
|
+
|
26
|
+
|
27
|
+
def get_config_value(cli_args, field_name: str, env_key: str, default, value_type: type = str):
|
28
|
+
"""
|
29
|
+
Get configuration value with proper precedence: CLI args > env vars > defaults.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
cli_args: CLI arguments object or None
|
33
|
+
field_name: Name of the CLI argument field
|
34
|
+
env_key: Environment variable key
|
35
|
+
default: Default value if neither CLI nor env var is set
|
36
|
+
value_type: Type to convert the value to (str, int, bool)
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
The configuration value converted to the specified type
|
40
|
+
"""
|
41
|
+
cli_value = getattr(cli_args, field_name, None) if cli_args else None
|
42
|
+
if cli_value is not None:
|
43
|
+
return cli_value
|
44
|
+
|
45
|
+
env_value = os.environ.get(env_key, '')
|
46
|
+
|
47
|
+
# Handle boolean conversion specially
|
48
|
+
if value_type == bool:
|
49
|
+
if env_value.strip().lower() in ('true', '1', 'yes'):
|
50
|
+
return True
|
51
|
+
elif env_value.strip().lower() in ('false', '0', 'no'):
|
52
|
+
return False
|
53
|
+
return default
|
54
|
+
|
55
|
+
# Handle other types
|
56
|
+
if not env_value:
|
57
|
+
return default
|
58
|
+
|
59
|
+
try:
|
60
|
+
return value_type(env_value)
|
61
|
+
except (ValueError, TypeError):
|
62
|
+
return default
|
63
|
+
|
64
|
+
|
65
|
+
def get_config_value_str(cli_args, field_name: str, env_key: str, default: str = '') -> str:
|
66
|
+
"""Get string configuration value."""
|
67
|
+
return get_config_value(cli_args, field_name, env_key, default, str)
|
68
|
+
|
69
|
+
|
70
|
+
def get_config_value_int(cli_args, field_name: str, env_key: str, default: int = 0) -> int:
|
71
|
+
"""Get integer configuration value."""
|
72
|
+
return get_config_value(cli_args, field_name, env_key, default, int)
|
73
|
+
|
74
|
+
|
75
|
+
def get_config_value_bool(cli_args, field_name: str, env_key: str, default: bool = False) -> bool:
|
76
|
+
"""Get boolean configuration value."""
|
77
|
+
return get_config_value(cli_args, field_name, env_key, default, bool)
|
78
|
+
|
79
|
+
|
80
|
+
@dataclass
|
81
|
+
class Config:
|
82
|
+
"""Configuration object containing all application settings."""
|
83
|
+
|
84
|
+
# Plex server configuration
|
85
|
+
plex_url: str
|
86
|
+
plex_token: str
|
87
|
+
plex_timeout: int
|
88
|
+
plex_libraries: List[str]
|
89
|
+
|
90
|
+
# Media paths
|
91
|
+
plex_config_folder: str
|
92
|
+
plex_local_videos_path_mapping: str
|
93
|
+
plex_videos_path_mapping: str
|
94
|
+
|
95
|
+
# Processing configuration
|
96
|
+
plex_bif_frame_interval: int
|
97
|
+
thumbnail_quality: int
|
98
|
+
regenerate_thumbnails: bool
|
99
|
+
|
100
|
+
# Threading configuration
|
101
|
+
gpu_threads: int
|
102
|
+
cpu_threads: int
|
103
|
+
gpu_selection: str
|
104
|
+
|
105
|
+
# System paths
|
106
|
+
tmp_folder: str
|
107
|
+
ffmpeg_path: str
|
108
|
+
|
109
|
+
# Logging
|
110
|
+
log_level: str
|
111
|
+
|
112
|
+
# Internal constants
|
113
|
+
worker_pool_timeout: int = 30
|
114
|
+
|
115
|
+
|
116
|
+
def show_docker_help():
|
117
|
+
"""Show Docker-optimized help message with environment variables prominently displayed."""
|
118
|
+
logger.info('🐳 Docker Environment Detected - Configuration via Environment Variables')
|
119
|
+
logger.info('=' * 80)
|
120
|
+
logger.info('')
|
121
|
+
logger.info('📋 Required Environment Variables:')
|
122
|
+
logger.info('')
|
123
|
+
logger.info(' PLEX_URL Plex server URL (e.g., http://localhost:32400)')
|
124
|
+
logger.info(' PLEX_TOKEN Plex authentication token')
|
125
|
+
logger.info(' PLEX_CONFIG_FOLDER Path to Plex Media Server configuration folder')
|
126
|
+
logger.info('')
|
127
|
+
logger.info('📋 Optional Environment Variables:')
|
128
|
+
logger.info('')
|
129
|
+
logger.info(' PLEX_TIMEOUT Plex API timeout in seconds (default: 60)')
|
130
|
+
logger.info(' PLEX_LIBRARIES Comma-separated library names (e.g., "Movies, TV Shows")')
|
131
|
+
logger.info(' PLEX_LOCAL_VIDEOS_PATH_MAPPING Local videos path mapping')
|
132
|
+
logger.info(' PLEX_VIDEOS_PATH_MAPPING Plex videos path mapping')
|
133
|
+
logger.info(' PLEX_BIF_FRAME_INTERVAL Interval between preview images in seconds (default: 5)')
|
134
|
+
logger.info(' THUMBNAIL_QUALITY Preview image quality 1-10 (default: 4)')
|
135
|
+
logger.info(' REGENERATE_THUMBNAILS Regenerate existing thumbnails (true/false, default: false)')
|
136
|
+
logger.info(' GPU_THREADS Number of GPU worker threads (default: 1)')
|
137
|
+
logger.info(' CPU_THREADS Number of CPU worker threads (default: 1)')
|
138
|
+
logger.info(' GPU_SELECTION GPU selection: "all" or comma-separated indices (default: all)')
|
139
|
+
logger.info(' TMP_FOLDER Temporary folder for processing (default: /tmp/plex_generate_previews)')
|
140
|
+
logger.info(' LOG_LEVEL Logging level: DEBUG, INFO, WARNING, ERROR (default: INFO)')
|
141
|
+
logger.info('')
|
142
|
+
logger.info('💡 Example Docker Run Command:')
|
143
|
+
logger.info('')
|
144
|
+
logger.info(' docker run -it --rm --runtime=nvidia \\')
|
145
|
+
logger.info(' -e PLEX_URL="http://localhost:32400" \\')
|
146
|
+
logger.info(' -e PLEX_TOKEN="your_token_here" \\')
|
147
|
+
logger.info(' -e PLEX_CONFIG_FOLDER="/config/plex/Library/Application Support/Plex Media Server" \\')
|
148
|
+
logger.info(' -e GPU_THREADS=1 \\')
|
149
|
+
logger.info(' -e CPU_THREADS=1 \\')
|
150
|
+
logger.info(' -v /path/to/plex/config:/config \\')
|
151
|
+
logger.info(' -v /path/to/videos:/data \\')
|
152
|
+
logger.info(' plex_generate_vid_previews:latest')
|
153
|
+
logger.info('')
|
154
|
+
logger.info('🔧 For CLI arguments (non-Docker), use: plex-generate-previews --help')
|
155
|
+
|
156
|
+
|
157
|
+
def load_config(cli_args=None) -> Config:
|
158
|
+
"""
|
159
|
+
Load and validate configuration from CLI arguments and environment variables.
|
160
|
+
CLI arguments take precedence over environment variables.
|
161
|
+
|
162
|
+
Args:
|
163
|
+
cli_args: Parsed CLI arguments or None
|
164
|
+
|
165
|
+
Returns:
|
166
|
+
Config: Validated configuration object
|
167
|
+
|
168
|
+
Raises:
|
169
|
+
SystemExit: If required configuration is missing or invalid
|
170
|
+
"""
|
171
|
+
# Extract CLI values (None if not provided)
|
172
|
+
if cli_args is None:
|
173
|
+
cli_args = None # Empty namespace
|
174
|
+
|
175
|
+
# Load configuration with precedence: CLI args > env vars > defaults
|
176
|
+
plex_url = get_config_value_str(cli_args, 'plex_url', 'PLEX_URL', '')
|
177
|
+
plex_token = get_config_value_str(cli_args, 'plex_token', 'PLEX_TOKEN', '')
|
178
|
+
plex_timeout = get_config_value_int(cli_args, 'plex_timeout', 'PLEX_TIMEOUT', 60)
|
179
|
+
|
180
|
+
# Handle plex_libraries (special case for comma-separated values)
|
181
|
+
plex_libraries = get_config_value_str(cli_args, 'plex_libraries', 'PLEX_LIBRARIES', '')
|
182
|
+
plex_libraries = [library.strip().lower() for library in plex_libraries.split(',') if library.strip()]
|
183
|
+
|
184
|
+
plex_config_folder = get_config_value_str(cli_args, 'plex_config_folder', 'PLEX_CONFIG_FOLDER', '/path_to/plex/Library/Application Support/Plex Media Server')
|
185
|
+
plex_local_videos_path_mapping = get_config_value_str(cli_args, 'plex_local_videos_path_mapping', 'PLEX_LOCAL_VIDEOS_PATH_MAPPING', '')
|
186
|
+
plex_videos_path_mapping = get_config_value_str(cli_args, 'plex_videos_path_mapping', 'PLEX_VIDEOS_PATH_MAPPING', '')
|
187
|
+
|
188
|
+
plex_bif_frame_interval = get_config_value_int(cli_args, 'plex_bif_frame_interval', 'PLEX_BIF_FRAME_INTERVAL', 5)
|
189
|
+
thumbnail_quality = get_config_value_int(cli_args, 'thumbnail_quality', 'THUMBNAIL_QUALITY', 4)
|
190
|
+
regenerate_thumbnails = get_config_value_bool(cli_args, 'regenerate_thumbnails', 'REGENERATE_THUMBNAILS', False)
|
191
|
+
|
192
|
+
gpu_threads = get_config_value_int(cli_args, 'gpu_threads', 'GPU_THREADS', 1)
|
193
|
+
cpu_threads = get_config_value_int(cli_args, 'cpu_threads', 'CPU_THREADS', 1)
|
194
|
+
gpu_selection = get_config_value_str(cli_args, 'gpu_selection', 'GPU_SELECTION', 'all')
|
195
|
+
|
196
|
+
tmp_folder = get_config_value_str(cli_args, 'tmp_folder', 'TMP_FOLDER', '/tmp/plex_generate_previews')
|
197
|
+
|
198
|
+
# Handle log_level (case insensitive)
|
199
|
+
log_level = get_config_value_str(cli_args, 'log_level', 'LOG_LEVEL', 'INFO').upper()
|
200
|
+
|
201
|
+
# Initialize validation lists
|
202
|
+
missing_params = []
|
203
|
+
validation_errors = []
|
204
|
+
|
205
|
+
# Validate log level
|
206
|
+
valid_log_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
207
|
+
if log_level not in valid_log_levels:
|
208
|
+
validation_errors.append(f'LOG_LEVEL must be one of {valid_log_levels} (got: {log_level})')
|
209
|
+
|
210
|
+
# Update logging level early so debug statements work
|
211
|
+
if log_level in valid_log_levels:
|
212
|
+
from .cli import setup_logging
|
213
|
+
setup_logging(log_level)
|
214
|
+
|
215
|
+
# Find FFmpeg path
|
216
|
+
ffmpeg_path = shutil.which("ffmpeg")
|
217
|
+
if not ffmpeg_path:
|
218
|
+
logger.error('FFmpeg not found. FFmpeg must be installed and available in PATH.')
|
219
|
+
sys.exit(1)
|
220
|
+
|
221
|
+
# Test FFmpeg actually works
|
222
|
+
try:
|
223
|
+
result = subprocess.run([ffmpeg_path, '-version'], capture_output=True, text=True, timeout=5)
|
224
|
+
if result.returncode != 0:
|
225
|
+
validation_errors.append('FFmpeg found but not working properly')
|
226
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
227
|
+
validation_errors.append('FFmpeg found but cannot execute properly')
|
228
|
+
|
229
|
+
# Check basic required parameters first
|
230
|
+
if not plex_url:
|
231
|
+
if is_docker_environment():
|
232
|
+
missing_params.append('PLEX_URL is required (set PLEX_URL environment variable)')
|
233
|
+
else:
|
234
|
+
missing_params.append('PLEX_URL is required (use --plex-url or set PLEX_URL environment variable)')
|
235
|
+
elif not plex_url.startswith(('http://', 'https://')):
|
236
|
+
validation_errors.append(f'PLEX_URL must start with http:// or https:// (got: {plex_url})')
|
237
|
+
|
238
|
+
if not plex_token:
|
239
|
+
if is_docker_environment():
|
240
|
+
missing_params.append('PLEX_TOKEN is required (set PLEX_TOKEN environment variable)')
|
241
|
+
else:
|
242
|
+
missing_params.append('PLEX_TOKEN is required (use --plex-token or set PLEX_TOKEN environment variable)')
|
243
|
+
|
244
|
+
# Check PLEX_CONFIG_FOLDER
|
245
|
+
if not plex_config_folder or plex_config_folder == '/path_to/plex/Library/Application Support/Plex Media Server':
|
246
|
+
if is_docker_environment():
|
247
|
+
missing_params.append('PLEX_CONFIG_FOLDER is required (set PLEX_CONFIG_FOLDER environment variable)')
|
248
|
+
else:
|
249
|
+
missing_params.append('PLEX_CONFIG_FOLDER is required (use --plex-config-folder or set PLEX_CONFIG_FOLDER environment variable)')
|
250
|
+
else:
|
251
|
+
# Path is provided, validate it
|
252
|
+
if not os.path.exists(plex_config_folder):
|
253
|
+
# Enhanced debugging for path issues
|
254
|
+
debug_info = []
|
255
|
+
debug_info.append(f'PLEX_CONFIG_FOLDER ({plex_config_folder}) does not exist')
|
256
|
+
|
257
|
+
# Walk back to find existing parent directories
|
258
|
+
current_path = plex_config_folder
|
259
|
+
found_existing = False
|
260
|
+
while current_path and current_path != '/' and current_path != os.path.dirname(current_path):
|
261
|
+
parent_path = os.path.dirname(current_path)
|
262
|
+
if os.path.exists(parent_path):
|
263
|
+
if not found_existing:
|
264
|
+
debug_info.append(f'Checked folder path and found that up to this directory exists: {parent_path}')
|
265
|
+
found_existing = True
|
266
|
+
try:
|
267
|
+
contents = os.listdir(parent_path)
|
268
|
+
debug_info.append(f'Contents of {parent_path}:')
|
269
|
+
if contents:
|
270
|
+
for item in sorted(contents)[:10]: # Show first 10 items
|
271
|
+
item_path = os.path.join(parent_path, item)
|
272
|
+
item_type = "DIR" if os.path.isdir(item_path) else "FILE"
|
273
|
+
debug_info.append(f' {item_type}: {item}')
|
274
|
+
if len(contents) > 10:
|
275
|
+
debug_info.append(f' ... and {len(contents) - 10} more items')
|
276
|
+
else:
|
277
|
+
debug_info.append(' (empty directory)')
|
278
|
+
except PermissionError:
|
279
|
+
debug_info.append(f' Permission denied reading {parent_path}')
|
280
|
+
break
|
281
|
+
current_path = parent_path
|
282
|
+
|
283
|
+
if not found_existing:
|
284
|
+
debug_info.append('Checked folder path but no parent directories exist')
|
285
|
+
|
286
|
+
# Show current working directory and environment
|
287
|
+
debug_info.append(f'Current working directory: {os.getcwd()}')
|
288
|
+
debug_info.append(f'User: {os.getenv("USER", "unknown")}')
|
289
|
+
|
290
|
+
validation_errors.append('\n'.join(debug_info))
|
291
|
+
else:
|
292
|
+
# Config folder exists, validate it contains Plex server structure
|
293
|
+
try:
|
294
|
+
config_contents = os.listdir(plex_config_folder)
|
295
|
+
found_folders = [item for item in config_contents if os.path.isdir(os.path.join(plex_config_folder, item))]
|
296
|
+
|
297
|
+
# Check for essential Plex server folders (only require Cache and Media)
|
298
|
+
essential_folders = ['Cache', 'Media']
|
299
|
+
found_essential = [folder for folder in essential_folders if folder in found_folders]
|
300
|
+
|
301
|
+
if len(found_essential) < 2: # Need both Cache and Media
|
302
|
+
debug_info = []
|
303
|
+
debug_info.append(f'PLEX_CONFIG_FOLDER exists but does not appear to be a valid Plex Media Server directory')
|
304
|
+
debug_info.append(f'Are you sure you mapped the right Plex folder?')
|
305
|
+
debug_info.append(f'Expected: Essential Plex folders (Cache, Media)')
|
306
|
+
debug_info.append(f'Found: {sorted(found_folders)}')
|
307
|
+
debug_info.append(f'Missing: {sorted([f for f in essential_folders if f not in found_folders])}')
|
308
|
+
debug_info.append(f'💡 Tip: Point to the main Plex directory:')
|
309
|
+
debug_info.append(f' Linux: /var/lib/plexmediaserver/Library/Application Support/Plex Media Server')
|
310
|
+
debug_info.append(f' Docker: /config/plex/Library/Application Support/Plex Media Server')
|
311
|
+
debug_info.append(f' Windows: C:\\Users\\[Username]\\AppData\\Local\\Plex Media Server')
|
312
|
+
debug_info.append(f' macOS: ~/Library/Application Support/Plex Media Server')
|
313
|
+
validation_errors.append('\n'.join(debug_info))
|
314
|
+
else:
|
315
|
+
# Config folder looks good, now check Media/localhost
|
316
|
+
media_path = os.path.join(plex_config_folder, 'Media')
|
317
|
+
localhost_path = os.path.join(media_path, 'localhost')
|
318
|
+
|
319
|
+
if not os.path.exists(media_path):
|
320
|
+
validation_errors.append(f'PLEX_CONFIG_FOLDER/Media directory does not exist: {media_path}')
|
321
|
+
elif not os.path.exists(localhost_path):
|
322
|
+
validation_errors.append(f'PLEX_CONFIG_FOLDER/Media/localhost directory does not exist: {localhost_path}')
|
323
|
+
else:
|
324
|
+
# localhost folder exists, validate it contains Plex database structure
|
325
|
+
try:
|
326
|
+
localhost_contents = os.listdir(localhost_path)
|
327
|
+
found_localhost_folders = [item for item in localhost_contents if os.path.isdir(os.path.join(localhost_path, item))]
|
328
|
+
|
329
|
+
# Check for either hex directories (0-f) or standard Plex folders
|
330
|
+
hex_folders = [item for item in localhost_contents if len(item) == 1 and item in '0123456789abcdef']
|
331
|
+
standard_folders = ['Metadata', 'Cache', 'Plug-ins', 'Logs', 'Plug-in Support']
|
332
|
+
found_standard = [folder for folder in standard_folders if folder in found_localhost_folders]
|
333
|
+
|
334
|
+
# Accept if we have either hex directories OR standard folders
|
335
|
+
has_hex_structure = len(hex_folders) >= 10 # Most of 0-f
|
336
|
+
has_standard_structure = len(found_standard) >= 3 # At least 3 standard folders
|
337
|
+
|
338
|
+
if not has_hex_structure and not has_standard_structure:
|
339
|
+
debug_info = []
|
340
|
+
debug_info.append(f'PLEX_CONFIG_FOLDER/Media/localhost exists but does not appear to be a valid Plex database')
|
341
|
+
debug_info.append(f'Expected: Either hex directories (0-f) OR standard Plex folders (Metadata, Cache, etc.)')
|
342
|
+
debug_info.append(f'Found: {sorted(found_localhost_folders)}')
|
343
|
+
if hex_folders:
|
344
|
+
debug_info.append(f'Hex directories found: {len(hex_folders)}/16 (need 10+)')
|
345
|
+
if found_standard:
|
346
|
+
debug_info.append(f'Standard folders found: {len(found_standard)}/5 (need 3+)')
|
347
|
+
debug_info.append(f'This suggests the path may not point to the correct Plex Media Server database location')
|
348
|
+
validation_errors.append('\n'.join(debug_info))
|
349
|
+
except PermissionError:
|
350
|
+
validation_errors.append(f'Permission denied reading localhost folder: {localhost_path}')
|
351
|
+
except PermissionError:
|
352
|
+
validation_errors.append(f'Permission denied reading PLEX_CONFIG_FOLDER: {plex_config_folder}')
|
353
|
+
|
354
|
+
# Validate numeric ranges
|
355
|
+
if plex_bif_frame_interval < 1 or plex_bif_frame_interval > 60:
|
356
|
+
validation_errors.append(f'PLEX_BIF_FRAME_INTERVAL must be between 1-60 seconds (got: {plex_bif_frame_interval})')
|
357
|
+
|
358
|
+
if thumbnail_quality < 1 or thumbnail_quality > 10:
|
359
|
+
validation_errors.append(f'THUMBNAIL_QUALITY must be between 1-10 (got: {thumbnail_quality})')
|
360
|
+
|
361
|
+
if plex_timeout < 10 or plex_timeout > 3600:
|
362
|
+
validation_errors.append(f'PLEX_TIMEOUT must be between 10-3600 seconds (got: {plex_timeout})')
|
363
|
+
|
364
|
+
# Validate thread counts
|
365
|
+
if gpu_threads < 0 or gpu_threads > 32:
|
366
|
+
validation_errors.append(f'GPU_THREADS must be between 0-32 (got: {gpu_threads})')
|
367
|
+
|
368
|
+
if cpu_threads < 0 or cpu_threads > 32:
|
369
|
+
validation_errors.append(f'CPU_THREADS must be between 0-32 (got: {cpu_threads})')
|
370
|
+
|
371
|
+
# Validate gpu_selection format
|
372
|
+
if gpu_selection.lower() != 'all':
|
373
|
+
try:
|
374
|
+
# Parse comma-separated GPU indices
|
375
|
+
gpu_indices = [int(x.strip()) for x in gpu_selection.split(',') if x.strip()]
|
376
|
+
if not gpu_indices:
|
377
|
+
validation_errors.append(f'GPU_SELECTION must be "all" or comma-separated GPU indices (got: {gpu_selection})')
|
378
|
+
elif any(idx < 0 for idx in gpu_indices):
|
379
|
+
validation_errors.append(f'GPU_SELECTION indices must be non-negative (got: {gpu_selection})')
|
380
|
+
except ValueError:
|
381
|
+
validation_errors.append(f'GPU_SELECTION must be "all" or comma-separated integers (got: {gpu_selection})')
|
382
|
+
|
383
|
+
# Validate tmp_folder exists and is writable (user must create it)
|
384
|
+
if not os.path.exists(tmp_folder):
|
385
|
+
validation_errors.append(f'TMP_FOLDER does not exist: {tmp_folder}')
|
386
|
+
validation_errors.append(f'Please create the directory first: mkdir -p {tmp_folder}')
|
387
|
+
elif not os.access(tmp_folder, os.W_OK):
|
388
|
+
validation_errors.append(f'TMP_FOLDER ({tmp_folder}) is not writable')
|
389
|
+
validation_errors.append(f'Please fix permissions: chmod 755 {tmp_folder}')
|
390
|
+
|
391
|
+
# Additional safety check: warn if tmp_folder is a system directory
|
392
|
+
if tmp_folder in ['/tmp', '/var/tmp', '/']:
|
393
|
+
validation_errors.append(f'TMP_FOLDER should not be a system directory like {tmp_folder}. Use a subdirectory instead (e.g., {tmp_folder}/plex_previews)')
|
394
|
+
|
395
|
+
# Check available disk space in tmp_folder
|
396
|
+
if os.path.exists(tmp_folder):
|
397
|
+
try:
|
398
|
+
statvfs = os.statvfs(tmp_folder)
|
399
|
+
free_space_gb = (statvfs.f_frsize * statvfs.f_bavail) / (1024**3)
|
400
|
+
if free_space_gb < 1: # Less than 1GB
|
401
|
+
validation_errors.append(f'TMP_FOLDER has less than 1GB free space ({free_space_gb:.1f}GB available)')
|
402
|
+
except OSError:
|
403
|
+
validation_errors.append(f'Cannot check disk space for TMP_FOLDER ({tmp_folder})')
|
404
|
+
|
405
|
+
# Handle missing parameters (show help)
|
406
|
+
if missing_params:
|
407
|
+
logger.error('❌ Configuration Error: Missing required parameters:')
|
408
|
+
for i, error_msg in enumerate(missing_params, 1):
|
409
|
+
logger.error(f' {i}. {error_msg}')
|
410
|
+
logger.info('')
|
411
|
+
|
412
|
+
# Show Docker-optimized help if running in Docker, otherwise show CLI help
|
413
|
+
if is_docker_environment():
|
414
|
+
show_docker_help()
|
415
|
+
else:
|
416
|
+
logger.info('📋 Showing help for all available options:')
|
417
|
+
logger.info('=' * 60)
|
418
|
+
# Show help automatically
|
419
|
+
sys.argv = [sys.argv[0], '--help']
|
420
|
+
try:
|
421
|
+
from .cli import parse_arguments
|
422
|
+
parse_arguments()
|
423
|
+
except SystemExit:
|
424
|
+
pass
|
425
|
+
|
426
|
+
return None # Return None to indicate validation failure
|
427
|
+
|
428
|
+
# Handle validation errors (standard error messages)
|
429
|
+
if validation_errors:
|
430
|
+
logger.error('❌ Configuration Error:')
|
431
|
+
for i, error_msg in enumerate(validation_errors, 1):
|
432
|
+
logger.error(f' {i}. {error_msg}')
|
433
|
+
return None # Return None to indicate validation failure
|
434
|
+
|
435
|
+
# Validate thread configuration
|
436
|
+
if cpu_threads == 0 and gpu_threads == 0:
|
437
|
+
logger.error('❌ Configuration Error: Both CPU_THREADS and GPU_THREADS are set to 0.')
|
438
|
+
logger.error('📋 At least one processing method must be enabled.')
|
439
|
+
logger.info('💡 Use --help to see all available options.')
|
440
|
+
logger.info('💡 Example: plex-generate-previews --cpu-threads 4 --gpu-threads 2')
|
441
|
+
sys.exit(1)
|
442
|
+
|
443
|
+
config = Config(
|
444
|
+
plex_url=plex_url,
|
445
|
+
plex_token=plex_token,
|
446
|
+
plex_timeout=plex_timeout,
|
447
|
+
plex_libraries=plex_libraries,
|
448
|
+
plex_config_folder=plex_config_folder,
|
449
|
+
plex_local_videos_path_mapping=plex_local_videos_path_mapping,
|
450
|
+
plex_videos_path_mapping=plex_videos_path_mapping,
|
451
|
+
plex_bif_frame_interval=plex_bif_frame_interval,
|
452
|
+
thumbnail_quality=thumbnail_quality,
|
453
|
+
regenerate_thumbnails=regenerate_thumbnails,
|
454
|
+
gpu_threads=gpu_threads,
|
455
|
+
cpu_threads=cpu_threads,
|
456
|
+
gpu_selection=gpu_selection,
|
457
|
+
tmp_folder=tmp_folder,
|
458
|
+
ffmpeg_path=ffmpeg_path,
|
459
|
+
log_level=log_level
|
460
|
+
)
|
461
|
+
|
462
|
+
# Set the timeout envvar for https://github.com/pkkid/python-plexapi
|
463
|
+
os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(config.plex_timeout)
|
464
|
+
|
465
|
+
# Output debug information
|
466
|
+
logger.debug(f'PLEX_URL = {config.plex_url}')
|
467
|
+
logger.debug(f'PLEX_BIF_FRAME_INTERVAL = {config.plex_bif_frame_interval}')
|
468
|
+
logger.debug(f'THUMBNAIL_QUALITY = {config.thumbnail_quality}')
|
469
|
+
logger.debug(f'PLEX_CONFIG_FOLDER = {config.plex_config_folder}')
|
470
|
+
logger.debug(f'TMP_FOLDER = {config.tmp_folder}')
|
471
|
+
logger.debug(f'PLEX_TIMEOUT = {config.plex_timeout}')
|
472
|
+
logger.debug(f'PLEX_LOCAL_VIDEOS_PATH_MAPPING = {config.plex_local_videos_path_mapping}')
|
473
|
+
logger.debug(f'PLEX_VIDEOS_PATH_MAPPING = {config.plex_videos_path_mapping}')
|
474
|
+
logger.debug(f'GPU_THREADS = {config.gpu_threads}')
|
475
|
+
logger.debug(f'CPU_THREADS = {config.cpu_threads}')
|
476
|
+
logger.debug(f'GPU_SELECTION = {config.gpu_selection}')
|
477
|
+
logger.debug(f'REGENERATE_THUMBNAILS = {config.regenerate_thumbnails}')
|
478
|
+
|
479
|
+
return config
|