python-mpv-jsonipc 1.4.0__py3-none-any.whl → 1.5.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.4.0
3
+ Version: 1.5.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
@@ -55,6 +55,10 @@ mpv.volume = 20
55
55
  # You can also send commands.
56
56
  mpv.command_name(arg1, arg2)
57
57
 
58
+ # Or send one without waiting for it. Returns a concurrent.futures.Future.
59
+ mpv.command_async("sub-add", "https://example.com/slow.srt",
60
+ callback=lambda error, result: print(error or "loaded"))
61
+
58
62
  # Bind to key press events with a decorator
59
63
  @mpv.on_key_press("space")
60
64
  def space_handler():
@@ -0,0 +1,6 @@
1
+ python_mpv_jsonipc.py,sha256=6xQ45aP3vLNxLy6OdyReB-MPnQvmOJ00CzzNQ13iGS4,61266
2
+ python_mpv_jsonipc-1.5.0.dist-info/licenses/LICENSE.md,sha256=7GQBUOA3dNG1Jk-yaPOCncNnRVURCRMev23QpZ1ew-s,10495
3
+ python_mpv_jsonipc-1.5.0.dist-info/METADATA,sha256=axbDuWhKzIPoR1rvoF-b-MXVJx78LqF2MzFK6CGbmP8,2489
4
+ python_mpv_jsonipc-1.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
5
+ python_mpv_jsonipc-1.5.0.dist-info/top_level.txt,sha256=nGk4yjpVHKmIrL39XdGD2PhCeWF0oMjwey0nsNOd2TM,19
6
+ python_mpv_jsonipc-1.5.0.dist-info/RECORD,,
python_mpv_jsonipc.py CHANGED
@@ -8,6 +8,7 @@ import subprocess
8
8
  import random
9
9
  import queue
10
10
  import logging
11
+ from concurrent.futures import Future
11
12
 
12
13
  log = logging.getLogger('mpv-jsonipc')
13
14
 
@@ -420,8 +421,13 @@ class WindowsSocket(threading.Thread):
420
421
  # finally below, only once its own read has returned.
421
422
  nread = self._transfer(_kernel32.ReadFile, self._read_event,
422
423
  buf, len(buf))
423
- if not nread:
424
+ if nread is None: # the peer is gone, or stop()
424
425
  break
426
+ if nread == 0:
427
+ # A zero-length message, not the end: mpv's pipe is
428
+ # message-mode and it answers an async command with an
429
+ # empty write. Treating it as EOF closed the connection.
430
+ continue
425
431
 
426
432
  data += buf.raw[:nread]
427
433
  if data[-1] != 10:
@@ -699,6 +705,27 @@ class MPVProcess:
699
705
  except FileNotFoundError:
700
706
  pass # The other thread got there first, which is all we wanted.
701
707
 
708
+ #: Stands in a sync command's result slot when the connection closed first.
709
+ _CLOSED = object()
710
+
711
+ def _call_user(what, func, *args):
712
+ """Call user code from a thread this library owns, logging anything it
713
+ raises -- SystemExit included: a caller's bug must not end the reader,
714
+ the close sweep or the drain. Not for EventHandler.run, which a
715
+ BaseException still ends (see there)."""
716
+ try:
717
+ func(*args)
718
+ except BaseException:
719
+ log.error("%s raised.", what, exc_info=1)
720
+
721
+ def _reply_outcome(data):
722
+ """``(error, result)`` for one IPC reply."""
723
+ if data["error"] == "success":
724
+ return None, data.get("data")
725
+ if data["error"] == "property unavailable":
726
+ return None, None
727
+ return MPVError(data["error"]), None
728
+
702
729
  class MPVInter:
703
730
  """
704
731
  Low-level interface to MPV. Does NOT manage an mpv process. (Internal)
@@ -708,6 +735,7 @@ class MPVInter:
708
735
 
709
736
  *ipc_socket* is the path to the Unix/Linux socket or name of the Windows pipe.
710
737
  *callback(event_name, data)* is the function for recieving events.
738
+ It runs on the reader thread; an exception from it is logged.
711
739
  *quit_callback* is called when the socket connection to MPV dies.
712
740
  """
713
741
  Socket = UnixSocket
@@ -719,25 +747,64 @@ class MPVInter:
719
747
  if self.callback is None:
720
748
  self.callback = lambda event, data: None
721
749
 
722
- self.socket = Socket(ipc_socket, self.event_callback, self.quit_callback)
723
- self.socket.start()
724
750
  self.command_id = 1
725
751
  self.rid_lock = threading.Lock()
726
752
  self.socket_lock = threading.Lock()
727
753
  self.cid_result = {}
728
754
  self.cid_wait = {}
755
+ # Before the reader starts: it can deliver a reply, or die, at once.
756
+ self.cid_async = {}
757
+ self.async_lock = threading.Lock()
758
+ self.closed = False
759
+
760
+ self.socket = Socket(ipc_socket, self.event_callback, self._on_quit)
761
+ self.socket.start()
729
762
 
730
763
  def stop(self, join=True):
731
764
  """Terminate the underlying connection."""
732
765
  self.socket.stop(join)
766
+ # The reader's _on_quit usually gets here first. This one does not
767
+ # depend on it, so nothing is left pending once stop() returns.
768
+ self._fail_async()
769
+
770
+ def _on_quit(self):
771
+ self._fail_async()
772
+ if self.quit_callback:
773
+ self.quit_callback()
774
+
775
+ def _fail_async(self):
776
+ """Answer every pending command, async or not: nothing else ever will."""
777
+ with self.async_lock:
778
+ self.closed = True
779
+ pending, self.cid_async = self.cid_async, {}
780
+ for command_id, event in list(self.cid_wait.items()):
781
+ # setdefault: a reply that landed just before still wins.
782
+ self.cid_result.setdefault(command_id, _CLOSED)
783
+ event.set()
784
+ for on_reply in pending.values():
785
+ _call_user("command_async reply handler", on_reply,
786
+ BrokenPipeError("MPV connection closed."), None)
733
787
 
734
788
  def event_callback(self, data):
735
789
  """Internal callback for recieving events from MPV."""
736
790
  if "request_id" in data:
737
- self.cid_result[data["request_id"]] = data
738
- self.cid_wait[data["request_id"]].set()
791
+ request_id = data["request_id"]
792
+ with self.async_lock:
793
+ on_reply = self.cid_async.pop(request_id, None)
794
+ if on_reply is not None:
795
+ _call_user("command_async reply handler", on_reply,
796
+ *_reply_outcome(data))
797
+ return
798
+ # Not indexed: a reply nobody waits for (mpv answers id 0 to a
799
+ # request without one) would raise here, on the reader thread,
800
+ # and end the read loop as if the connection had died.
801
+ event = self.cid_wait.get(request_id)
802
+ if event is not None:
803
+ self.cid_result[request_id] = data
804
+ event.set()
739
805
  elif "event" in data:
740
- self.callback(data["event"], data)
806
+ _call_user("MPVInter event callback", self.callback,
807
+ data["event"], data)
741
808
 
742
809
  def command(self, command, *args):
743
810
  """
@@ -746,7 +813,8 @@ class MPVInter:
746
813
  *command* is the name of the MPV command
747
814
 
748
815
  All further arguments are forwarded to the MPV command.
749
- Throws TimeoutError if timeout of 120 seconds is reached.
816
+ Throws TimeoutError if timeout of 120 seconds is reached, and
817
+ BrokenPipeError as soon as the connection closes before the reply.
750
818
  """
751
819
  self.rid_lock.acquire()
752
820
  command_id = self.command_id
@@ -754,7 +822,12 @@ class MPVInter:
754
822
  self.rid_lock.release()
755
823
 
756
824
  event = threading.Event()
757
- self.cid_wait[command_id] = event
825
+ # Under the lock _fail_async takes, so a close either finds this
826
+ # waiter or is seen here first; neither way waits out TIMEOUT.
827
+ with self.async_lock:
828
+ if self.closed:
829
+ raise BrokenPipeError("MPV connection closed.")
830
+ self.cid_wait[command_id] = event
758
831
 
759
832
  command_list = [command]
760
833
  command_list.extend(args)
@@ -766,23 +839,59 @@ class MPVInter:
766
839
 
767
840
  has_event = event.wait(timeout=TIMEOUT)
768
841
  if has_event:
769
- data = self.cid_result[command_id]
770
- del self.cid_result[command_id]
842
+ data = self.cid_result.pop(command_id)
771
843
  del self.cid_wait[command_id]
772
- if data["error"] != "success":
773
- if data["error"] == "property unavailable":
774
- return None
775
- raise MPVError(data["error"])
776
- else:
777
- return data.get("data")
844
+ if data is _CLOSED:
845
+ raise BrokenPipeError("MPV connection closed.")
846
+ error, result = _reply_outcome(data)
847
+ if error is not None:
848
+ raise error
849
+ return result
778
850
  else:
779
851
  raise TimeoutError("No response from MPV.")
780
852
 
853
+ def command_async(self, on_reply, command, *args):
854
+ """
855
+ Issue a command to MPV without waiting for it to finish.
856
+
857
+ *on_reply(error, result)* is called exactly once: with MPV's answer on
858
+ the transport's reader thread; with BrokenPipeError on the thread
859
+ that closes the connection, or on the calling thread if it is already
860
+ closed; or with the send's own error on the calling thread. An
861
+ exception from it is logged when the reader or the close calls it,
862
+ and raised to the caller otherwise. There is no timeout.
863
+ """
864
+ with self.rid_lock:
865
+ command_id = self.command_id
866
+ self.command_id += 1
867
+
868
+ with self.async_lock:
869
+ closed = self.closed
870
+ if not closed:
871
+ self.cid_async[command_id] = on_reply
872
+ if closed:
873
+ on_reply(BrokenPipeError("MPV connection closed."), None)
874
+ return
875
+
876
+ command_list = [command]
877
+ command_list.extend(args)
878
+ try:
879
+ with self.socket_lock:
880
+ self.socket.send({"command": command_list,
881
+ "request_id": command_id, "async": True})
882
+ except Exception as error:
883
+ with self.async_lock:
884
+ owned = self.cid_async.pop(command_id, None)
885
+ if owned is not None: # else _fail_async has already answered
886
+ on_reply(error, None)
887
+
781
888
  class EventHandler(threading.Thread):
782
889
  """Event handling thread. (Internal)"""
783
890
  def __init__(self):
784
891
  """Create an instance of the thread."""
785
892
  self.queue = queue.Queue()
893
+ self.stopped = False
894
+ self.stop_lock = threading.Lock()
786
895
  threading.Thread.__init__(self)
787
896
  self.daemon = True
788
897
 
@@ -793,24 +902,62 @@ class EventHandler(threading.Thread):
793
902
  *func* is the function to call
794
903
 
795
904
  All further arguments are forwarded to *func*.
905
+
906
+ Returns False, without queueing it, once the thread has stopped --
907
+ by stop(), or because a task raised SystemExit or the like.
796
908
  """
797
- self.queue.put((func, args))
909
+ with self.stop_lock:
910
+ if self.stopped:
911
+ return False
912
+ self.queue.put((func, args))
913
+ return True
798
914
 
799
915
  def stop(self, join=True):
800
916
  """Terminate the thread."""
801
- self.queue.put("quit")
802
- self.join(join)
917
+ with self.stop_lock:
918
+ self.stopped = True
919
+ self.queue.put("quit")
920
+ # A callback that calls terminate() is on this thread. The guard is
921
+ # the transports' (see UnixSocket.stop); `join` stays the timeout.
922
+ if self.ident is not None and self is not threading.current_thread():
923
+ self.join(join)
803
924
 
804
925
  def run(self):
805
926
  """Process socket events. Do not run this directly. Use *start*."""
927
+ try:
928
+ while True:
929
+ event = self.queue.get()
930
+ if event == "quit":
931
+ break
932
+ try:
933
+ event[0](*event[1])
934
+ except Exception:
935
+ log.error("EventHandler caught exception from %s.", event, exc_info=1)
936
+ except BaseException as ex:
937
+ log.warning("A callback raised %r, ending the event thread: "
938
+ "property observers, key bindings and events will no "
939
+ "longer be delivered; command_async callbacks now run "
940
+ "on whichever thread delivers their outcome.", ex)
941
+ # Not _call_user, deliberately: a callback's SystemExit ends this
942
+ # thread, as it has since 2020.
943
+ raise
944
+ finally:
945
+ self._drain()
946
+
947
+ def _drain(self):
948
+ """A callback's SystemExit (or similar) ends this thread, as it always
949
+ has. Refuse work from here on, so put_task says so, and run what was
950
+ already queued: a command_async future may be waiting in it."""
951
+ with self.stop_lock:
952
+ self.stopped = True
806
953
  while True:
807
- event = self.queue.get()
808
- if event == "quit":
809
- break
810
954
  try:
811
- event[0](*event[1])
812
- except Exception:
813
- log.error("EventHandler caught exception from {0}.".format(event), exc_info=1)
955
+ event = self.queue.get_nowait()
956
+ except queue.Empty:
957
+ return
958
+ if event == "quit":
959
+ continue
960
+ _call_user("EventHandler task", event[0], *event[1])
814
961
 
815
962
  class MPV:
816
963
  """
@@ -833,7 +980,8 @@ class MPV:
833
980
  *mpv_location* is the location of MPV for *start_mpv*. (Default: Use MPV in PATH)
834
981
  *log_handler(level, prefix, text)* is an optional handler for log events. (Default: Disabled)
835
982
  *loglevel* is the level for log messages. Levels are fatal, error, warn, info, v, debug, trace. (Default: Disabled)
836
- *quit_callback* is called when the socket connection to MPV dies.
983
+ *quit_callback* is called on the reader thread when the socket connection to MPV dies.
984
+ An exception from it is logged, and MPV is still terminated.
837
985
  *diagnose_start_failures* re-runs MPV once, on the first failed start, to find out
838
986
  why it refused to start. If MPV names an option it does not have, MPVProcessError is
839
987
  raised immediately with *bad_option* set instead of spending every retry on something
@@ -958,7 +1106,7 @@ class MPV:
958
1106
  Internal handler for quit events.
959
1107
  """
960
1108
  if self.quit_callback:
961
- self.quit_callback()
1109
+ _call_user("quit_callback", self.quit_callback)
962
1110
  self.terminate(join=False)
963
1111
 
964
1112
  def bind_event(self, name, callback):
@@ -1068,6 +1216,11 @@ class MPV:
1068
1216
  Waits for the value of a property to change.
1069
1217
 
1070
1218
  *name* is the name of the property.
1219
+
1220
+ Never call it from inside a callback here: the change is delivered on
1221
+ the event thread, which the wait would be blocking, so it never ends.
1222
+ It also never returns once a callback has ended the event thread,
1223
+ since the change is then never delivered.
1071
1224
  """
1072
1225
  event = threading.Event()
1073
1226
  first_event = True
@@ -1126,6 +1279,80 @@ class MPV:
1126
1279
  """
1127
1280
  return self.mpv_inter.command(command, *args)
1128
1281
 
1282
+ def command_async(self, command, *args, callback=None):
1283
+ """
1284
+ Send a command to MPV without waiting for it to finish, for one that
1285
+ can take a long time (``sub-add`` of a slow URL). Unlike *command*,
1286
+ it holds neither the caller nor the connection: other commands are
1287
+ answered while it runs, and it has no timeout.
1288
+
1289
+ Returns a ``concurrent.futures.Future``. With no *callback* it holds
1290
+ MPV's result or the error, as *command* would have returned or raised.
1291
+ *callback(error, result)* is called first, with *error* None on
1292
+ success, and the future then holds what it returns or raises.
1293
+
1294
+ The callback runs on the event thread, like every other callback
1295
+ here. If an earlier callback ended that thread by raising SystemExit
1296
+ or the like, a warning is logged, and property observers, key
1297
+ bindings and events are no longer delivered at all; command_async
1298
+ callbacks still run, on whichever thread delivers their outcome
1299
+ instead -- the connection's reader while MPV is connected, or the
1300
+ thread that closes the connection -- and a SystemExit from one there
1301
+ is logged, not raised. With the event thread gone, a call made once
1302
+ the connection has closed (by *terminate* or by MPV exiting), or
1303
+ whose send fails, runs its callback on the calling thread before
1304
+ command_async returns, and a SystemExit from it is raised to that
1305
+ caller. A logging handler that raises SystemExit is not caught
1306
+ anywhere: as in the standard library, it ends the thread that logged,
1307
+ and in the close that leaves the futures after it unresolved.
1308
+
1309
+ The future resolves when MPV answers or, if the connection closes
1310
+ first, with BrokenPipeError -- meaning no answer will come, not that
1311
+ the command did not run. Until then it is pending: a command MPV is
1312
+ still running has no timeout.
1313
+
1314
+ Never block on the future from inside a callback here (a property
1315
+ observer, a key binding, an event, another command_async callback).
1316
+ Those run on the event thread, the thread that would resolve it, so
1317
+ the wait never ends. python-mpv behaves the same.
1318
+
1319
+ Once the event thread is gone, a callback must not block at all. On
1320
+ the reader, a sync *command* waits out TIMEOUT with nothing else read
1321
+ meanwhile, and a wait on a future never ends -- nor does a
1322
+ *terminate* called from another thread. python-mpv fails here too:
1323
+ once a callback's SystemExit stops its event loop, no callback or
1324
+ future is delivered at all.
1325
+
1326
+ The future cannot be cancelled: MPV's IPC protocol cannot abort a
1327
+ command. Needs MPV 0.33 or later; an older MPV ignores the request to
1328
+ run in the background and runs the command as *command* does, blocking
1329
+ every other command on the connection until it finishes.
1330
+ """
1331
+ future = Future()
1332
+ future.set_running_or_notify_cancel()
1333
+
1334
+ def finish(error, result):
1335
+ try:
1336
+ if callback is not None:
1337
+ future.set_result(callback(error, result))
1338
+ elif error is not None:
1339
+ future.set_exception(error)
1340
+ else:
1341
+ future.set_result(result)
1342
+ except BaseException as ex:
1343
+ future.set_exception(ex)
1344
+ if not isinstance(ex, Exception):
1345
+ raise # which thread it reaches, and what happens: see the docstring
1346
+ log.error("command_async callback for {0} raised.".format(
1347
+ command), exc_info=1)
1348
+
1349
+ def on_reply(error, result):
1350
+ if not self.event_handler.put_task(finish, error, result):
1351
+ finish(error, result)
1352
+
1353
+ self.mpv_inter.command_async(on_reply, command, *args)
1354
+ return future
1355
+
1129
1356
  def __getattr__(self, name):
1130
1357
  if name in self.properties:
1131
1358
  return self.command("get_property", name.replace("_", "-"))
@@ -1,6 +0,0 @@
1
- python_mpv_jsonipc.py,sha256=g5BxL4Bzljm4QnsXG5kOKGGfHuiKlpqQqKmd5atQ-nM,50502
2
- python_mpv_jsonipc-1.4.0.dist-info/licenses/LICENSE.md,sha256=7GQBUOA3dNG1Jk-yaPOCncNnRVURCRMev23QpZ1ew-s,10495
3
- python_mpv_jsonipc-1.4.0.dist-info/METADATA,sha256=-gKDmmYj96EQjMvcTwgj_1AnBouKSD9ENAr1r77sVXU,2277
4
- python_mpv_jsonipc-1.4.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
5
- python_mpv_jsonipc-1.4.0.dist-info/top_level.txt,sha256=nGk4yjpVHKmIrL39XdGD2PhCeWF0oMjwey0nsNOd2TM,19
6
- python_mpv_jsonipc-1.4.0.dist-info/RECORD,,