Add iwalton3's Python MPV JSONIPC library (Apache 2.0)
This commit is contained in:
parent
da6ec880bb
commit
7887c2bf14
195
syncplay/players/python_mpv_jsonipc/LICENSE.md
Normal file
195
syncplay/players/python_mpv_jsonipc/LICENSE.md
Normal file
@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
56
syncplay/players/python_mpv_jsonipc/README.md
Normal file
56
syncplay/players/python_mpv_jsonipc/README.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Python MPV JSONIPC
|
||||
|
||||
This implements an interface similar to `python-mpv`, but it uses the JSON IPC protocol instead of the C API. This means
|
||||
you can control external instances of MPV including players like SMPlayer, and it can use MPV players that are prebuilt
|
||||
instead of needing `libmpv1`. It may also be more resistant to crashes such as Segmentation Faults, but since it isn't
|
||||
directly communicating with MPV via the C API the performance will be worse.
|
||||
|
||||
Please note that this only implements the subset of `python-mpv` that is used by `plex-mpv-shim` and
|
||||
`jellyfin-mpv-shim`. Other functionality has not been implemented.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
sudo pip3 install python-mpv-jsonipc
|
||||
```
|
||||
|
||||
## Basic usage
|
||||
|
||||
Create an instance of MPV. You can use an already running MPV or have the library start a
|
||||
managed copy of MPV. Command arguments can be specified when initializing MPV if you are
|
||||
starting a managed copy of MPV.
|
||||
|
||||
Please also see the [API Documentation](https://github.com/iwalton3/python-mpv-jsonipc/blob/master/docs.md).
|
||||
|
||||
```python
|
||||
from python_mpv_jsonipc import MPV
|
||||
|
||||
# Uses MPV that is in the PATH.
|
||||
mpv = MPV()
|
||||
|
||||
# Use MPV that is running and connected to /tmp/mpv-socket.
|
||||
mpv = MPV(start_mpv=False, ipc_socket="/tmp/mpv-socket")
|
||||
|
||||
# Uses MPV that is found at /path/to/mpv.
|
||||
mpv = MPV(mpv_location="/path/to/mpv")
|
||||
|
||||
# After you have an MPV, you can read and set (if applicable) properties.
|
||||
mpv.volume # 100.0 by default
|
||||
mpv.volume = 20
|
||||
|
||||
# You can also send commands.
|
||||
mpv.command_name(arg1, arg2)
|
||||
|
||||
# Bind to key press events with a decorator
|
||||
@mpv.on_key_press("space")
|
||||
def space_handler():
|
||||
pass
|
||||
|
||||
# You can also observe and wait for properties.
|
||||
@mpv.property_observer("eof-reached")
|
||||
def handle_eof(name, value):
|
||||
pass
|
||||
|
||||
# Or simply wait for the value to change once.
|
||||
mpv.wait_for_property("duration")
|
||||
```
|
||||
460
syncplay/players/python_mpv_jsonipc/docs.md
Normal file
460
syncplay/players/python_mpv_jsonipc/docs.md
Normal file
@ -0,0 +1,460 @@
|
||||
<a name=".python_mpv_jsonipc"></a>
|
||||
|
||||
## python\_mpv\_jsonipc
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPVError"></a>
|
||||
|
||||
### MPVError
|
||||
|
||||
``` python
|
||||
class MPVError(Exception):
|
||||
| MPVError(**args, ****kwargs)
|
||||
```
|
||||
|
||||
An error originating from MPV or due to a problem with MPV.
|
||||
|
||||
<a name=".python_mpv_jsonipc.WindowsSocket"></a>
|
||||
|
||||
### WindowsSocket
|
||||
|
||||
``` python
|
||||
class WindowsSocket(threading.Thread)
|
||||
```
|
||||
|
||||
Wraps a Windows named pipe in a high-level interface. (Internal)
|
||||
|
||||
Data is automatically encoded and decoded as JSON. The callback
|
||||
function will be called for each inbound message.
|
||||
|
||||
<a name=".python_mpv_jsonipc.WindowsSocket.__init__"></a>
|
||||
|
||||
#### \_\_init\_\_
|
||||
|
||||
``` python
|
||||
| __init__(ipc_socket, callback=None)
|
||||
```
|
||||
|
||||
Create the wrapper.
|
||||
|
||||
**ipc\_socket** is the pipe name. (Not including \\\\.\\pipe\\)
|
||||
**callback(json\_data)** is the function for recieving events.
|
||||
|
||||
<a name=".python_mpv_jsonipc.WindowsSocket.stop"></a>
|
||||
|
||||
#### stop
|
||||
|
||||
``` python
|
||||
| stop()
|
||||
```
|
||||
|
||||
Terminate the thread.
|
||||
|
||||
<a name=".python_mpv_jsonipc.WindowsSocket.send"></a>
|
||||
|
||||
#### send
|
||||
|
||||
``` python
|
||||
| send(data)
|
||||
```
|
||||
|
||||
Send **data** to the pipe, encoded as JSON.
|
||||
|
||||
<a name=".python_mpv_jsonipc.WindowsSocket.run"></a>
|
||||
|
||||
#### run
|
||||
|
||||
``` python
|
||||
| run()
|
||||
```
|
||||
|
||||
Process pipe events. Do not run this directly. Use **start**.
|
||||
|
||||
<a name=".python_mpv_jsonipc.UnixSocket"></a>
|
||||
|
||||
### UnixSocket
|
||||
|
||||
``` python
|
||||
class UnixSocket(threading.Thread)
|
||||
```
|
||||
|
||||
Wraps a Unix/Linux socket in a high-level interface. (Internal)
|
||||
|
||||
Data is automatically encoded and decoded as JSON. The callback
|
||||
function will be called for each inbound message.
|
||||
|
||||
<a name=".python_mpv_jsonipc.UnixSocket.__init__"></a>
|
||||
|
||||
#### \_\_init\_\_
|
||||
|
||||
``` python
|
||||
| __init__(ipc_socket, callback=None)
|
||||
```
|
||||
|
||||
Create the wrapper.
|
||||
|
||||
**ipc\_socket** is the path to the socket.
|
||||
**callback(json\_data)** is the function for recieving events.
|
||||
|
||||
<a name=".python_mpv_jsonipc.UnixSocket.stop"></a>
|
||||
|
||||
#### stop
|
||||
|
||||
``` python
|
||||
| stop()
|
||||
```
|
||||
|
||||
Terminate the thread.
|
||||
|
||||
<a name=".python_mpv_jsonipc.UnixSocket.send"></a>
|
||||
|
||||
#### send
|
||||
|
||||
``` python
|
||||
| send(data)
|
||||
```
|
||||
|
||||
Send **data** to the socket, encoded as JSON.
|
||||
|
||||
<a name=".python_mpv_jsonipc.UnixSocket.run"></a>
|
||||
|
||||
#### run
|
||||
|
||||
``` python
|
||||
| run()
|
||||
```
|
||||
|
||||
Process socket events. Do not run this directly. Use **start**.
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPVProcess"></a>
|
||||
|
||||
### MPVProcess
|
||||
|
||||
``` python
|
||||
class MPVProcess()
|
||||
```
|
||||
|
||||
Manages an MPV process, ensuring the socket or pipe is available. (Internal)
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPVProcess.__init__"></a>
|
||||
|
||||
#### \_\_init\_\_
|
||||
|
||||
``` python
|
||||
| __init__(ipc_socket, mpv_location=None, ****kwargs)
|
||||
```
|
||||
|
||||
Create and start the MPV process. Will block until socket/pipe is available.
|
||||
|
||||
**ipc\_socket** is the path to the Unix/Linux socket or name of the Windows pipe.
|
||||
**mpv\_location** is the path to mpv. If left unset it tries the one in the PATH.
|
||||
|
||||
All other arguments are forwarded to MPV as command-line arguments.
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPVProcess.stop"></a>
|
||||
|
||||
#### stop
|
||||
|
||||
``` python
|
||||
| stop()
|
||||
```
|
||||
|
||||
Terminate the process.
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPVInter"></a>
|
||||
|
||||
### MPVInter
|
||||
|
||||
``` python
|
||||
class MPVInter()
|
||||
```
|
||||
|
||||
Low-level interface to MPV. Does NOT manage an mpv process. (Internal)
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPVInter.__init__"></a>
|
||||
|
||||
#### \_\_init\_\_
|
||||
|
||||
``` python
|
||||
| __init__(ipc_socket, callback=None)
|
||||
```
|
||||
|
||||
Create the wrapper.
|
||||
|
||||
**ipc\_socket** is the path to the Unix/Linux socket or name of the Windows pipe.
|
||||
**callback(event\_name, data)** is the function for recieving events.
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPVInter.stop"></a>
|
||||
|
||||
#### stop
|
||||
|
||||
``` python
|
||||
| stop()
|
||||
```
|
||||
|
||||
Terminate the underlying connection.
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPVInter.event_callback"></a>
|
||||
|
||||
#### event\_callback
|
||||
|
||||
``` python
|
||||
| event_callback(data)
|
||||
```
|
||||
|
||||
Internal callback for recieving events from MPV.
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPVInter.command"></a>
|
||||
|
||||
#### command
|
||||
|
||||
``` python
|
||||
| command(command, **args)
|
||||
```
|
||||
|
||||
Issue a command to MPV. Will block until completed or timeout is reached.
|
||||
|
||||
**command** is the name of the MPV command
|
||||
|
||||
All further arguments are forwarded to the MPV command.
|
||||
Throws TimeoutError if timeout of 120 seconds is reached.
|
||||
|
||||
<a name=".python_mpv_jsonipc.EventHandler"></a>
|
||||
|
||||
### EventHandler
|
||||
|
||||
``` python
|
||||
class EventHandler(threading.Thread)
|
||||
```
|
||||
|
||||
Event handling thread. (Internal)
|
||||
|
||||
<a name=".python_mpv_jsonipc.EventHandler.__init__"></a>
|
||||
|
||||
#### \_\_init\_\_
|
||||
|
||||
``` python
|
||||
| __init__()
|
||||
```
|
||||
|
||||
Create an instance of the thread.
|
||||
|
||||
<a name=".python_mpv_jsonipc.EventHandler.put_task"></a>
|
||||
|
||||
#### put\_task
|
||||
|
||||
``` python
|
||||
| put_task(func, **args)
|
||||
```
|
||||
|
||||
Put a new task to the thread.
|
||||
|
||||
**func** is the function to call
|
||||
|
||||
All further arguments are forwarded to **func**.
|
||||
|
||||
<a name=".python_mpv_jsonipc.EventHandler.stop"></a>
|
||||
|
||||
#### stop
|
||||
|
||||
``` python
|
||||
| stop()
|
||||
```
|
||||
|
||||
Terminate the thread.
|
||||
|
||||
<a name=".python_mpv_jsonipc.EventHandler.run"></a>
|
||||
|
||||
#### run
|
||||
|
||||
``` python
|
||||
| run()
|
||||
```
|
||||
|
||||
Process socket events. Do not run this directly. Use **start**.
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPV"></a>
|
||||
|
||||
### MPV
|
||||
|
||||
``` python
|
||||
class MPV()
|
||||
```
|
||||
|
||||
The main MPV interface class. Use this to control MPV.
|
||||
|
||||
This will expose all mpv commands as callable methods and all properties.
|
||||
You can set properties and call the commands directly.
|
||||
|
||||
Please note that if you are using a really old MPV version, a fallback command
|
||||
list is used. Not all commands may actually work when this fallback is used.
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPV.__init__"></a>
|
||||
|
||||
#### \_\_init\_\_
|
||||
|
||||
``` python
|
||||
| __init__(start_mpv=True, ipc_socket=None, mpv_location=None, log_handler=None, loglevel=None, ****kwargs)
|
||||
```
|
||||
|
||||
Create the interface to MPV and process instance.
|
||||
|
||||
**start\_mpv** will start an MPV process if true. (Default: True)
|
||||
**ipc\_socket** is the path to the Unix/Linux socket or name of Windows pipe. (Default: Random Temp File)
|
||||
**mpv\_location** is the location of MPV for **start\_mpv**. (Default: Use MPV in PATH)
|
||||
**log\_handler(level, prefix, text)** is an optional handler for log events. (Default: Disabled)
|
||||
**loglevel** is the level for log messages. Levels are fatal, error, warn, info, v, debug, trace. (Default: Disabled)
|
||||
|
||||
All other arguments are forwarded to MPV as command-line arguments if **start\_mpv** is used.
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPV.bind_event"></a>
|
||||
|
||||
#### bind\_event
|
||||
|
||||
``` python
|
||||
| bind_event(name, callback)
|
||||
```
|
||||
|
||||
Bind a callback to an MPV event.
|
||||
|
||||
**name** is the MPV event name.
|
||||
**callback(event\_data)** is the function to call.
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPV.on_event"></a>
|
||||
|
||||
#### on\_event
|
||||
|
||||
``` python
|
||||
| on_event(name)
|
||||
```
|
||||
|
||||
Decorator to bind a callback to an MPV event.
|
||||
|
||||
@on\_event(name)
|
||||
def my\_callback(event\_data):
|
||||
pass
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPV.event_callback"></a>
|
||||
|
||||
#### event\_callback
|
||||
|
||||
``` python
|
||||
| event_callback(name)
|
||||
```
|
||||
|
||||
An alias for on\_event to maintain compatibility with python-mpv.
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPV.on_key_press"></a>
|
||||
|
||||
#### on\_key\_press
|
||||
|
||||
``` python
|
||||
| on_key_press(name)
|
||||
```
|
||||
|
||||
Decorator to bind a callback to an MPV keypress event.
|
||||
|
||||
@on\_key\_press(key\_name)
|
||||
def my\_callback():
|
||||
pass
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPV.bind_key_press"></a>
|
||||
|
||||
#### bind\_key\_press
|
||||
|
||||
``` python
|
||||
| bind_key_press(name, callback)
|
||||
```
|
||||
|
||||
Bind a callback to an MPV keypress event.
|
||||
|
||||
**name** is the key symbol.
|
||||
**callback()** is the function to call.
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPV.bind_property_observer"></a>
|
||||
|
||||
#### bind\_property\_observer
|
||||
|
||||
``` python
|
||||
| bind_property_observer(name, callback)
|
||||
```
|
||||
|
||||
Bind a callback to an MPV property change.
|
||||
|
||||
**name** is the property name.
|
||||
**callback(name, data)** is the function to call.
|
||||
|
||||
Returns a unique observer ID needed to destroy the observer.
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPV.unbind_property_observer"></a>
|
||||
|
||||
#### unbind\_property\_observer
|
||||
|
||||
``` python
|
||||
| unbind_property_observer(observer_id)
|
||||
```
|
||||
|
||||
Remove callback to an MPV property change.
|
||||
|
||||
**observer\_id** is the id returned by bind\_property\_observer.
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPV.property_observer"></a>
|
||||
|
||||
#### property\_observer
|
||||
|
||||
``` python
|
||||
| property_observer(name)
|
||||
```
|
||||
|
||||
Decorator to bind a callback to an MPV property change.
|
||||
|
||||
@on\_key\_press(property\_name)
|
||||
def my\_callback(name, data):
|
||||
pass
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPV.wait_for_property"></a>
|
||||
|
||||
#### wait\_for\_property
|
||||
|
||||
``` python
|
||||
| wait_for_property(name)
|
||||
```
|
||||
|
||||
Waits for the value of a property to change.
|
||||
|
||||
**name** is the name of the property.
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPV.play"></a>
|
||||
|
||||
#### play
|
||||
|
||||
``` python
|
||||
| play(url)
|
||||
```
|
||||
|
||||
Play the specified URL. An alias to loadfile().
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPV.terminate"></a>
|
||||
|
||||
#### terminate
|
||||
|
||||
``` python
|
||||
| terminate()
|
||||
```
|
||||
|
||||
Terminate the connection to MPV and process (if started by this module).
|
||||
|
||||
<a name=".python_mpv_jsonipc.MPV.command"></a>
|
||||
|
||||
#### command
|
||||
|
||||
``` python
|
||||
| command(command, **args)
|
||||
```
|
||||
|
||||
Send a command to MPV. All commands are bound to the class by default,
|
||||
except JSON IPC specific commands. This may also be useful to retain
|
||||
compatibility with python-mpv, as it does not bind all of the commands.
|
||||
|
||||
**command** is the command name.
|
||||
|
||||
All further arguments are forwarded to the MPV command.
|
||||
607
syncplay/players/python_mpv_jsonipc/python_mpv_jsonipc.py
Normal file
607
syncplay/players/python_mpv_jsonipc/python_mpv_jsonipc.py
Normal file
@ -0,0 +1,607 @@
|
||||
import threading
|
||||
import socket
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import random
|
||||
import queue
|
||||
import logging
|
||||
|
||||
log = logging.getLogger('mpv-jsonipc')
|
||||
|
||||
if os.name == "nt":
|
||||
import _winapi
|
||||
from multiprocessing.connection import PipeConnection
|
||||
|
||||
TIMEOUT = 120
|
||||
|
||||
# Older MPV versions do not allow us to dynamically retrieve the command list.
|
||||
FALLBACK_COMMAND_LIST = [
|
||||
'ignore', 'seek', 'revert-seek', 'quit', 'quit-watch-later', 'stop', 'frame-step', 'frame-back-step',
|
||||
'playlist-next', 'playlist-prev', 'playlist-shuffle', 'playlist-unshuffle', 'sub-step', 'sub-seek',
|
||||
'print-text', 'show-text', 'expand-text', 'expand-path', 'show-progress', 'sub-add', 'audio-add',
|
||||
'video-add', 'sub-remove', 'audio-remove', 'video-remove', 'sub-reload', 'audio-reload', 'video-reload',
|
||||
'rescan-external-files', 'screenshot', 'screenshot-to-file', 'screenshot-raw', 'loadfile', 'loadlist',
|
||||
'playlist-clear', 'playlist-remove', 'playlist-move', 'run', 'subprocess', 'set', 'change-list', 'add',
|
||||
'cycle', 'multiply', 'cycle-values', 'enable-section', 'disable-section', 'define-section', 'ab-loop',
|
||||
'drop-buffers', 'af', 'vf', 'af-command', 'vf-command', 'ao-reload', 'script-binding', 'script-message',
|
||||
'script-message-to', 'overlay-add', 'overlay-remove', 'osd-overlay', 'write-watch-later-config',
|
||||
'hook-add', 'hook-ack', 'mouse', 'keybind', 'keypress', 'keydown', 'keyup', 'apply-profile',
|
||||
'load-script', 'dump-cache', 'ab-loop-dump-cache', 'ab-loop-align-cache']
|
||||
|
||||
class MPVError(Exception):
|
||||
"""An error originating from MPV or due to a problem with MPV."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MPVError, self).__init__(*args, **kwargs)
|
||||
|
||||
class WindowsSocket(threading.Thread):
|
||||
"""
|
||||
Wraps a Windows named pipe in a high-level interface. (Internal)
|
||||
|
||||
Data is automatically encoded and decoded as JSON. The callback
|
||||
function will be called for each inbound message.
|
||||
"""
|
||||
def __init__(self, ipc_socket, callback=None):
|
||||
"""Create the wrapper.
|
||||
|
||||
*ipc_socket* is the pipe name. (Not including \\\\.\\pipe\\)
|
||||
*callback(json_data)* is the function for recieving events.
|
||||
"""
|
||||
ipc_socket = "\\\\.\\pipe\\" + ipc_socket
|
||||
self.callback = callback
|
||||
|
||||
access = _winapi.GENERIC_READ | _winapi.GENERIC_WRITE
|
||||
limit = 5 # Connection may fail at first. Try 5 times.
|
||||
for _ in range(limit):
|
||||
try:
|
||||
pipe_handle = _winapi.CreateFile(
|
||||
ipc_socket, access, 0, _winapi.NULL, _winapi.OPEN_EXISTING,
|
||||
_winapi.FILE_FLAG_OVERLAPPED, _winapi.NULL
|
||||
)
|
||||
break
|
||||
except OSError:
|
||||
time.sleep(1)
|
||||
else:
|
||||
raise MPVError("Cannot connect to pipe.")
|
||||
self.socket = PipeConnection(pipe_handle)
|
||||
|
||||
if self.callback is None:
|
||||
self.callback = lambda data: None
|
||||
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
def stop(self):
|
||||
"""Terminate the thread."""
|
||||
if self.socket is not None:
|
||||
self.socket.close()
|
||||
self.join()
|
||||
|
||||
def send(self, data):
|
||||
"""Send *data* to the pipe, encoded as JSON."""
|
||||
self.socket.send_bytes(json.dumps(data).encode('utf-8') + b'\n')
|
||||
|
||||
def run(self):
|
||||
"""Process pipe events. Do not run this directly. Use *start*."""
|
||||
data = b''
|
||||
try:
|
||||
while True:
|
||||
current_data = self.socket.recv_bytes(2048)
|
||||
if current_data == b'':
|
||||
break
|
||||
|
||||
data += current_data
|
||||
if data[-1] != 10:
|
||||
continue
|
||||
|
||||
data = data.decode('utf-8', 'ignore').encode('utf-8')
|
||||
for item in data.split(b'\n'):
|
||||
if item == b'':
|
||||
continue
|
||||
json_data = json.loads(item)
|
||||
self.callback(json_data)
|
||||
data = b''
|
||||
except EOFError:
|
||||
pass
|
||||
|
||||
class UnixSocket(threading.Thread):
|
||||
"""
|
||||
Wraps a Unix/Linux socket in a high-level interface. (Internal)
|
||||
|
||||
Data is automatically encoded and decoded as JSON. The callback
|
||||
function will be called for each inbound message.
|
||||
"""
|
||||
def __init__(self, ipc_socket, callback=None):
|
||||
"""Create the wrapper.
|
||||
|
||||
*ipc_socket* is the path to the socket.
|
||||
*callback(json_data)* is the function for recieving events.
|
||||
"""
|
||||
self.ipc_socket = ipc_socket
|
||||
self.callback = callback
|
||||
self.socket = socket.socket(socket.AF_UNIX)
|
||||
self.socket.connect(self.ipc_socket)
|
||||
|
||||
if self.callback is None:
|
||||
self.callback = lambda data: None
|
||||
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
def stop(self):
|
||||
"""Terminate the thread."""
|
||||
if self.socket is not None:
|
||||
self.socket.shutdown(socket.SHUT_WR)
|
||||
self.socket.close()
|
||||
self.join()
|
||||
|
||||
def send(self, data):
|
||||
"""Send *data* to the socket, encoded as JSON."""
|
||||
self.socket.send(json.dumps(data).encode('utf-8') + b'\n')
|
||||
|
||||
def run(self):
|
||||
"""Process socket events. Do not run this directly. Use *start*."""
|
||||
data = b''
|
||||
while True:
|
||||
current_data = self.socket.recv(1024)
|
||||
if current_data == b'':
|
||||
break
|
||||
|
||||
data += current_data
|
||||
if data[-1] != 10:
|
||||
continue
|
||||
|
||||
data = data.decode('utf-8', 'ignore').encode('utf-8')
|
||||
for item in data.split(b'\n'):
|
||||
if item == b'':
|
||||
continue
|
||||
json_data = json.loads(item)
|
||||
self.callback(json_data)
|
||||
data = b''
|
||||
|
||||
class MPVProcess:
|
||||
"""
|
||||
Manages an MPV process, ensuring the socket or pipe is available. (Internal)
|
||||
"""
|
||||
def __init__(self, ipc_socket, mpv_location=None, **kwargs):
|
||||
"""
|
||||
Create and start the MPV process. Will block until socket/pipe is available.
|
||||
|
||||
*ipc_socket* is the path to the Unix/Linux socket or name of the Windows pipe.
|
||||
*mpv_location* is the path to mpv. If left unset it tries the one in the PATH.
|
||||
|
||||
All other arguments are forwarded to MPV as command-line arguments.
|
||||
"""
|
||||
if mpv_location is None:
|
||||
if os.name == 'nt':
|
||||
mpv_location = "mpv.exe"
|
||||
else:
|
||||
mpv_location = "mpv"
|
||||
|
||||
log.debug("Staring MPV from {0}.".format(mpv_location))
|
||||
ipc_socket_name = ipc_socket
|
||||
if os.name == 'nt':
|
||||
ipc_socket = "\\\\.\\pipe\\" + ipc_socket
|
||||
|
||||
if os.name != 'nt' and os.path.exists(ipc_socket):
|
||||
os.remove(ipc_socket)
|
||||
|
||||
log.debug("Using IPC socket {0} for MPV.".format(ipc_socket))
|
||||
self.ipc_socket = ipc_socket
|
||||
args = [mpv_location]
|
||||
self._set_default(kwargs, "idle", True)
|
||||
self._set_default(kwargs, "input_ipc_server", ipc_socket_name)
|
||||
self._set_default(kwargs, "input_terminal", False)
|
||||
self._set_default(kwargs, "terminal", False)
|
||||
args.extend("--{0}={1}".format(v[0].replace("_", "-"), self._mpv_fmt(v[1]))
|
||||
for v in kwargs.items())
|
||||
self.process = subprocess.Popen(args)
|
||||
ipc_exists = False
|
||||
for _ in range(100): # Give MPV 10 seconds to start.
|
||||
time.sleep(0.1)
|
||||
self.process.poll()
|
||||
if os.path.exists(ipc_socket):
|
||||
ipc_exists = True
|
||||
log.debug("Found MPV socket.")
|
||||
break
|
||||
if self.process.returncode is not None:
|
||||
log.error("MPV failed with returncode {0}.".format(self.process.returncode))
|
||||
break
|
||||
else:
|
||||
self.process.terminate()
|
||||
raise MPVError("MPV start timed out.")
|
||||
|
||||
if not ipc_exists or self.process.returncode is not None:
|
||||
self.process.terminate()
|
||||
raise MPVError("MPV not started.")
|
||||
|
||||
def _set_default(self, prop_dict, key, value):
|
||||
if key not in prop_dict:
|
||||
prop_dict[key] = value
|
||||
|
||||
def _mpv_fmt(self, data):
|
||||
if data == True:
|
||||
return "yes"
|
||||
elif data == False:
|
||||
return "no"
|
||||
else:
|
||||
return data
|
||||
|
||||
def stop(self):
|
||||
"""Terminate the process."""
|
||||
self.process.terminate()
|
||||
if os.name != 'nt' and os.path.exists(self.ipc_socket):
|
||||
os.remove(self.ipc_socket)
|
||||
|
||||
class MPVInter:
|
||||
"""
|
||||
Low-level interface to MPV. Does NOT manage an mpv process. (Internal)
|
||||
"""
|
||||
def __init__(self, ipc_socket, callback=None):
|
||||
"""Create the wrapper.
|
||||
|
||||
*ipc_socket* is the path to the Unix/Linux socket or name of the Windows pipe.
|
||||
*callback(event_name, data)* is the function for recieving events.
|
||||
"""
|
||||
Socket = UnixSocket
|
||||
if os.name == 'nt':
|
||||
Socket = WindowsSocket
|
||||
|
||||
self.callback = callback
|
||||
if self.callback is None:
|
||||
self.callback = lambda event, data: None
|
||||
|
||||
self.socket = Socket(ipc_socket, self.event_callback)
|
||||
self.socket.start()
|
||||
self.command_id = 1
|
||||
self.rid_lock = threading.Lock()
|
||||
self.socket_lock = threading.Lock()
|
||||
self.cid_result = {}
|
||||
self.cid_wait = {}
|
||||
|
||||
def stop(self):
|
||||
"""Terminate the underlying connection."""
|
||||
self.socket.stop()
|
||||
|
||||
def event_callback(self, data):
|
||||
"""Internal callback for recieving events from MPV."""
|
||||
if "request_id" in data:
|
||||
self.cid_result[data["request_id"]] = data
|
||||
self.cid_wait[data["request_id"]].set()
|
||||
elif "event" in data:
|
||||
self.callback(data["event"], data)
|
||||
|
||||
def command(self, command, *args):
|
||||
"""
|
||||
Issue a command to MPV. Will block until completed or timeout is reached.
|
||||
|
||||
*command* is the name of the MPV command
|
||||
|
||||
All further arguments are forwarded to the MPV command.
|
||||
Throws TimeoutError if timeout of 120 seconds is reached.
|
||||
"""
|
||||
self.rid_lock.acquire()
|
||||
command_id = self.command_id
|
||||
self.command_id += 1
|
||||
self.rid_lock.release()
|
||||
|
||||
event = threading.Event()
|
||||
self.cid_wait[command_id] = event
|
||||
|
||||
command_list = [command]
|
||||
command_list.extend(args)
|
||||
try:
|
||||
self.socket_lock.acquire()
|
||||
self.socket.send({"command":command_list, "request_id": command_id})
|
||||
finally:
|
||||
self.socket_lock.release()
|
||||
|
||||
has_event = event.wait(timeout=TIMEOUT)
|
||||
if has_event:
|
||||
data = self.cid_result[command_id]
|
||||
del self.cid_result[command_id]
|
||||
del self.cid_wait[command_id]
|
||||
if data["error"] != "success":
|
||||
if data["error"] == "property unavailable":
|
||||
return None
|
||||
raise MPVError(data["error"])
|
||||
else:
|
||||
return data.get("data")
|
||||
else:
|
||||
raise TimeoutError("No response from MPV.")
|
||||
|
||||
class EventHandler(threading.Thread):
|
||||
"""Event handling thread. (Internal)"""
|
||||
def __init__(self):
|
||||
"""Create an instance of the thread."""
|
||||
self.queue = queue.Queue()
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
def put_task(self, func, *args):
|
||||
"""
|
||||
Put a new task to the thread.
|
||||
|
||||
*func* is the function to call
|
||||
|
||||
All further arguments are forwarded to *func*.
|
||||
"""
|
||||
self.queue.put((func, args))
|
||||
|
||||
def stop(self):
|
||||
"""Terminate the thread."""
|
||||
self.queue.put("quit")
|
||||
self.join()
|
||||
|
||||
def run(self):
|
||||
"""Process socket events. Do not run this directly. Use *start*."""
|
||||
while True:
|
||||
event = self.queue.get()
|
||||
if event == "quit":
|
||||
break
|
||||
try:
|
||||
event[0](*event[1])
|
||||
except Exception:
|
||||
log.error("EventHandler caught exception from {0}.".format(event), exc_info=1)
|
||||
|
||||
class MPV:
|
||||
"""
|
||||
The main MPV interface class. Use this to control MPV.
|
||||
|
||||
This will expose all mpv commands as callable methods and all properties.
|
||||
You can set properties and call the commands directly.
|
||||
|
||||
Please note that if you are using a really old MPV version, a fallback command
|
||||
list is used. Not all commands may actually work when this fallback is used.
|
||||
"""
|
||||
def __init__(self, start_mpv=True, ipc_socket=None, mpv_location=None, log_handler=None, loglevel=None, **kwargs):
|
||||
"""
|
||||
Create the interface to MPV and process instance.
|
||||
|
||||
*start_mpv* will start an MPV process if true. (Default: True)
|
||||
*ipc_socket* is the path to the Unix/Linux socket or name of Windows pipe. (Default: Random Temp File)
|
||||
*mpv_location* is the location of MPV for *start_mpv*. (Default: Use MPV in PATH)
|
||||
*log_handler(level, prefix, text)* is an optional handler for log events. (Default: Disabled)
|
||||
*loglevel* is the level for log messages. Levels are fatal, error, warn, info, v, debug, trace. (Default: Disabled)
|
||||
|
||||
All other arguments are forwarded to MPV as command-line arguments if *start_mpv* is used.
|
||||
"""
|
||||
self.properties = {}
|
||||
self.event_bindings = {}
|
||||
self.key_bindings = {}
|
||||
self.property_bindings = {}
|
||||
self.mpv_process = None
|
||||
self.mpv_inter = None
|
||||
self.event_handler = EventHandler()
|
||||
self.event_handler.start()
|
||||
if ipc_socket is None:
|
||||
rand_file = "mpv{0}".format(random.randint(0, 2**48))
|
||||
if os.name == "nt":
|
||||
ipc_socket = rand_file
|
||||
else:
|
||||
ipc_socket = "/tmp/{0}".format(rand_file)
|
||||
|
||||
if start_mpv:
|
||||
# Attempt to start MPV 3 times.
|
||||
for i in range(3):
|
||||
try:
|
||||
self.mpv_process = MPVProcess(ipc_socket, mpv_location, **kwargs)
|
||||
break
|
||||
except MPVError:
|
||||
log.warning("MPV start failed.", exc_info=1)
|
||||
continue
|
||||
else:
|
||||
raise MPVError("MPV process retry limit reached.")
|
||||
|
||||
self.mpv_inter = MPVInter(ipc_socket, self._callback)
|
||||
self.properties = set(x.replace("-", "_") for x in self.command("get_property", "property-list"))
|
||||
try:
|
||||
command_list = [x["name"] for x in self.command("get_property", "command-list")]
|
||||
except MPVError:
|
||||
log.warning("Using fallback command list.")
|
||||
command_list = FALLBACK_COMMAND_LIST
|
||||
for command in command_list:
|
||||
object.__setattr__(self, command.replace("-", "_"), self._get_wrapper(command))
|
||||
|
||||
self._dir = list(self.properties)
|
||||
self._dir.extend(object.__dir__(self))
|
||||
|
||||
self.observer_id = 1
|
||||
self.observer_lock = threading.Lock()
|
||||
self.keybind_id = 1
|
||||
self.keybind_lock = threading.Lock()
|
||||
|
||||
if log_handler is not None and loglevel is not None:
|
||||
self.command("request_log_messages", loglevel)
|
||||
@self.on_event("log-message")
|
||||
def log_handler_event(data):
|
||||
self.event_handler.put_task(log_handler, data["level"], data["prefix"], data["text"].strip())
|
||||
|
||||
@self.on_event("property-change")
|
||||
def event_handler(data):
|
||||
if data.get("id") in self.property_bindings:
|
||||
self.event_handler.put_task(self.property_bindings[data["id"]], data["name"], data.get("data"))
|
||||
|
||||
@self.on_event("client-message")
|
||||
def client_message_handler(data):
|
||||
args = data["args"]
|
||||
if len(args) == 2 and args[0] == "custom-bind":
|
||||
self.event_handler.put_task(self.key_bindings[args[1]])
|
||||
|
||||
def bind_event(self, name, callback):
|
||||
"""
|
||||
Bind a callback to an MPV event.
|
||||
|
||||
*name* is the MPV event name.
|
||||
*callback(event_data)* is the function to call.
|
||||
"""
|
||||
if name not in self.event_bindings:
|
||||
self.event_bindings[name] = set()
|
||||
self.event_bindings[name].add(callback)
|
||||
|
||||
def on_event(self, name):
|
||||
"""
|
||||
Decorator to bind a callback to an MPV event.
|
||||
|
||||
@on_event(name)
|
||||
def my_callback(event_data):
|
||||
pass
|
||||
"""
|
||||
def wrapper(func):
|
||||
self.bind_event(name, func)
|
||||
return func
|
||||
return wrapper
|
||||
|
||||
# Added for compatibility.
|
||||
def event_callback(self, name):
|
||||
"""An alias for on_event to maintain compatibility with python-mpv."""
|
||||
return self.on_event(name)
|
||||
|
||||
def on_key_press(self, name):
|
||||
"""
|
||||
Decorator to bind a callback to an MPV keypress event.
|
||||
|
||||
@on_key_press(key_name)
|
||||
def my_callback():
|
||||
pass
|
||||
"""
|
||||
def wrapper(func):
|
||||
self.bind_key_press(name, func)
|
||||
return func
|
||||
return wrapper
|
||||
|
||||
def bind_key_press(self, name, callback):
|
||||
"""
|
||||
Bind a callback to an MPV keypress event.
|
||||
|
||||
*name* is the key symbol.
|
||||
*callback()* is the function to call.
|
||||
"""
|
||||
self.keybind_lock.acquire()
|
||||
keybind_id = self.keybind_id
|
||||
self.keybind_id += 1
|
||||
self.keybind_lock.release()
|
||||
|
||||
bind_name = "bind{0}".format(keybind_id)
|
||||
self.key_bindings["bind{0}".format(keybind_id)] = callback
|
||||
try:
|
||||
self.keybind(name, "script-message custom-bind {0}".format(bind_name))
|
||||
except MPVError:
|
||||
self.define_section(bind_name, "{0} script-message custom-bind {1}".format(name, bind_name))
|
||||
self.enable_section(bind_name)
|
||||
|
||||
def bind_property_observer(self, name, callback):
|
||||
"""
|
||||
Bind a callback to an MPV property change.
|
||||
|
||||
*name* is the property name.
|
||||
*callback(name, data)* is the function to call.
|
||||
|
||||
Returns a unique observer ID needed to destroy the observer.
|
||||
"""
|
||||
self.observer_lock.acquire()
|
||||
observer_id = self.observer_id
|
||||
self.observer_id += 1
|
||||
self.observer_lock.release()
|
||||
|
||||
self.property_bindings[observer_id] = callback
|
||||
self.command("observe_property", observer_id, name)
|
||||
return observer_id
|
||||
|
||||
def unbind_property_observer(self, observer_id):
|
||||
"""
|
||||
Remove callback to an MPV property change.
|
||||
|
||||
*observer_id* is the id returned by bind_property_observer.
|
||||
"""
|
||||
self.command("unobserve_property", observer_id)
|
||||
del self.property_bindings[observer_id]
|
||||
|
||||
def property_observer(self, name):
|
||||
"""
|
||||
Decorator to bind a callback to an MPV property change.
|
||||
|
||||
@property_observer(property_name)
|
||||
def my_callback(name, data):
|
||||
pass
|
||||
"""
|
||||
def wrapper(func):
|
||||
self.bind_property_observer(name, func)
|
||||
return func
|
||||
return wrapper
|
||||
|
||||
def wait_for_property(self, name):
|
||||
"""
|
||||
Waits for the value of a property to change.
|
||||
|
||||
*name* is the name of the property.
|
||||
"""
|
||||
event = threading.Event()
|
||||
first_event = True
|
||||
def handler(*_):
|
||||
nonlocal first_event
|
||||
if first_event == True:
|
||||
first_event = False
|
||||
else:
|
||||
event.set()
|
||||
observer_id = self.bind_property_observer(name, handler)
|
||||
event.wait()
|
||||
self.unbind_property_observer(observer_id)
|
||||
|
||||
def _get_wrapper(self, name):
|
||||
def wrapper(*args):
|
||||
return self.command(name, *args)
|
||||
return wrapper
|
||||
|
||||
def _callback(self, event, data):
|
||||
if event in self.event_bindings:
|
||||
for callback in self.event_bindings[event]:
|
||||
self.event_handler.put_task(callback, data)
|
||||
|
||||
def play(self, url):
|
||||
"""Play the specified URL. An alias to loadfile()."""
|
||||
self.loadfile(url)
|
||||
|
||||
def __del__(self):
|
||||
self.terminate()
|
||||
|
||||
def terminate(self):
|
||||
"""Terminate the connection to MPV and process (if *start_mpv* is used)."""
|
||||
if self.mpv_process:
|
||||
self.mpv_process.stop()
|
||||
if self.mpv_inter:
|
||||
self.mpv_inter.stop()
|
||||
self.event_handler.stop()
|
||||
|
||||
def command(self, command, *args):
|
||||
"""
|
||||
Send a command to MPV. All commands are bound to the class by default,
|
||||
except JSON IPC specific commands. This may also be useful to retain
|
||||
compatibility with python-mpv, as it does not bind all of the commands.
|
||||
|
||||
*command* is the command name.
|
||||
|
||||
All further arguments are forwarded to the MPV command.
|
||||
"""
|
||||
return self.mpv_inter.command(command, *args)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self.properties:
|
||||
return self.command("get_property", name.replace("_", "-"))
|
||||
return object.__getattribute__(self, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name not in {"properties", "command"} and name in self.properties:
|
||||
return self.command("set_property", name.replace("_", "-"), value)
|
||||
return object.__setattr__(self, name, value)
|
||||
|
||||
def __hasattr__(self, name):
|
||||
if object.__hasattr__(self, name):
|
||||
return True
|
||||
else:
|
||||
try:
|
||||
getattr(self, name)
|
||||
return True
|
||||
except MPVError:
|
||||
return False
|
||||
|
||||
def __dir__(self):
|
||||
return self._dir
|
||||
25
syncplay/players/python_mpv_jsonipc/setup.py
Normal file
25
syncplay/players/python_mpv_jsonipc/setup.py
Normal file
@ -0,0 +1,25 @@
|
||||
from setuptools import setup
|
||||
import os
|
||||
|
||||
with open("README.md", "r") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setup(
|
||||
name='python-mpv-jsonipc',
|
||||
version='1.1.10',
|
||||
author="Ian Walton",
|
||||
author_email="iwalton3@gmail.com",
|
||||
description="Python API to MPV using JSON IPC",
|
||||
license='Apache-2.0',
|
||||
long_description=open('README.md').read(),
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/iwalton3/python-mpv-jsonipc",
|
||||
py_modules=['python_mpv_jsonipc'],
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
],
|
||||
python_requires='>=3.6',
|
||||
install_requires=[]
|
||||
)
|
||||
Loading…
x
Reference in New Issue
Block a user