python-mpv-jsonipc 1.2.3__py3-none-any.whl → 1.4.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.
- {python_mpv_jsonipc-1.2.3.dist-info → python_mpv_jsonipc-1.4.0.dist-info}/METADATA +1 -1
- python_mpv_jsonipc-1.4.0.dist-info/RECORD +6 -0
- {python_mpv_jsonipc-1.2.3.dist-info → python_mpv_jsonipc-1.4.0.dist-info}/WHEEL +1 -1
- python_mpv_jsonipc.py +530 -58
- python_mpv_jsonipc-1.2.3.dist-info/RECORD +0 -6
- {python_mpv_jsonipc-1.2.3.dist-info → python_mpv_jsonipc-1.4.0.dist-info}/licenses/LICENSE.md +0 -0
- {python_mpv_jsonipc-1.2.3.dist-info → python_mpv_jsonipc-1.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,6 @@
|
|
|
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,,
|
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
|
|
@@ -11,8 +12,100 @@ import logging
|
|
|
11
12
|
log = logging.getLogger('mpv-jsonipc')
|
|
12
13
|
|
|
13
14
|
if os.name == "nt":
|
|
14
|
-
import
|
|
15
|
-
|
|
15
|
+
import ctypes
|
|
16
|
+
import ctypes.wintypes as _w
|
|
17
|
+
|
|
18
|
+
# The Windows transport calls kernel32 directly rather than going through
|
|
19
|
+
# ``_winapi`` and ``multiprocessing.connection.PipeConnection``. The second
|
|
20
|
+
# of those is a live bug, not tidiness:
|
|
21
|
+
#
|
|
22
|
+
# PipeConnection says outright that "a connection should only be used by
|
|
23
|
+
# a single thread", and this library used it from two -- a reader parked
|
|
24
|
+
# in recv_bytes, and whoever called stop(). Its _close() cancels a
|
|
25
|
+
# pending *send* before closing the handle, but has no equivalent for a
|
|
26
|
+
# pending *read*, because _recv_bytes never stores its OVERLAPPED
|
|
27
|
+
# anywhere reachable. So stop() closed the handle with an overlapped
|
|
28
|
+
# read outstanding and a kernel-owned buffer still live, which is
|
|
29
|
+
# undefined: the kernel may complete the I/O into memory Python is
|
|
30
|
+
# tearing down. CPython 3.13+ allocates its thread primitives from the
|
|
31
|
+
# parking lot, so that corruption now lands as an abort --
|
|
32
|
+
# ``Fatal Python error: _PySemaphore_Wakeup: ReleaseSemaphore failed
|
|
33
|
+
# (error: 6)`` -- seen in CI on both 3.13 and 3.14.
|
|
34
|
+
#
|
|
35
|
+
# Neither ``_winapi`` (private C extension) nor ``PipeConnection`` (absent
|
|
36
|
+
# from __all__, defined conditionally, an implementation detail of Pipe())
|
|
37
|
+
# carries a compatibility contract, so this also removes a dependency that
|
|
38
|
+
# breaks along the *Python* axis rather than ours. ctypes is stdlib and
|
|
39
|
+
# needs no PyInstaller hooks or bundled DLLs, which is the property the
|
|
40
|
+
# original _winapi choice was buying and that downstream projects such as
|
|
41
|
+
# SyncPlay and jellyfin-mpv-shim adopted this library for.
|
|
42
|
+
_kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
43
|
+
|
|
44
|
+
_GENERIC_READ = 0x80000000
|
|
45
|
+
_GENERIC_WRITE = 0x40000000
|
|
46
|
+
_OPEN_EXISTING = 3
|
|
47
|
+
_FILE_FLAG_OVERLAPPED = 0x40000000
|
|
48
|
+
_INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
|
|
49
|
+
_INFINITE = 0xFFFFFFFF
|
|
50
|
+
_WAIT_OBJECT_0 = 0
|
|
51
|
+
_ERROR_FILE_NOT_FOUND = 2
|
|
52
|
+
_ERROR_PATH_NOT_FOUND = 3
|
|
53
|
+
_ERROR_BROKEN_PIPE = 109
|
|
54
|
+
# Also "the other end is gone", and reached when the peer disconnects
|
|
55
|
+
# rather than exits. Without them a normal disconnect logs an ERROR and a
|
|
56
|
+
# traceback. Measured on real Windows, including against the old
|
|
57
|
+
# PipeConnection build, which logged it too -- it only raised EOFError for
|
|
58
|
+
# 109 -- so this is an improvement on the previous behaviour rather than a
|
|
59
|
+
# regression being repaired. Real MPV exiting gives 109, which is why no
|
|
60
|
+
# CI leg ever showed it.
|
|
61
|
+
_ERROR_NO_DATA = 232
|
|
62
|
+
_ERROR_PIPE_NOT_CONNECTED = 233
|
|
63
|
+
_ERROR_MORE_DATA = 234
|
|
64
|
+
_ERROR_OPERATION_ABORTED = 995
|
|
65
|
+
_ERROR_IO_PENDING = 997
|
|
66
|
+
|
|
67
|
+
#: Every way the far end can be gone. All of them are a clean end of
|
|
68
|
+
#: stream, not an error to log.
|
|
69
|
+
_PEER_GONE = (_ERROR_BROKEN_PIPE, _ERROR_NO_DATA, _ERROR_PIPE_NOT_CONNECTED)
|
|
70
|
+
|
|
71
|
+
class _OVERLAPPED(ctypes.Structure):
|
|
72
|
+
_fields_ = [("Internal", ctypes.c_void_p),
|
|
73
|
+
("InternalHigh", ctypes.c_void_p),
|
|
74
|
+
("Offset", _w.DWORD),
|
|
75
|
+
("OffsetHigh", _w.DWORD),
|
|
76
|
+
("hEvent", _w.HANDLE)]
|
|
77
|
+
|
|
78
|
+
# Declared explicitly: without argtypes ctypes truncates pointers to int
|
|
79
|
+
# on 64-bit, which fails in ways that look like random handle errors.
|
|
80
|
+
_kernel32.CreateFileW.argtypes = [_w.LPCWSTR, _w.DWORD, _w.DWORD,
|
|
81
|
+
ctypes.c_void_p, _w.DWORD, _w.DWORD,
|
|
82
|
+
_w.HANDLE]
|
|
83
|
+
_kernel32.CreateFileW.restype = _w.HANDLE
|
|
84
|
+
_kernel32.CreateEventW.argtypes = [ctypes.c_void_p, _w.BOOL, _w.BOOL,
|
|
85
|
+
_w.LPCWSTR]
|
|
86
|
+
_kernel32.CreateEventW.restype = _w.HANDLE
|
|
87
|
+
_kernel32.SetEvent.argtypes = [_w.HANDLE]
|
|
88
|
+
_kernel32.SetEvent.restype = _w.BOOL
|
|
89
|
+
_kernel32.ResetEvent.argtypes = [_w.HANDLE]
|
|
90
|
+
_kernel32.ResetEvent.restype = _w.BOOL
|
|
91
|
+
_kernel32.ReadFile.argtypes = [_w.HANDLE, ctypes.c_void_p, _w.DWORD,
|
|
92
|
+
_w.LPDWORD, ctypes.c_void_p]
|
|
93
|
+
_kernel32.ReadFile.restype = _w.BOOL
|
|
94
|
+
_kernel32.WriteFile.argtypes = [_w.HANDLE, ctypes.c_void_p, _w.DWORD,
|
|
95
|
+
_w.LPDWORD, ctypes.c_void_p]
|
|
96
|
+
_kernel32.WriteFile.restype = _w.BOOL
|
|
97
|
+
_kernel32.GetOverlappedResult.argtypes = [_w.HANDLE, ctypes.c_void_p,
|
|
98
|
+
_w.LPDWORD, _w.BOOL]
|
|
99
|
+
_kernel32.GetOverlappedResult.restype = _w.BOOL
|
|
100
|
+
_kernel32.CancelIoEx.argtypes = [_w.HANDLE, ctypes.c_void_p]
|
|
101
|
+
_kernel32.CancelIoEx.restype = _w.BOOL
|
|
102
|
+
_kernel32.WaitForMultipleObjects.argtypes = [_w.DWORD, ctypes.c_void_p,
|
|
103
|
+
_w.BOOL, _w.DWORD]
|
|
104
|
+
_kernel32.WaitForMultipleObjects.restype = _w.DWORD
|
|
105
|
+
_kernel32.CloseHandle.argtypes = [_w.HANDLE]
|
|
106
|
+
_kernel32.CloseHandle.restype = _w.BOOL
|
|
107
|
+
_kernel32.WaitNamedPipeW.argtypes = [_w.LPCWSTR, _w.DWORD]
|
|
108
|
+
_kernel32.WaitNamedPipeW.restype = _w.BOOL
|
|
16
109
|
|
|
17
110
|
TIMEOUT = 120
|
|
18
111
|
|
|
@@ -35,6 +128,79 @@ class MPVError(Exception):
|
|
|
35
128
|
def __init__(self, *args, **kwargs):
|
|
36
129
|
super(MPVError, self).__init__(*args, **kwargs)
|
|
37
130
|
|
|
131
|
+
class MPVProcessError(MPVError):
|
|
132
|
+
"""MPV would not start, with whatever MPV said about why.
|
|
133
|
+
|
|
134
|
+
A subclass so that every existing ``except MPVError`` keeps working
|
|
135
|
+
unchanged -- this library is depended on by projects that will never be
|
|
136
|
+
updated, and several of them catch only that.
|
|
137
|
+
|
|
138
|
+
*returncode* is MPV's exit status, if it got far enough to have one.
|
|
139
|
+
*bad_option* is the option MPV refused, when that could be determined.
|
|
140
|
+
*log_output* is what MPV wrote about the failure.
|
|
141
|
+
*retryable* is False when starting again cannot possibly work.
|
|
142
|
+
*argv* is the command line that was tried.
|
|
143
|
+
"""
|
|
144
|
+
def __init__(self, message, returncode=None, bad_option=None,
|
|
145
|
+
log_output=None, retryable=True, argv=None):
|
|
146
|
+
super(MPVProcessError, self).__init__(message)
|
|
147
|
+
self.returncode = returncode
|
|
148
|
+
self.bad_option = bad_option
|
|
149
|
+
self.log_output = log_output
|
|
150
|
+
self.retryable = retryable
|
|
151
|
+
self.argv = argv
|
|
152
|
+
|
|
153
|
+
#: MPV's own wording when it is handed an option it does not have. Matching
|
|
154
|
+
#: it is what turns "MPV exited" into "MPV has no --input-gamepad".
|
|
155
|
+
_OPTION_ERROR = re.compile(r"Error parsing option ([^\s]+) \(option not found\)")
|
|
156
|
+
|
|
157
|
+
def _diagnose_start_failure(argv, timeout=15):
|
|
158
|
+
"""Ask MPV why it refused *argv*. Returns ``(bad_option, output)``.
|
|
159
|
+
|
|
160
|
+
Three details make this reliable, and each was measured rather than
|
|
161
|
+
reasoned about:
|
|
162
|
+
|
|
163
|
+
* **The message comes back on stdout, not stderr.** MPV writes its
|
|
164
|
+
terminal output to stdout; stderr is empty. Capturing the wrong stream
|
|
165
|
+
gets a confident, permanent "no idea why".
|
|
166
|
+
* **``--terminal=yes`` has to come first.** Options are parsed in order
|
|
167
|
+
and this library sets ``--terminal=no`` by default, so with the flags
|
|
168
|
+
left as they are MPV has already silenced itself by the time it reaches
|
|
169
|
+
the option it objects to. The library's own terminal flags are dropped
|
|
170
|
+
rather than fought with.
|
|
171
|
+
* **``--version`` goes last**, so a configuration that turns out to be
|
|
172
|
+
fine parses everything and then exits (~15ms) instead of starting up
|
|
173
|
+
and having to be killed. It exits before binding the IPC socket, so
|
|
174
|
+
this cannot disturb the real MPV that is about to start.
|
|
175
|
+
|
|
176
|
+
An earlier version of this read ``--log-file`` instead, on the theory
|
|
177
|
+
that Windows has no stderr to capture. That log is written
|
|
178
|
+
**unreliably**: MPV does not flush it before exiting, so it arrives
|
|
179
|
+
truncated at a random point -- measured at 6/8 and 7/8 runs containing
|
|
180
|
+
the error, cut off anywhere from 102 bytes in. A pipe has no such
|
|
181
|
+
problem, and is byte-identical across runs.
|
|
182
|
+
|
|
183
|
+
Every failure here is answered with ``(None, ...)``, which puts the
|
|
184
|
+
caller back on the path it took before this existed. Being unable to
|
|
185
|
+
explain a failed start must never turn into a second failure.
|
|
186
|
+
"""
|
|
187
|
+
terminal_flags = ("--terminal=", "--input-terminal=")
|
|
188
|
+
argv = list(argv)
|
|
189
|
+
probe = ([argv[0], "--terminal=yes"]
|
|
190
|
+
+ [a for a in argv[1:] if not a.startswith(terminal_flags)]
|
|
191
|
+
+ ["--version"])
|
|
192
|
+
try:
|
|
193
|
+
completed = subprocess.run(probe,
|
|
194
|
+
stdout=subprocess.PIPE,
|
|
195
|
+
stderr=subprocess.STDOUT,
|
|
196
|
+
timeout=timeout)
|
|
197
|
+
except Exception:
|
|
198
|
+
log.debug("Could not run MPV to diagnose the failure.", exc_info=1)
|
|
199
|
+
return None, None
|
|
200
|
+
output = completed.stdout.decode("utf-8", "replace")
|
|
201
|
+
match = _OPTION_ERROR.search(output)
|
|
202
|
+
return (match.group(1) if match else None), output
|
|
203
|
+
|
|
38
204
|
class WindowsSocket(threading.Thread):
|
|
39
205
|
"""
|
|
40
206
|
Wraps a Windows named pipe in a high-level interface. (Internal)
|
|
@@ -49,62 +215,215 @@ class WindowsSocket(threading.Thread):
|
|
|
49
215
|
*callback(json_data)* is the function for recieving events.
|
|
50
216
|
*quit_callback* is called when the socket connection dies.
|
|
51
217
|
"""
|
|
218
|
+
# First, so Thread's own attributes are in place before ours rather
|
|
219
|
+
# than landing on top of them. Thread grew a `self._handle` of its own
|
|
220
|
+
# in 3.13, so the pipe handle is `_pipe`: named `_handle` and assigned
|
|
221
|
+
# before this call, Thread silently replaced it with a
|
|
222
|
+
# _thread._ThreadHandle and every read died in ctypes argument
|
|
223
|
+
# conversion. Anything added below needs a name Thread does not use.
|
|
224
|
+
threading.Thread.__init__(self)
|
|
225
|
+
self.daemon = True
|
|
226
|
+
|
|
52
227
|
ipc_socket = "\\\\.\\pipe\\" + ipc_socket
|
|
53
228
|
self.callback = callback
|
|
54
229
|
self.quit_callback = quit_callback
|
|
55
230
|
self._stopping = False
|
|
56
|
-
|
|
57
|
-
|
|
231
|
+
self._closed = False
|
|
232
|
+
# Every send is held under this lock, so holding it is proof that no
|
|
233
|
+
# write is in flight and the pipe is safe to close. The reader is the
|
|
234
|
+
# exception and must stay one -- see run().
|
|
235
|
+
self._io_lock = threading.RLock()
|
|
236
|
+
# Guards the handle *values* against being used after _close cleared
|
|
237
|
+
# them. Held only for the moment it takes to signal or close, never
|
|
238
|
+
# across a wait.
|
|
239
|
+
self._handle_lock = threading.RLock()
|
|
240
|
+
|
|
241
|
+
access = _GENERIC_READ | _GENERIC_WRITE
|
|
58
242
|
limit = 5 # Connection may fail at first. Try 5 times.
|
|
59
243
|
for _ in range(limit):
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
244
|
+
# Still five attempts, and still load-bearing: _ipc_endpoint_ready
|
|
245
|
+
# does not run on the start_mpv=False attach path, and it answers
|
|
246
|
+
# True for a pipe whose instances are all busy, which CreateFile
|
|
247
|
+
# then refuses with ERROR_PIPE_BUSY.
|
|
248
|
+
handle = _kernel32.CreateFileW(
|
|
249
|
+
ipc_socket, access, 0, None, _OPEN_EXISTING,
|
|
250
|
+
_FILE_FLAG_OVERLAPPED, None)
|
|
251
|
+
if handle != _INVALID_HANDLE_VALUE:
|
|
65
252
|
break
|
|
66
|
-
|
|
67
|
-
time.sleep(1)
|
|
253
|
+
time.sleep(1)
|
|
68
254
|
else:
|
|
255
|
+
# Deliberately not an OSError: downstream and the failure tests
|
|
256
|
+
# both pin that this is what the Windows attach path raises.
|
|
69
257
|
raise MPVError("Cannot connect to pipe.")
|
|
70
|
-
self.
|
|
258
|
+
self._pipe = handle
|
|
259
|
+
|
|
260
|
+
# Manual-reset, so a stop stays visible to the reader and to any send
|
|
261
|
+
# waiting alongside it rather than waking exactly one of them.
|
|
262
|
+
self._stop_event = _kernel32.CreateEventW(None, True, False, None)
|
|
263
|
+
self._read_event = _kernel32.CreateEventW(None, True, False, None)
|
|
264
|
+
self._write_event = _kernel32.CreateEventW(None, True, False, None)
|
|
265
|
+
if not all((self._stop_event, self._read_event, self._write_event)):
|
|
266
|
+
self._close()
|
|
267
|
+
raise MPVError("Cannot create pipe events.")
|
|
71
268
|
|
|
72
269
|
if self.callback is None:
|
|
73
270
|
self.callback = lambda data: None
|
|
74
271
|
|
|
75
|
-
|
|
76
|
-
|
|
272
|
+
def _transfer(self, func, event, buf, size):
|
|
273
|
+
"""Run one overlapped operation, waiting for it or for *stop*.
|
|
274
|
+
|
|
275
|
+
Returns the bytes transferred, or None if the pipe ended or *stop* cut
|
|
276
|
+
the operation short.
|
|
277
|
+
|
|
278
|
+
The GetOverlappedResult(wait=True) below is the point of this whole
|
|
279
|
+
class. It does not return until the kernel has finished with *buf* and
|
|
280
|
+
the OVERLAPPED -- including when we cancelled it -- so neither is ever
|
|
281
|
+
released while an I/O could still write into it.
|
|
282
|
+
"""
|
|
283
|
+
ov = _OVERLAPPED()
|
|
284
|
+
ov.hEvent = event
|
|
285
|
+
_kernel32.ResetEvent(event)
|
|
286
|
+
moved = _w.DWORD(0)
|
|
287
|
+
|
|
288
|
+
# The guarded region starts *before* the call, and assumes the
|
|
289
|
+
# operation may be outstanding until proven otherwise. An
|
|
290
|
+
# asynchronous exception -- a KeyboardInterrupt, which lands on the
|
|
291
|
+
# main thread, which is where send() runs -- can arrive between the
|
|
292
|
+
# call returning ERROR_IO_PENDING and any flag recording that, so a
|
|
293
|
+
# flag set after the call is wrong in exactly the direction that
|
|
294
|
+
# matters: it would let the frame, and its buffer, go while the kernel
|
|
295
|
+
# still owns them. Assuming the worst costs nothing when nothing was
|
|
296
|
+
# started, because a fresh OVERLAPPED has Internal = STATUS_SUCCESS
|
|
297
|
+
# and GetOverlappedResult then returns immediately (measured).
|
|
298
|
+
outstanding = True
|
|
299
|
+
try:
|
|
300
|
+
if func(self._pipe, buf, size, ctypes.byref(moved),
|
|
301
|
+
ctypes.byref(ov)):
|
|
302
|
+
outstanding = False
|
|
303
|
+
return moved.value
|
|
304
|
+
|
|
305
|
+
err = ctypes.get_last_error()
|
|
306
|
+
if err != _ERROR_IO_PENDING:
|
|
307
|
+
outstanding = False
|
|
308
|
+
if err in _PEER_GONE:
|
|
309
|
+
return None
|
|
310
|
+
if err == _ERROR_MORE_DATA:
|
|
311
|
+
# A completed read that filled the buffer, not a failure.
|
|
312
|
+
# The completion path below already treats it that way;
|
|
313
|
+
# raising here would have desynchronised the two.
|
|
314
|
+
return moved.value
|
|
315
|
+
raise ctypes.WinError(err)
|
|
316
|
+
|
|
317
|
+
handles = (_w.HANDLE * 2)(event, self._stop_event)
|
|
318
|
+
waitres = _kernel32.WaitForMultipleObjects(2, handles, False,
|
|
319
|
+
_INFINITE)
|
|
320
|
+
if waitres != _WAIT_OBJECT_0:
|
|
321
|
+
# stop() wants us gone, or the wait itself failed. Either way
|
|
322
|
+
# we will not be consuming this operation. Read the wait's
|
|
323
|
+
# error *first*: CancelIoEx overwrites the thread's last error,
|
|
324
|
+
# so reading it afterwards reports the cancel's success and
|
|
325
|
+
# loses the real cause on the path hardest to reproduce.
|
|
326
|
+
waitfail = ctypes.get_last_error()
|
|
327
|
+
_kernel32.CancelIoEx(self._pipe, ctypes.byref(ov))
|
|
328
|
+
if waitres != _WAIT_OBJECT_0 + 1:
|
|
329
|
+
raise ctypes.WinError(waitfail)
|
|
330
|
+
except BaseException:
|
|
331
|
+
if outstanding:
|
|
332
|
+
_kernel32.CancelIoEx(self._pipe, ctypes.byref(ov))
|
|
333
|
+
raise
|
|
334
|
+
finally:
|
|
335
|
+
# Cancelling only *asks*. This is the wait that makes it true.
|
|
336
|
+
if outstanding:
|
|
337
|
+
completed = _kernel32.GetOverlappedResult(
|
|
338
|
+
self._pipe, ctypes.byref(ov), ctypes.byref(moved), True)
|
|
339
|
+
failure = 0 if completed else ctypes.get_last_error()
|
|
340
|
+
|
|
341
|
+
if not completed:
|
|
342
|
+
if failure in _PEER_GONE or failure == _ERROR_OPERATION_ABORTED:
|
|
343
|
+
return None
|
|
344
|
+
if failure != _ERROR_MORE_DATA:
|
|
345
|
+
raise ctypes.WinError(failure)
|
|
346
|
+
return moved.value
|
|
347
|
+
|
|
348
|
+
def _close(self):
|
|
349
|
+
"""Close the handles, once, with no I/O outstanding."""
|
|
350
|
+
# _io_lock first: holding it means no send is mid-write. _handle_lock
|
|
351
|
+
# second, to exclude a concurrent stop() signalling an event we are
|
|
352
|
+
# about to close. Nothing ever takes these in the other order, and
|
|
353
|
+
# _handle_lock is never held across a wait, so stop() cannot block.
|
|
354
|
+
with self._io_lock, self._handle_lock:
|
|
355
|
+
if self._closed:
|
|
356
|
+
return
|
|
357
|
+
self._closed = True
|
|
358
|
+
for name in ("_pipe", "_stop_event", "_read_event",
|
|
359
|
+
"_write_event"):
|
|
360
|
+
handle = getattr(self, name, None)
|
|
361
|
+
if handle:
|
|
362
|
+
_kernel32.CloseHandle(handle)
|
|
363
|
+
# Cleared, not just closed: terminate() is routinely called
|
|
364
|
+
# twice, and a closed handle value can be reused by the OS.
|
|
365
|
+
setattr(self, name, None)
|
|
77
366
|
|
|
78
367
|
def stop(self, join=True):
|
|
79
368
|
"""Terminate the thread."""
|
|
80
369
|
self._stopping = True
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
370
|
+
# Deliberately does NOT close the handle. Closing it from here with
|
|
371
|
+
# the reader's overlapped read still pending is the corruption this
|
|
372
|
+
# class was rewritten to remove; instead we wake the reader and let it
|
|
373
|
+
# close the handle it owns, once its read has provably finished.
|
|
374
|
+
with self._handle_lock:
|
|
375
|
+
if self._stop_event:
|
|
376
|
+
_kernel32.SetEvent(self._stop_event)
|
|
377
|
+
# Two ways join() raises, and both would skip the _close() below and
|
|
378
|
+
# leak all four handles. is_alive() is True for the calling thread, so
|
|
379
|
+
# it never prevented the self-join it looked like it prevented -- the
|
|
380
|
+
# reader reaches here whenever a user's quit_callback calls
|
|
381
|
+
# terminate(). And `ident is None` means start() was never called,
|
|
382
|
+
# where join() raises too.
|
|
383
|
+
if (join and self.ident is not None
|
|
384
|
+
and self is not threading.current_thread()):
|
|
87
385
|
self.join()
|
|
386
|
+
if not self.is_alive():
|
|
387
|
+
# The reader has finished, or never ran. Either way nothing can
|
|
388
|
+
# have I/O outstanding, so the handle is ours to close.
|
|
389
|
+
self._close()
|
|
88
390
|
|
|
89
391
|
def send(self, data):
|
|
90
392
|
"""Send *data* to the pipe, encoded as JSON."""
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if len(ex.args) == 1 and ex.args[0] == "handle is closed":
|
|
393
|
+
blob = json.dumps(data).encode('utf-8') + b'\n'
|
|
394
|
+
with self._io_lock:
|
|
395
|
+
if self._closed:
|
|
95
396
|
raise BrokenPipeError("handle is closed")
|
|
96
|
-
|
|
397
|
+
written = self._transfer(_kernel32.WriteFile, self._write_event,
|
|
398
|
+
blob, len(blob))
|
|
399
|
+
if written is None:
|
|
400
|
+
raise BrokenPipeError("handle is closed")
|
|
97
401
|
|
|
98
402
|
def run(self):
|
|
99
403
|
"""Process pipe events. Do not run this directly. Use *start*."""
|
|
404
|
+
buf = ctypes.create_string_buffer(2048)
|
|
100
405
|
data = b''
|
|
101
406
|
try:
|
|
102
|
-
while
|
|
103
|
-
|
|
104
|
-
|
|
407
|
+
while not self._stopping:
|
|
408
|
+
# The stop event is only observed when a read *pends*, and a
|
|
409
|
+
# read that completes synchronously returns before ever
|
|
410
|
+
# reaching the wait. A peer with data always available could
|
|
411
|
+
# therefore keep stop() waiting in join(). Not reproducible --
|
|
412
|
+
# 124k messages of flooding still let the reader park -- but
|
|
413
|
+
# the flag costs nothing and closes the argument.
|
|
414
|
+
#
|
|
415
|
+
# Emphatically NOT under _io_lock: this read is parked
|
|
416
|
+
# whenever MPV has nothing to say, which is almost always, and
|
|
417
|
+
# holding the lock across it would block every send. The lock
|
|
418
|
+
# exists to keep a send and the close apart; the reader needs
|
|
419
|
+
# no such guard because it closes the handle itself, in the
|
|
420
|
+
# finally below, only once its own read has returned.
|
|
421
|
+
nread = self._transfer(_kernel32.ReadFile, self._read_event,
|
|
422
|
+
buf, len(buf))
|
|
423
|
+
if not nread:
|
|
105
424
|
break
|
|
106
425
|
|
|
107
|
-
data +=
|
|
426
|
+
data += buf.raw[:nread]
|
|
108
427
|
if data[-1] != 10:
|
|
109
428
|
continue
|
|
110
429
|
|
|
@@ -115,15 +434,14 @@ class WindowsSocket(threading.Thread):
|
|
|
115
434
|
json_data = json.loads(item)
|
|
116
435
|
self.callback(json_data)
|
|
117
436
|
data = b''
|
|
118
|
-
except
|
|
119
|
-
if self.quit_callback:
|
|
120
|
-
self.quit_callback()
|
|
121
|
-
except Exception as ex:
|
|
437
|
+
except Exception:
|
|
122
438
|
# Only log if not intentionally stopping
|
|
123
439
|
if not self._stopping:
|
|
124
440
|
log.error("Pipe connection died.", exc_info=1)
|
|
125
|
-
|
|
126
|
-
|
|
441
|
+
finally:
|
|
442
|
+
self._close()
|
|
443
|
+
if self.quit_callback:
|
|
444
|
+
self.quit_callback()
|
|
127
445
|
|
|
128
446
|
class UnixSocket(threading.Thread):
|
|
129
447
|
"""
|
|
@@ -155,28 +473,41 @@ class UnixSocket(threading.Thread):
|
|
|
155
473
|
def stop(self, join=True):
|
|
156
474
|
"""Terminate the thread."""
|
|
157
475
|
self._stopping = True
|
|
158
|
-
|
|
476
|
+
# Bind once. Two threads reach here -- the caller's terminate() and
|
|
477
|
+
# the reader's quit_callback -> terminate(join=False) -- and making
|
|
478
|
+
# stop() wait for MPV widened that overlap, because the wait ends on
|
|
479
|
+
# the same MPV exit that wakes the reader. Re-reading self.socket
|
|
480
|
+
# after the None check let the loser call shutdown/close on None,
|
|
481
|
+
# which is an AttributeError and so is *not* caught below.
|
|
482
|
+
sock, self.socket = self.socket, None
|
|
483
|
+
if sock is not None:
|
|
159
484
|
try:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
self.socket = None
|
|
485
|
+
sock.shutdown(socket.SHUT_WR)
|
|
486
|
+
sock.close()
|
|
163
487
|
except OSError:
|
|
164
488
|
pass # Ignore socket close failure.
|
|
165
|
-
if join
|
|
489
|
+
# Not a bare `if join`: the reader reaches here whenever a user's
|
|
490
|
+
# quit_callback calls terminate(), and joining yourself raises -- which
|
|
491
|
+
# escaped run() and leaked the event handler. `ident is None` is the
|
|
492
|
+
# never-started case, where join() raises as well.
|
|
493
|
+
if (join and self.ident is not None
|
|
494
|
+
and self is not threading.current_thread()):
|
|
166
495
|
self.join()
|
|
167
496
|
|
|
168
497
|
def send(self, data):
|
|
169
498
|
"""Send *data* to the socket, encoded as JSON."""
|
|
170
|
-
|
|
499
|
+
sock = self.socket # bind once; see stop()
|
|
500
|
+
if sock is None:
|
|
171
501
|
raise BrokenPipeError("socket is closed")
|
|
172
|
-
|
|
502
|
+
sock.send(json.dumps(data).encode('utf-8') + b'\n')
|
|
173
503
|
|
|
174
504
|
def run(self):
|
|
175
505
|
"""Process socket events. Do not run this directly. Use *start*."""
|
|
176
506
|
data = b''
|
|
507
|
+
sock = self.socket # bind once; see stop()
|
|
177
508
|
try:
|
|
178
509
|
while True:
|
|
179
|
-
current_data =
|
|
510
|
+
current_data = sock.recv(1024)
|
|
180
511
|
if current_data == b'':
|
|
181
512
|
break
|
|
182
513
|
|
|
@@ -198,16 +529,41 @@ class UnixSocket(threading.Thread):
|
|
|
198
529
|
if self.quit_callback:
|
|
199
530
|
self.quit_callback()
|
|
200
531
|
|
|
532
|
+
def _ipc_endpoint_ready(ipc_socket):
|
|
533
|
+
"""Return True once MPV's IPC endpoint is available.
|
|
534
|
+
|
|
535
|
+
On POSIX the endpoint is a filesystem socket, so ``os.path.exists`` works. On
|
|
536
|
+
Windows it is a *named pipe*, for which ``os.path.exists`` is always False — so we
|
|
537
|
+
probe it with ``WaitNamedPipe`` instead. Using ``os.path.exists`` on Windows made
|
|
538
|
+
the startup poll never detect the pipe, raising a spurious "MPV start timed out".
|
|
539
|
+
"""
|
|
540
|
+
if os.name != 'nt':
|
|
541
|
+
return os.path.exists(ipc_socket)
|
|
542
|
+
if _kernel32.WaitNamedPipeW(ipc_socket, 0):
|
|
543
|
+
return True
|
|
544
|
+
# Both codes, because the old _winapi version raised FileNotFoundError and
|
|
545
|
+
# CPython maps ERROR_PATH_NOT_FOUND to ENOENT as well. Treating 3 as
|
|
546
|
+
# "present but busy" would report a successful start and then burn
|
|
547
|
+
# WindowsSocket's five CreateFileW retries before failing with the wrong
|
|
548
|
+
# exception for the caller's retry loop.
|
|
549
|
+
if ctypes.get_last_error() in (_ERROR_FILE_NOT_FOUND, _ERROR_PATH_NOT_FOUND):
|
|
550
|
+
return False
|
|
551
|
+
# Pipe exists but all instances are momentarily busy — it is present.
|
|
552
|
+
return True
|
|
553
|
+
|
|
554
|
+
|
|
201
555
|
class MPVProcess:
|
|
202
556
|
"""
|
|
203
557
|
Manages an MPV process, ensuring the socket or pipe is available. (Internal)
|
|
204
558
|
"""
|
|
205
|
-
def __init__(self, ipc_socket, mpv_location=None, **kwargs):
|
|
559
|
+
def __init__(self, ipc_socket, mpv_location=None, discard_output=False, **kwargs):
|
|
206
560
|
"""
|
|
207
561
|
Create and start the MPV process. Will block until socket/pipe is available.
|
|
208
562
|
|
|
209
563
|
*ipc_socket* is the path to the Unix/Linux socket or name of the Windows pipe.
|
|
210
564
|
*mpv_location* is the path to mpv. If left unset it tries the one in the PATH.
|
|
565
|
+
*discard_output* sends MPV's stdout and stderr to devnull instead of inheriting
|
|
566
|
+
the caller's. (Default: False)
|
|
211
567
|
|
|
212
568
|
All other arguments are forwarded to MPV as command-line arguments.
|
|
213
569
|
"""
|
|
@@ -243,12 +599,20 @@ class MPVProcess:
|
|
|
243
599
|
|
|
244
600
|
args.extend("--{0}={1}".format(v[0].replace("_", "-"), self._mpv_fmt(v[1]))
|
|
245
601
|
for v in arg_pairs)
|
|
246
|
-
self.
|
|
602
|
+
self.argv = args
|
|
603
|
+
# close_fds only covers fds above 2, so MPV inherits our stdout and
|
|
604
|
+
# stderr. It never writes to them under --terminal=no, which is why
|
|
605
|
+
# this is invisible until something waits for EOF on the other end of
|
|
606
|
+
# that pipe: an MPV that outlives us holds it open and that reader
|
|
607
|
+
# blocks forever. Opt-in rather than default because callers who do
|
|
608
|
+
# want MPV's output on their terminal have had it for years.
|
|
609
|
+
stdio = subprocess.DEVNULL if discard_output else None
|
|
610
|
+
self.process = subprocess.Popen(args, stdout=stdio, stderr=stdio)
|
|
247
611
|
ipc_exists = False
|
|
248
612
|
for _ in range(100): # Give MPV 10 seconds to start.
|
|
249
613
|
time.sleep(0.1)
|
|
250
614
|
self.process.poll()
|
|
251
|
-
if
|
|
615
|
+
if _ipc_endpoint_ready(ipc_socket):
|
|
252
616
|
ipc_exists = True
|
|
253
617
|
log.debug("Found MPV socket.")
|
|
254
618
|
break
|
|
@@ -256,12 +620,22 @@ class MPVProcess:
|
|
|
256
620
|
log.error("MPV failed with returncode {0}.".format(self.process.returncode))
|
|
257
621
|
break
|
|
258
622
|
else:
|
|
259
|
-
|
|
260
|
-
|
|
623
|
+
# stop(), not a bare terminate(): a failed start orphans MPV
|
|
624
|
+
# exactly like a failed teardown did, and MPV.__init__ retries
|
|
625
|
+
# this up to start_retries times -- so a machine where MPV wedges
|
|
626
|
+
# without opening its pipe used to leave five live processes,
|
|
627
|
+
# each still holding the caller's stdout.
|
|
628
|
+
self._stop_quietly()
|
|
629
|
+
raise MPVProcessError("MPV start timed out.", argv=args)
|
|
261
630
|
|
|
262
631
|
if not ipc_exists or self.process.returncode is not None:
|
|
263
|
-
self.
|
|
264
|
-
|
|
632
|
+
self._stop_quietly()
|
|
633
|
+
# Message unchanged: it has been this string for years and is not
|
|
634
|
+
# ours to break. What is new is the argv riding along, which is
|
|
635
|
+
# what lets the caller ask MPV why.
|
|
636
|
+
raise MPVProcessError("MPV not started.",
|
|
637
|
+
returncode=self.process.returncode,
|
|
638
|
+
argv=args)
|
|
265
639
|
|
|
266
640
|
def _set_default(self, prop_dict, key, value):
|
|
267
641
|
if key not in prop_dict:
|
|
@@ -279,11 +653,51 @@ class MPVProcess:
|
|
|
279
653
|
else:
|
|
280
654
|
return data
|
|
281
655
|
|
|
282
|
-
def
|
|
283
|
-
"""
|
|
656
|
+
def _stop_quietly(self):
|
|
657
|
+
"""stop(), for the paths that are already raising something better.
|
|
658
|
+
|
|
659
|
+
A failed unlink here would replace MPVProcessError -- and an OSError
|
|
660
|
+
escapes MPV.__init__'s ``except MPVError`` retry loop, so the caller
|
|
661
|
+
would lose both the retries and the argv and returncode that the
|
|
662
|
+
error carries.
|
|
663
|
+
"""
|
|
664
|
+
try:
|
|
665
|
+
self.stop()
|
|
666
|
+
except OSError:
|
|
667
|
+
log.debug("Cleanup after a failed MPV start did not complete.",
|
|
668
|
+
exc_info=1)
|
|
669
|
+
|
|
670
|
+
def stop(self, timeout=5):
|
|
671
|
+
"""Terminate the process, and do not return while it may still be alive.
|
|
672
|
+
|
|
673
|
+
*timeout* is the seconds to wait for a polite exit before killing, and
|
|
674
|
+
again for the kill to land.
|
|
675
|
+
"""
|
|
676
|
+
# terminate() only *asks*. Returning here while MPV is still running
|
|
677
|
+
# left the caller no supported way to wait for it -- and a caller that
|
|
678
|
+
# exits at that moment orphans an MPV still holding its stdout. Both
|
|
679
|
+
# waits are bounded so a wedged MPV cannot hang teardown.
|
|
284
680
|
self.process.terminate()
|
|
285
|
-
|
|
286
|
-
|
|
681
|
+
try:
|
|
682
|
+
self.process.wait(timeout=timeout)
|
|
683
|
+
except subprocess.TimeoutExpired:
|
|
684
|
+
log.warning("MPV ignored terminate for {0}s; killing it.".format(timeout))
|
|
685
|
+
self.process.kill()
|
|
686
|
+
try:
|
|
687
|
+
self.process.wait(timeout=timeout)
|
|
688
|
+
except subprocess.TimeoutExpired:
|
|
689
|
+
log.error("MPV survived kill; giving up on reaping it.")
|
|
690
|
+
if os.name != 'nt':
|
|
691
|
+
# try/except rather than exists()-then-remove, because two threads
|
|
692
|
+
# reach here: the caller's terminate(), and the reader's
|
|
693
|
+
# quit_callback -> terminate(join=False) when MPV's exit closes
|
|
694
|
+
# the socket. Waiting above made them *overlap by construction* --
|
|
695
|
+
# the wait ends when MPV dies, which is the same event that wakes
|
|
696
|
+
# the reader -- so the check-then-act window is reliably lost.
|
|
697
|
+
try:
|
|
698
|
+
os.remove(self.ipc_socket)
|
|
699
|
+
except FileNotFoundError:
|
|
700
|
+
pass # The other thread got there first, which is all we wanted.
|
|
287
701
|
|
|
288
702
|
class MPVInter:
|
|
289
703
|
"""
|
|
@@ -409,7 +823,8 @@ class MPV:
|
|
|
409
823
|
list is used. Not all commands may actually work when this fallback is used.
|
|
410
824
|
"""
|
|
411
825
|
def __init__(self, start_mpv=True, ipc_socket=None, mpv_location=None,
|
|
412
|
-
log_handler=None, loglevel=None, quit_callback=None, start_retries=5, start_retry_delay_ms=1000,
|
|
826
|
+
log_handler=None, loglevel=None, quit_callback=None, start_retries=5, start_retry_delay_ms=1000,
|
|
827
|
+
diagnose_start_failures=True, discard_output=False, **kwargs):
|
|
413
828
|
"""
|
|
414
829
|
Create the interface to MPV and process instance.
|
|
415
830
|
|
|
@@ -419,6 +834,14 @@ class MPV:
|
|
|
419
834
|
*log_handler(level, prefix, text)* is an optional handler for log events. (Default: Disabled)
|
|
420
835
|
*loglevel* is the level for log messages. Levels are fatal, error, warn, info, v, debug, trace. (Default: Disabled)
|
|
421
836
|
*quit_callback* is called when the socket connection to MPV dies.
|
|
837
|
+
*diagnose_start_failures* re-runs MPV once, on the first failed start, to find out
|
|
838
|
+
why it refused to start. If MPV names an option it does not have, MPVProcessError is
|
|
839
|
+
raised immediately with *bad_option* set instead of spending every retry on something
|
|
840
|
+
that cannot succeed. Set it to False for the pre-1.3 behaviour. (Default: True)
|
|
841
|
+
*discard_output* sends MPV's stdout and stderr to devnull instead of letting it
|
|
842
|
+
inherit the caller's. Set it if anything waits for EOF on your stdout, since an
|
|
843
|
+
MPV that outlives *terminate* would otherwise hold that pipe open. Note this is
|
|
844
|
+
not MPV's own --quiet, which is still forwarded as an MPV option. (Default: False)
|
|
422
845
|
|
|
423
846
|
All other arguments are forwarded to MPV as command-line arguments if *start_mpv* is used.
|
|
424
847
|
"""
|
|
@@ -440,16 +863,57 @@ class MPV:
|
|
|
440
863
|
|
|
441
864
|
if start_mpv:
|
|
442
865
|
# Attempt to start MPV multiple times.
|
|
866
|
+
last_error = None
|
|
867
|
+
diagnosed = False
|
|
868
|
+
log_output = None
|
|
443
869
|
for i in range(start_retries):
|
|
444
870
|
try:
|
|
445
|
-
self.mpv_process = MPVProcess(ipc_socket, mpv_location,
|
|
871
|
+
self.mpv_process = MPVProcess(ipc_socket, mpv_location,
|
|
872
|
+
discard_output=discard_output, **kwargs)
|
|
446
873
|
break
|
|
447
|
-
except MPVError:
|
|
874
|
+
except MPVError as error:
|
|
875
|
+
last_error = error
|
|
448
876
|
log.warning("MPV start failed.", exc_info=1)
|
|
877
|
+
|
|
878
|
+
# Ask MPV why, once, on the first failure. An option MPV
|
|
879
|
+
# does not have cannot start on the fifth attempt either,
|
|
880
|
+
# and spending the whole retry budget on it produces the
|
|
881
|
+
# least useful error this library has ever raised: a
|
|
882
|
+
# timeout that names nothing. The probe costs one ~20ms
|
|
883
|
+
# MPV run and only ever happens on a start that has
|
|
884
|
+
# already failed.
|
|
885
|
+
argv = getattr(error, "argv", None)
|
|
886
|
+
if diagnose_start_failures and argv and not diagnosed:
|
|
887
|
+
diagnosed = True
|
|
888
|
+
bad_option, diagnosis = _diagnose_start_failure(argv)
|
|
889
|
+
# Keep what MPV said even when it does not name an
|
|
890
|
+
# option. Refusing to start over a missing DLL or an
|
|
891
|
+
# unusable VO is the same predicament from the user's
|
|
892
|
+
# side -- something they changed, and nothing to go
|
|
893
|
+
# on -- and on Windows there is no console for them to
|
|
894
|
+
# have seen it in. It rides out on .log_output below.
|
|
895
|
+
log_output = diagnosis
|
|
896
|
+
if bad_option is not None:
|
|
897
|
+
raise MPVProcessError(
|
|
898
|
+
"MPV rejected the option --{0}.".format(
|
|
899
|
+
bad_option),
|
|
900
|
+
returncode=getattr(error, "returncode", None),
|
|
901
|
+
bad_option=bad_option,
|
|
902
|
+
log_output=log_output,
|
|
903
|
+
retryable=False,
|
|
904
|
+
argv=argv)
|
|
905
|
+
|
|
449
906
|
time.sleep(start_retry_delay_ms / 1000)
|
|
450
907
|
continue
|
|
451
908
|
else:
|
|
452
|
-
|
|
909
|
+
# The message is unchanged on purpose. Downstream projects
|
|
910
|
+
# that will never be updated may be matching on it, and this
|
|
911
|
+
# is still the same situation it has always described.
|
|
912
|
+
raise MPVProcessError(
|
|
913
|
+
"MPV process retry limit reached.",
|
|
914
|
+
returncode=getattr(last_error, "returncode", None),
|
|
915
|
+
log_output=log_output,
|
|
916
|
+
argv=getattr(last_error, "argv", None))
|
|
453
917
|
|
|
454
918
|
self.mpv_inter = MPVInter(ipc_socket, self._callback, self._quit_callback)
|
|
455
919
|
self.properties = set(x.replace("-", "_") for x in self.command("get_property", "property-list"))
|
|
@@ -637,6 +1101,14 @@ class MPV:
|
|
|
637
1101
|
def terminate(self, join=True):
|
|
638
1102
|
"""Terminate the connection to MPV and process (if *start_mpv* is used)."""
|
|
639
1103
|
if self.mpv_process:
|
|
1104
|
+
# Unconditionally, regardless of *join*. That flag only ever
|
|
1105
|
+
# reaches our own threads -- and imperfectly: the transports join
|
|
1106
|
+
# properly, while EventHandler.stop passes it as Thread.join's
|
|
1107
|
+
# *timeout*, so join=True waits at most a second there and does
|
|
1108
|
+
# not confirm the thread exited. Either way it says nothing about
|
|
1109
|
+
# MPV, and _quit_callback passes False only because a thread
|
|
1110
|
+
# cannot join itself. Letting it skip this wait would hand the
|
|
1111
|
+
# orphaned-MPV bug back to anyone who passed it.
|
|
640
1112
|
self.mpv_process.stop()
|
|
641
1113
|
if self.mpv_inter:
|
|
642
1114
|
self.mpv_inter.stop(join)
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
python_mpv_jsonipc.py,sha256=-UvwnvcI2x1JyVue-4xGLipkzzGFlCrT6PqGe_FVAkY,24302
|
|
2
|
-
python_mpv_jsonipc-1.2.3.dist-info/licenses/LICENSE.md,sha256=7GQBUOA3dNG1Jk-yaPOCncNnRVURCRMev23QpZ1ew-s,10495
|
|
3
|
-
python_mpv_jsonipc-1.2.3.dist-info/METADATA,sha256=YF-8QYjJ5EtyYcPfaVC2_8dywv_pkKujnsgl2-qAU1g,2277
|
|
4
|
-
python_mpv_jsonipc-1.2.3.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
5
|
-
python_mpv_jsonipc-1.2.3.dist-info/top_level.txt,sha256=nGk4yjpVHKmIrL39XdGD2PhCeWF0oMjwey0nsNOd2TM,19
|
|
6
|
-
python_mpv_jsonipc-1.2.3.dist-info/RECORD,,
|
{python_mpv_jsonipc-1.2.3.dist-info → python_mpv_jsonipc-1.4.0.dist-info}/licenses/LICENSE.md
RENAMED
|
File without changes
|
|
File without changes
|