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.
@@ -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
- self.console.show_cursor(True)
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 "0.0x"
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
- output_file = f'/tmp/ffmpeg_output_{os.getpid()}_{int(time.time())}.log'
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.05) # Reduced from 0.1s
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 new lines
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) # Store for debugging
229
-
230
- # Parse FFmpeg output line
231
- total_duration = parse_ffmpeg_progress_line(line, total_duration, progress_callback)
232
- line_count = len(lines)
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}x')
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:
@@ -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.01) # 10ms sleep for better stability with multiple workers
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plex-generate-previews
3
- Version: 2.0.0
3
+ Version: 2.1.1
4
4
  Summary: Generate video preview thumbnails for Plex Media Server
5
5
  Author-email: stevezau <stevezau@example.com>
6
6
  License-Expression: MIT
@@ -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=NMEp-2JzeZ1K7_IFxfFuEZPwJ34GNezFqO8v0LO2bNI,19251
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=nsr-h0RLwunm0_vzWscEHFIs4cM8SF-U913q_zJjjVA,18718
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=W50TbcuoIWQIeOu7B2yE6QPwtJFx6lSSvv2DBTvHVk8,19740
11
- plex_generate_previews-2.0.0.dist-info/METADATA,sha256=88Ke9patJ9r9PoB0KpwajycD3o7_M7cclOCWNHhQ4GM,23843
12
- plex_generate_previews-2.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- plex_generate_previews-2.0.0.dist-info/entry_points.txt,sha256=AvOKrBTsHLpjsCJWZgz-tNfA-0KInF35YKMr4QzHoA8,75
14
- plex_generate_previews-2.0.0.dist-info/top_level.txt,sha256=d0aQi-UccrXBheWaw8GfpInS6qdbfI8D9OO5fr0ZT3g,23
15
- plex_generate_previews-2.0.0.dist-info/RECORD,,
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,,