plex-generate-previews 2.0.0__py3-none-any.whl → 2.1.1__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/cli.py +30 -3
- plex_generate_previews/media_processing.py +52 -26
- plex_generate_previews/worker.py +1 -1
- {plex_generate_previews-2.0.0.dist-info → plex_generate_previews-2.1.1.dist-info}/METADATA +1 -1
- {plex_generate_previews-2.0.0.dist-info → plex_generate_previews-2.1.1.dist-info}/RECORD +8 -8
- {plex_generate_previews-2.0.0.dist-info → plex_generate_previews-2.1.1.dist-info}/WHEEL +0 -0
- {plex_generate_previews-2.0.0.dist-info → plex_generate_previews-2.1.1.dist-info}/entry_points.txt +0 -0
- {plex_generate_previews-2.0.0.dist-info → plex_generate_previews-2.1.1.dist-info}/top_level.txt +0 -0
plex_generate_previews/cli.py
CHANGED
@@ -40,9 +40,25 @@ class ApplicationState:
|
|
40
40
|
|
41
41
|
def cleanup(self):
|
42
42
|
"""Perform cleanup operations."""
|
43
|
-
# Restore terminal cursor visibility
|
43
|
+
# Restore terminal cursor visibility using Rich's proper methods
|
44
44
|
if self.console:
|
45
|
-
|
45
|
+
try:
|
46
|
+
# Rich's proper way to restore terminal state
|
47
|
+
self.console.show_cursor(True)
|
48
|
+
# Force Rich to restore the terminal to its original state
|
49
|
+
if hasattr(self.console, '_live'):
|
50
|
+
self.console._live = None
|
51
|
+
# Clear any pending output and ensure proper terminal state
|
52
|
+
self.console.print("", end="")
|
53
|
+
# Force a newline to ensure we're on a fresh line
|
54
|
+
self.console.print()
|
55
|
+
except Exception as e:
|
56
|
+
# Fallback: direct terminal escape sequence
|
57
|
+
try:
|
58
|
+
print('\033[?25h', end='', flush=True)
|
59
|
+
print() # Ensure we're on a new line
|
60
|
+
except:
|
61
|
+
pass
|
46
62
|
|
47
63
|
# Clean up working tmp folder if it exists
|
48
64
|
try:
|
@@ -320,7 +336,7 @@ def create_progress_displays():
|
|
320
336
|
main_progress = Progress(
|
321
337
|
SpinnerColumn(),
|
322
338
|
TextColumn("[bold green]{task.description}"),
|
323
|
-
BarColumn(bar_width=None, style="green"),
|
339
|
+
BarColumn(bar_width=None, style="red", complete_style="green", finished_style="green"),
|
324
340
|
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
325
341
|
MofNCompleteColumn(),
|
326
342
|
TimeElapsedColumn(),
|
@@ -451,6 +467,17 @@ def run_processing(config, selected_gpus):
|
|
451
467
|
logger.debug(f"Cleaned up working temp folder: {config.working_tmp_folder}")
|
452
468
|
except Exception as cleanup_error:
|
453
469
|
logger.warning(f"Failed to clean up working temp folder {config.working_tmp_folder}: {cleanup_error}")
|
470
|
+
|
471
|
+
# Final terminal cleanup to ensure cursor is visible
|
472
|
+
try:
|
473
|
+
console.show_cursor(True)
|
474
|
+
# Force Rich to restore the terminal to its original state
|
475
|
+
if hasattr(console, '_live'):
|
476
|
+
console._live = None
|
477
|
+
# Ensure we're on a fresh line
|
478
|
+
console.print()
|
479
|
+
except:
|
480
|
+
pass
|
454
481
|
|
455
482
|
|
456
483
|
def main() -> None:
|
@@ -65,7 +65,7 @@ def parse_ffmpeg_progress_line(line: str, total_duration: float, progress_callba
|
|
65
65
|
q = float(q_match.group(1)) if q_match else 0
|
66
66
|
size = int(size_match.group(1)) if size_match else 0
|
67
67
|
bitrate = float(bitrate_match.group(1)) if bitrate_match else 0
|
68
|
-
speed = speed_match.group(1) + "x" if speed_match else
|
68
|
+
speed = speed_match.group(1) + "x" if speed_match else None
|
69
69
|
|
70
70
|
if time_match:
|
71
71
|
hours, minutes, seconds = time_match.groups()
|
@@ -84,7 +84,7 @@ def parse_ffmpeg_progress_line(line: str, total_duration: float, progress_callba
|
|
84
84
|
|
85
85
|
# Call progress callback with all FFmpeg data
|
86
86
|
if progress_callback:
|
87
|
-
progress_callback(progress_percent, current_time, total_duration, speed,
|
87
|
+
progress_callback(progress_percent, current_time, total_duration, speed or "0.0x",
|
88
88
|
remaining_time, frame, fps, q, size, time_str, bitrate)
|
89
89
|
|
90
90
|
return total_duration
|
@@ -197,7 +197,10 @@ def generate_images(video_file: str, output_folder: str, gpu: Optional[str],
|
|
197
197
|
|
198
198
|
# Use file polling approach for non-blocking, high-frequency progress monitoring
|
199
199
|
# This is faster than subprocess.PIPE which would block on readline() calls
|
200
|
-
|
200
|
+
# Use high-resolution timestamp and thread ID to ensure unique file per worker
|
201
|
+
import threading
|
202
|
+
thread_id = threading.get_ident()
|
203
|
+
output_file = f'/tmp/ffmpeg_output_{os.getpid()}_{thread_id}_{time.time_ns()}.log'
|
201
204
|
proc = subprocess.Popen(args, stderr=open(output_file, 'w'), stdout=subprocess.DEVNULL)
|
202
205
|
|
203
206
|
# Signal that FFmpeg process has started
|
@@ -212,39 +215,57 @@ def generate_images(video_file: str, output_folder: str, gpu: Optional[str],
|
|
212
215
|
ffmpeg_output_lines = [] # Store all FFmpeg output for debugging
|
213
216
|
line_count = 0
|
214
217
|
|
218
|
+
# Create a wrapper callback to capture speed updates
|
219
|
+
def speed_capture_callback(progress_percent, current_duration, total_duration, speed_value,
|
220
|
+
remaining_time=None, frame=0, fps=0, q=0, size=0, time_str="00:00:00.00", bitrate=0):
|
221
|
+
nonlocal speed
|
222
|
+
if speed_value and speed_value != "0.0x":
|
223
|
+
speed = speed_value
|
224
|
+
if progress_callback:
|
225
|
+
progress_callback(progress_percent, current_duration, total_duration, speed_value,
|
226
|
+
remaining_time, frame, fps, q, size, time_str, bitrate)
|
227
|
+
|
215
228
|
# Allow time for it to start
|
216
|
-
time.sleep(0.
|
229
|
+
time.sleep(0.02)
|
217
230
|
|
218
231
|
# Parse FFmpeg output using file polling (much faster)
|
232
|
+
poll_count = 0
|
219
233
|
while proc.poll() is None:
|
234
|
+
poll_count += 1
|
220
235
|
if os.path.exists(output_file):
|
236
|
+
try:
|
237
|
+
with open(output_file, 'r') as f:
|
238
|
+
lines = f.readlines()
|
239
|
+
if len(lines) > line_count:
|
240
|
+
# Process new lines
|
241
|
+
for i in range(line_count, len(lines)):
|
242
|
+
line = lines[i].strip()
|
243
|
+
if line:
|
244
|
+
ffmpeg_output_lines.append(line)
|
245
|
+
# Parse FFmpeg output line
|
246
|
+
total_duration = parse_ffmpeg_progress_line(line, total_duration, speed_capture_callback)
|
247
|
+
line_count = len(lines)
|
248
|
+
except (OSError, IOError):
|
249
|
+
# Handle file access issues gracefully
|
250
|
+
pass
|
251
|
+
|
252
|
+
time.sleep(0.005) # Poll every 5ms for very responsive updates
|
253
|
+
|
254
|
+
# Process any remaining data in the output file
|
255
|
+
if os.path.exists(output_file):
|
256
|
+
try:
|
221
257
|
with open(output_file, 'r') as f:
|
222
258
|
lines = f.readlines()
|
223
259
|
if len(lines) > line_count:
|
224
|
-
# Process
|
260
|
+
# Process any remaining lines
|
225
261
|
for i in range(line_count, len(lines)):
|
226
262
|
line = lines[i].strip()
|
227
263
|
if line:
|
228
|
-
ffmpeg_output_lines.append(line)
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
time.sleep(0.005) # Poll every 5ms for very responsive updates
|
235
|
-
|
236
|
-
# Process any remaining data in the output file
|
237
|
-
if os.path.exists(output_file):
|
238
|
-
with open(output_file, 'r') as f:
|
239
|
-
lines = f.readlines()
|
240
|
-
if len(lines) > line_count:
|
241
|
-
# Process any remaining lines
|
242
|
-
for i in range(line_count, len(lines)):
|
243
|
-
line = lines[i].strip()
|
244
|
-
if line:
|
245
|
-
ffmpeg_output_lines.append(line)
|
246
|
-
# Parse any remaining progress lines
|
247
|
-
total_duration = parse_ffmpeg_progress_line(line, total_duration, progress_callback)
|
264
|
+
ffmpeg_output_lines.append(line)
|
265
|
+
# Parse any remaining progress lines
|
266
|
+
total_duration = parse_ffmpeg_progress_line(line, total_duration, speed_capture_callback)
|
267
|
+
except (OSError, IOError):
|
268
|
+
pass
|
248
269
|
|
249
270
|
# Clean up the output file
|
250
271
|
try:
|
@@ -265,13 +286,18 @@ def generate_images(video_file: str, output_folder: str, gpu: Optional[str],
|
|
265
286
|
end = time.time()
|
266
287
|
seconds = round(end - start, 1)
|
267
288
|
|
289
|
+
# Calculate fallback speed if no valid speed was captured
|
290
|
+
if speed == "0.0x" and total_duration and total_duration > 0 and seconds > 0:
|
291
|
+
calculated_speed = total_duration / seconds
|
292
|
+
speed = f"{calculated_speed:.0f}x"
|
293
|
+
|
268
294
|
# Optimize and Rename Images
|
269
295
|
for image in glob.glob(f'{output_folder}/img*.jpg'):
|
270
296
|
frame_no = int(os.path.basename(image).strip('-img').strip('.jpg')) - 1
|
271
297
|
frame_second = frame_no * config.plex_bif_frame_interval
|
272
298
|
os.rename(image, os.path.join(output_folder, f'{frame_second:010d}.jpg'))
|
273
299
|
|
274
|
-
logger.info(f'Generated Video Preview for {video_file} HW={hw} TIME={seconds}seconds SPEED={speed}
|
300
|
+
logger.info(f'Generated Video Preview for {video_file} HW={hw} TIME={seconds}seconds SPEED={speed}')
|
275
301
|
|
276
302
|
|
277
303
|
def generate_bif(bif_filename: str, images_path: str, config: Config) -> None:
|
plex_generate_previews/worker.py
CHANGED
@@ -432,7 +432,7 @@ class WorkerPool:
|
|
432
432
|
|
433
433
|
# Adaptive sleep to balance responsiveness and CPU usage
|
434
434
|
if self.has_busy_workers():
|
435
|
-
time.sleep(0.
|
435
|
+
time.sleep(0.005) # 5ms sleep for better responsiveness with multiple workers
|
436
436
|
|
437
437
|
# Final statistics
|
438
438
|
total_completed = sum(worker.completed for worker in self.workers)
|
@@ -1,15 +1,15 @@
|
|
1
1
|
plex_generate_previews/__init__.py,sha256=AsZalox9lP8FYHvi-aAUo-G5hy8ngaZq7fG-FJit7Qg,304
|
2
2
|
plex_generate_previews/__main__.py,sha256=7aEa3EqUk32lBE_V99Nks2t8SVaP31_9LnGJ-qX8FDg,197
|
3
|
-
plex_generate_previews/cli.py,sha256=
|
3
|
+
plex_generate_previews/cli.py,sha256=7WtebiJ8GBlHMTIgf9Iat5LsgtGBD3cag9agAZlb5lE,20463
|
4
4
|
plex_generate_previews/config.py,sha256=REtXu_7MvTotJYfP00Fu_HcsMfzucuyuLW2gxDzGWxg,24019
|
5
5
|
plex_generate_previews/gpu_detection.py,sha256=BXGBFFjWJhvZKjIyUkAlDhFKUsqHUp3ibNCE2ENziek,20684
|
6
|
-
plex_generate_previews/media_processing.py,sha256=
|
6
|
+
plex_generate_previews/media_processing.py,sha256=d_LwZaMqJ-wLWQLsPgT5hLggWzQSaDsUaIoYdNRO3HM,19968
|
7
7
|
plex_generate_previews/plex_client.py,sha256=Qcy5hkuwL1n87ZL7-wvB2j4fa3CWd0aQ3QApI9u0ru4,8537
|
8
8
|
plex_generate_previews/utils.py,sha256=lRFbFu6saBV9Xgokd1oMME9cWaq7vyVJpnLRodox4MY,4375
|
9
9
|
plex_generate_previews/version_check.py,sha256=tleI-pdX4qUAgv5u_2sn1JPIOFc5kOs0jH04sbz18hw,6316
|
10
|
-
plex_generate_previews/worker.py,sha256=
|
11
|
-
plex_generate_previews-2.
|
12
|
-
plex_generate_previews-2.
|
13
|
-
plex_generate_previews-2.
|
14
|
-
plex_generate_previews-2.
|
15
|
-
plex_generate_previews-2.
|
10
|
+
plex_generate_previews/worker.py,sha256=nXLOOpWTRJvvG7Na4F5_GZR493QU9lGNVPAwy_JUxIE,19745
|
11
|
+
plex_generate_previews-2.1.1.dist-info/METADATA,sha256=taNk1NUBA-yYzDYuMGnl2X7LRiiURrxCk-FfJZmlhWI,23843
|
12
|
+
plex_generate_previews-2.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
13
|
+
plex_generate_previews-2.1.1.dist-info/entry_points.txt,sha256=AvOKrBTsHLpjsCJWZgz-tNfA-0KInF35YKMr4QzHoA8,75
|
14
|
+
plex_generate_previews-2.1.1.dist-info/top_level.txt,sha256=d0aQi-UccrXBheWaw8GfpInS6qdbfI8D9OO5fr0ZT3g,23
|
15
|
+
plex_generate_previews-2.1.1.dist-info/RECORD,,
|
File without changes
|
{plex_generate_previews-2.0.0.dist-info → plex_generate_previews-2.1.1.dist-info}/entry_points.txt
RENAMED
File without changes
|
{plex_generate_previews-2.0.0.dist-info → plex_generate_previews-2.1.1.dist-info}/top_level.txt
RENAMED
File without changes
|