python-mpv-jsonipc 1.2.2__py3-none-any.whl → 1.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-mpv-jsonipc
3
- Version: 1.2.2
3
+ Version: 1.3.0
4
4
  Summary: Python API to MPV using JSON IPC
5
5
  Author-email: Izzie Walton <izzie@iwalton.com>
6
6
  License-Expression: Apache-2.0
@@ -0,0 +1,6 @@
1
+ python_mpv_jsonipc.py,sha256=vsGhKYoGeAl4uw652aZZsMF2weee_i8o7sJV9Ssp80g,31762
2
+ python_mpv_jsonipc-1.3.0.dist-info/licenses/LICENSE.md,sha256=7GQBUOA3dNG1Jk-yaPOCncNnRVURCRMev23QpZ1ew-s,10495
3
+ python_mpv_jsonipc-1.3.0.dist-info/METADATA,sha256=1_LpKl1fCquLmZBb0qYczUSbHynCP6FLGdt92Z5jV4U,2277
4
+ python_mpv_jsonipc-1.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
5
+ python_mpv_jsonipc-1.3.0.dist-info/top_level.txt,sha256=nGk4yjpVHKmIrL39XdGD2PhCeWF0oMjwey0nsNOd2TM,19
6
+ python_mpv_jsonipc-1.3.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
python_mpv_jsonipc.py CHANGED
@@ -2,6 +2,7 @@ import threading
2
2
  import socket
3
3
  import json
4
4
  import os
5
+ import re
5
6
  import time
6
7
  import subprocess
7
8
  import random
@@ -35,6 +36,79 @@ class MPVError(Exception):
35
36
  def __init__(self, *args, **kwargs):
36
37
  super(MPVError, self).__init__(*args, **kwargs)
37
38
 
39
+ class MPVProcessError(MPVError):
40
+ """MPV would not start, with whatever MPV said about why.
41
+
42
+ A subclass so that every existing ``except MPVError`` keeps working
43
+ unchanged -- this library is depended on by projects that will never be
44
+ updated, and several of them catch only that.
45
+
46
+ *returncode* is MPV's exit status, if it got far enough to have one.
47
+ *bad_option* is the option MPV refused, when that could be determined.
48
+ *log_output* is what MPV wrote about the failure.
49
+ *retryable* is False when starting again cannot possibly work.
50
+ *argv* is the command line that was tried.
51
+ """
52
+ def __init__(self, message, returncode=None, bad_option=None,
53
+ log_output=None, retryable=True, argv=None):
54
+ super(MPVProcessError, self).__init__(message)
55
+ self.returncode = returncode
56
+ self.bad_option = bad_option
57
+ self.log_output = log_output
58
+ self.retryable = retryable
59
+ self.argv = argv
60
+
61
+ #: MPV's own wording when it is handed an option it does not have. Matching
62
+ #: it is what turns "MPV exited" into "MPV has no --input-gamepad".
63
+ _OPTION_ERROR = re.compile(r"Error parsing option ([^\s]+) \(option not found\)")
64
+
65
+ def _diagnose_start_failure(argv, timeout=15):
66
+ """Ask MPV why it refused *argv*. Returns ``(bad_option, output)``.
67
+
68
+ Three details make this reliable, and each was measured rather than
69
+ reasoned about:
70
+
71
+ * **The message comes back on stdout, not stderr.** MPV writes its
72
+ terminal output to stdout; stderr is empty. Capturing the wrong stream
73
+ gets a confident, permanent "no idea why".
74
+ * **``--terminal=yes`` has to come first.** Options are parsed in order
75
+ and this library sets ``--terminal=no`` by default, so with the flags
76
+ left as they are MPV has already silenced itself by the time it reaches
77
+ the option it objects to. The library's own terminal flags are dropped
78
+ rather than fought with.
79
+ * **``--version`` goes last**, so a configuration that turns out to be
80
+ fine parses everything and then exits (~15ms) instead of starting up
81
+ and having to be killed. It exits before binding the IPC socket, so
82
+ this cannot disturb the real MPV that is about to start.
83
+
84
+ An earlier version of this read ``--log-file`` instead, on the theory
85
+ that Windows has no stderr to capture. That log is written
86
+ **unreliably**: MPV does not flush it before exiting, so it arrives
87
+ truncated at a random point -- measured at 6/8 and 7/8 runs containing
88
+ the error, cut off anywhere from 102 bytes in. A pipe has no such
89
+ problem, and is byte-identical across runs.
90
+
91
+ Every failure here is answered with ``(None, ...)``, which puts the
92
+ caller back on the path it took before this existed. Being unable to
93
+ explain a failed start must never turn into a second failure.
94
+ """
95
+ terminal_flags = ("--terminal=", "--input-terminal=")
96
+ argv = list(argv)
97
+ probe = ([argv[0], "--terminal=yes"]
98
+ + [a for a in argv[1:] if not a.startswith(terminal_flags)]
99
+ + ["--version"])
100
+ try:
101
+ completed = subprocess.run(probe,
102
+ stdout=subprocess.PIPE,
103
+ stderr=subprocess.STDOUT,
104
+ timeout=timeout)
105
+ except Exception:
106
+ log.debug("Could not run MPV to diagnose the failure.", exc_info=1)
107
+ return None, None
108
+ output = completed.stdout.decode("utf-8", "replace")
109
+ match = _OPTION_ERROR.search(output)
110
+ return (match.group(1) if match else None), output
111
+
38
112
  class WindowsSocket(threading.Thread):
39
113
  """
40
114
  Wraps a Windows named pipe in a high-level interface. (Internal)
@@ -198,6 +272,26 @@ class UnixSocket(threading.Thread):
198
272
  if self.quit_callback:
199
273
  self.quit_callback()
200
274
 
275
+ def _ipc_endpoint_ready(ipc_socket):
276
+ """Return True once MPV's IPC endpoint is available.
277
+
278
+ On POSIX the endpoint is a filesystem socket, so ``os.path.exists`` works. On
279
+ Windows it is a *named pipe*, for which ``os.path.exists`` is always False — so we
280
+ probe it with ``WaitNamedPipe`` instead. Using ``os.path.exists`` on Windows made
281
+ the startup poll never detect the pipe, raising a spurious "MPV start timed out".
282
+ """
283
+ if os.name != 'nt':
284
+ return os.path.exists(ipc_socket)
285
+ try:
286
+ _winapi.WaitNamedPipe(ipc_socket, 0)
287
+ return True
288
+ except FileNotFoundError:
289
+ return False
290
+ except OSError:
291
+ # Pipe exists but all instances are momentarily busy — it is present.
292
+ return True
293
+
294
+
201
295
  class MPVProcess:
202
296
  """
203
297
  Manages an MPV process, ensuring the socket or pipe is available. (Internal)
@@ -243,12 +337,13 @@ class MPVProcess:
243
337
 
244
338
  args.extend("--{0}={1}".format(v[0].replace("_", "-"), self._mpv_fmt(v[1]))
245
339
  for v in arg_pairs)
340
+ self.argv = args
246
341
  self.process = subprocess.Popen(args)
247
342
  ipc_exists = False
248
343
  for _ in range(100): # Give MPV 10 seconds to start.
249
344
  time.sleep(0.1)
250
345
  self.process.poll()
251
- if os.path.exists(ipc_socket):
346
+ if _ipc_endpoint_ready(ipc_socket):
252
347
  ipc_exists = True
253
348
  log.debug("Found MPV socket.")
254
349
  break
@@ -257,20 +352,29 @@ class MPVProcess:
257
352
  break
258
353
  else:
259
354
  self.process.terminate()
260
- raise MPVError("MPV start timed out.")
355
+ raise MPVProcessError("MPV start timed out.", argv=args)
261
356
 
262
357
  if not ipc_exists or self.process.returncode is not None:
263
358
  self.process.terminate()
264
- raise MPVError("MPV not started.")
359
+ # Message unchanged: it has been this string for years and is not
360
+ # ours to break. What is new is the argv riding along, which is
361
+ # what lets the caller ask MPV why.
362
+ raise MPVProcessError("MPV not started.",
363
+ returncode=self.process.returncode,
364
+ argv=args)
265
365
 
266
366
  def _set_default(self, prop_dict, key, value):
267
367
  if key not in prop_dict:
268
368
  prop_dict[key] = value
269
369
 
270
370
  def _mpv_fmt(self, data):
271
- if data == True:
371
+ # Use identity comparison so numeric arguments are preserved. In Python
372
+ # ``1 == True`` and ``0 == False``, so ``==`` would convert numeric
373
+ # options such as ``video-scale-x=1`` into ``--video-scale-x=yes``,
374
+ # which MPV rejects.
375
+ if data is True:
272
376
  return "yes"
273
- elif data == False:
377
+ elif data is False:
274
378
  return "no"
275
379
  else:
276
380
  return data
@@ -405,7 +509,8 @@ class MPV:
405
509
  list is used. Not all commands may actually work when this fallback is used.
406
510
  """
407
511
  def __init__(self, start_mpv=True, ipc_socket=None, mpv_location=None,
408
- log_handler=None, loglevel=None, quit_callback=None, start_retries=5, start_retry_delay_ms=1000, **kwargs):
512
+ log_handler=None, loglevel=None, quit_callback=None, start_retries=5, start_retry_delay_ms=1000,
513
+ diagnose_start_failures=True, **kwargs):
409
514
  """
410
515
  Create the interface to MPV and process instance.
411
516
 
@@ -415,6 +520,10 @@ class MPV:
415
520
  *log_handler(level, prefix, text)* is an optional handler for log events. (Default: Disabled)
416
521
  *loglevel* is the level for log messages. Levels are fatal, error, warn, info, v, debug, trace. (Default: Disabled)
417
522
  *quit_callback* is called when the socket connection to MPV dies.
523
+ *diagnose_start_failures* re-runs MPV once, on the first failed start, to find out
524
+ why it refused to start. If MPV names an option it does not have, MPVProcessError is
525
+ raised immediately with *bad_option* set instead of spending every retry on something
526
+ that cannot succeed. Set it to False for the pre-1.3 behaviour. (Default: True)
418
527
 
419
528
  All other arguments are forwarded to MPV as command-line arguments if *start_mpv* is used.
420
529
  """
@@ -436,16 +545,56 @@ class MPV:
436
545
 
437
546
  if start_mpv:
438
547
  # Attempt to start MPV multiple times.
548
+ last_error = None
549
+ diagnosed = False
550
+ log_output = None
439
551
  for i in range(start_retries):
440
552
  try:
441
553
  self.mpv_process = MPVProcess(ipc_socket, mpv_location, **kwargs)
442
554
  break
443
- except MPVError:
555
+ except MPVError as error:
556
+ last_error = error
444
557
  log.warning("MPV start failed.", exc_info=1)
558
+
559
+ # Ask MPV why, once, on the first failure. An option MPV
560
+ # does not have cannot start on the fifth attempt either,
561
+ # and spending the whole retry budget on it produces the
562
+ # least useful error this library has ever raised: a
563
+ # timeout that names nothing. The probe costs one ~20ms
564
+ # MPV run and only ever happens on a start that has
565
+ # already failed.
566
+ argv = getattr(error, "argv", None)
567
+ if diagnose_start_failures and argv and not diagnosed:
568
+ diagnosed = True
569
+ bad_option, diagnosis = _diagnose_start_failure(argv)
570
+ # Keep what MPV said even when it does not name an
571
+ # option. Refusing to start over a missing DLL or an
572
+ # unusable VO is the same predicament from the user's
573
+ # side -- something they changed, and nothing to go
574
+ # on -- and on Windows there is no console for them to
575
+ # have seen it in. It rides out on .log_output below.
576
+ log_output = diagnosis
577
+ if bad_option is not None:
578
+ raise MPVProcessError(
579
+ "MPV rejected the option --{0}.".format(
580
+ bad_option),
581
+ returncode=getattr(error, "returncode", None),
582
+ bad_option=bad_option,
583
+ log_output=log_output,
584
+ retryable=False,
585
+ argv=argv)
586
+
445
587
  time.sleep(start_retry_delay_ms / 1000)
446
588
  continue
447
589
  else:
448
- raise MPVError("MPV process retry limit reached.")
590
+ # The message is unchanged on purpose. Downstream projects
591
+ # that will never be updated may be matching on it, and this
592
+ # is still the same situation it has always described.
593
+ raise MPVProcessError(
594
+ "MPV process retry limit reached.",
595
+ returncode=getattr(last_error, "returncode", None),
596
+ log_output=log_output,
597
+ argv=getattr(last_error, "argv", None))
449
598
 
450
599
  self.mpv_inter = MPVInter(ipc_socket, self._callback, self._quit_callback)
451
600
  self.properties = set(x.replace("-", "_") for x in self.command("get_property", "property-list"))
@@ -1,6 +0,0 @@
1
- python_mpv_jsonipc.py,sha256=McfALK21Mp48Ekpg8wszSnHlSExtlrpsrw90i9syvng,24041
2
- python_mpv_jsonipc-1.2.2.dist-info/licenses/LICENSE.md,sha256=7GQBUOA3dNG1Jk-yaPOCncNnRVURCRMev23QpZ1ew-s,10495
3
- python_mpv_jsonipc-1.2.2.dist-info/METADATA,sha256=AjVHMMb5-bKwYuf89xM3J9jLRPX17v-iXkMvuft8AF0,2277
4
- python_mpv_jsonipc-1.2.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
- python_mpv_jsonipc-1.2.2.dist-info/top_level.txt,sha256=nGk4yjpVHKmIrL39XdGD2PhCeWF0oMjwey0nsNOd2TM,19
6
- python_mpv_jsonipc-1.2.2.dist-info/RECORD,,