From 1d2a0bef46918009840effe1e6caa865e0c76487 Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 27 Apr 2022 18:14:57 +0200 Subject: [PATCH 01/71] Fix license field in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0b1c6a0..eface0a 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( url = 'https://github.com/jaseg/python-mpv', author = 'jaseg', author_email = 'github@jaseg.net', - license = 'GPLv3+', + license = 'GPLv2+', extras_require = { 'screenshot_raw': ['Pillow'] }, From 4a8554319f2457328330236dbce6c41a5f6c978f Mon Sep 17 00:00:00 2001 From: James Gerity Date: Mon, 13 Jun 2022 00:45:51 -0400 Subject: [PATCH 02/71] Error for known-incompatible libmpv (closes #223) --- mpv.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mpv.py b/mpv.py index 2fb18dc..f1d5424 100644 --- a/mpv.py +++ b/mpv.py @@ -537,6 +537,11 @@ def _mpv_client_api_version(): ver = backend.mpv_client_api_version() return ver>>16, ver&0xFFFF +MPV_VERSION = _mpv_client_api_version() +if MPV_VERSION < (1, 108): + ver = '.'.join(str(num) for num in MPV_VERSION) + raise RuntimeError(f"python-mpv requires libmpv with an API version of 1.108 or higher (libmpv >= 0.33), but you have an older version ({ver}).") + backend.mpv_free.argtypes = [c_void_p] _mpv_free = backend.mpv_free From cc9072d7d8bfa32718a536b62631d95f9ab89af3 Mon Sep 17 00:00:00 2001 From: Naglis Jonaitis Date: Mon, 16 May 2022 00:43:08 +0300 Subject: [PATCH 03/71] Update branch name references --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 5009cf8..3af7d9d 100644 --- a/README.rst +++ b/README.rst @@ -34,7 +34,7 @@ into ctypes, which is different to the one Windows uses internally. Consult `thi Python >= 3.7 (officially) .......................... -The ``master`` branch officially only supports recent python releases (3.5 onwards), but there is the somewhat outdated +The ``main`` branch officially only supports recent python releases (3.5 onwards), but there is the somewhat outdated but functional `py2compat branch`_ providing Python 2 compatibility. .. _`py2compat branch`: https://github.com/jaseg/python-mpv/tree/py2compat @@ -64,7 +64,7 @@ Usage player.play('https://youtu.be/DOmdB7D-pUU') player.wait_for_playback() -python-mpv mostly exposes mpv's built-in API to python, adding only some porcelain on top. Most "`input commands `_" are mapped to methods of the MPV class. Check out these methods and their docstrings in `the source `__ for things you can do. Additional controls and status information are exposed through `MPV properties `_. These can be accessed like ``player.metadata``, ``player.fullscreen`` and ``player.loop_playlist``. +python-mpv mostly exposes mpv's built-in API to python, adding only some porcelain on top. Most "`input commands `_" are mapped to methods of the MPV class. Check out these methods and their docstrings in `the source `__ for things you can do. Additional controls and status information are exposed through `MPV properties `_. These can be accessed like ``player.metadata``, ``player.fullscreen`` and ``player.loop_playlist``. Threading ~~~~~~~~~ @@ -395,7 +395,7 @@ python-mpv inherits the underlying libmpv's license, which can be either GPLv2 o For details, see `the mpv copyright page`_. .. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/ -.. _`mpv.py`: https://raw.githubusercontent.com/jaseg/python-mpv/master/mpv.py +.. _`mpv.py`: https://raw.githubusercontent.com/jaseg/python-mpv/main/mpv.py .. _cosven: https://github.com/cosven .. _Robozman: https://gitlab.com/robozman .. _dfaker: https://github.com/dfaker From 1c0340e2d7521623fcdd18e751d1a63a311c556e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis-Philippe=20V=C3=A9ronneau?= Date: Sun, 24 Apr 2022 22:56:09 -0400 Subject: [PATCH 04/71] Fix typo in function name --- tests/test_mpv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_mpv.py b/tests/test_mpv.py index 3fecfbd..670dbaf 100755 --- a/tests/test_mpv.py +++ b/tests/test_mpv.py @@ -659,7 +659,7 @@ class TestLifecycle(unittest.TestCase): m.terminate() self.disp.stop() - def test_wait_for_prooperty_event_overflow(self): + def test_wait_for_property_event_overflow(self): self.disp = Xvfb() self.disp.start() handler = mock.Mock() From dd4d016de8eb7f79c9fa0810e9e3dc59f949a2de Mon Sep 17 00:00:00 2001 From: Naglis Jonaitis Date: Mon, 16 May 2022 00:54:41 +0300 Subject: [PATCH 05/71] Fix external subtitles example Currently, running the example results in: > TypeError: MPV.play() got an unexpected keyword argument 'sub_file' --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3af7d9d..1f61efe 100644 --- a/README.rst +++ b/README.rst @@ -241,7 +241,8 @@ The easiest way to load custom subtitles from a file is to pass the ``--sub-file import mpv player = mpv.MPV() - player.play('test.webm', sub_file='test.srt') + player.loadfile('test.webm', sub_file='test.srt') + player.wait_for_playback() Note that you can also pass many other options to ``loadfile``. See the mpv docs for details. From 20ec2a74b8c407e1fcaabb1554a4acd83bb57bb9 Mon Sep 17 00:00:00 2001 From: Naglis Jonaitis Date: Mon, 16 May 2022 02:03:31 +0300 Subject: [PATCH 06/71] Fix `observe_property()` docstring 1. Fix decorator name - the decorator for property observation handlers is `@property_observer()`. 2. Fix handler's unregistration method name 3. Fix handler function signature - the signature is ``fun(property_name, new_value)`` and we can't have a bare `*` with no names after it. 4. Fix undefined variable in the example --- mpv.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mpv.py b/mpv.py index f1d5424..1f4f1e8 100644 --- a/mpv.py +++ b/mpv.py @@ -1483,13 +1483,13 @@ class MPV(object): function decorator if no handler is given. To unregister the observer, call either of ``mpv.unobserve_property(name, handler)``, - ``mpv.unobserve_all_properties(handler)`` or the handler's ``unregister_mpv_properties`` attribute:: + ``mpv.unobserve_all_properties(handler)`` or the handler's ``unobserve_mpv_properties`` attribute:: - @player.observe_property('volume') - def my_handler(new_volume, *): - print("It's loud!", volume) + @player.property_observer('volume') + def my_handler(property_name, new_volume): + print("It's loud!", new_volume) - my_handler.unregister_mpv_properties() + my_handler.unobserve_mpv_properties() exit_handler is a function taking no arguments that is called when the underlying mpv handle is terminated (e.g. from calling MPV.terminate() or issuing a "quit" input command). From 91422de05a0e408f175433cc8ee8a29d46936be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sun, 10 Jul 2022 15:13:45 +0200 Subject: [PATCH 07/71] Fix show_text level default parameter --- mpv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpv.py b/mpv.py index 1f4f1e8..d450a4a 100644 --- a/mpv.py +++ b/mpv.py @@ -1390,7 +1390,7 @@ class MPV(object): """Mapped mpv print-text command, see man mpv(1).""" self.command('print-text', text) - def show_text(self, string, duration='-1', level=None): + def show_text(self, string, duration='-1', level=0): """Mapped mpv show_text command, see man mpv(1).""" self.command('show_text', string, duration, level) From 3d90f471d26b1cefa8caebfa55479d81d1b54e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 18:17:00 +0200 Subject: [PATCH 08/71] Remove unused imports --- tests/test_mpv.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_mpv.py b/tests/test_mpv.py index 670dbaf..d0590e3 100755 --- a/tests/test_mpv.py +++ b/tests/test_mpv.py @@ -18,18 +18,11 @@ import unittest from unittest import mock -import math import threading from contextlib import contextmanager -from functools import wraps -import gc import os.path import os -import sys import time -import io -import platform -import ctypes from concurrent.futures import Future os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] From 721cff015b46b4f7bab9d4911fa55856519d9aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 18:17:10 +0200 Subject: [PATCH 09/71] Add requirements file --- tests/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tests/requirements.txt diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..4a9b79c --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +xvfbwrapper>=0.2.9 +pytest>=7.1.2 From dffd89630bb7728327fe2a3f140d17e6b8fb9eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 18:24:16 +0200 Subject: [PATCH 10/71] Add test workflow --- .github/workflows/tests.yml | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..a1b02ee --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,50 @@ +name: 'Tests' + + +on: + push: + branches: [ '**' ] + + +defaults: + run: + shell: bash + + +jobs: + test-python: + runs-on: ubuntu-22.04 + strategy: + matrix: + python-version: [ '3.10' ] # '3.7', '3.8', '3.9' + name: 'Python' + steps: + - uses: actions/checkout@v2 + - name: 'Install Python' + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: 'Install pip' + run: | + function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } + execute python -m pip install --upgrade pip + - name: 'Update Packages' + run: | + function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } + execute sudo apt update -y && sudo apt upgrade -y + - name: 'Install Dependencies' + run: | + function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } + execute sudo apt install -y libmpv1 xvfb + - name: 'Create Virtual Environment' + run: | + function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } + execute python -m venv venv + execute source venv/bin/activate + execute python -m pip install wheel + execute python -m pip install -r tests/requirements.txt + - name: 'Run Python Tests' + run: | + function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } + source venv/bin/activate + execute python -m pytest From e91fd16f35dd8048356f5aab9368123997b1618d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 18:31:42 +0200 Subject: [PATCH 11/71] Start Xvfb before running tests --- .github/workflows/tests.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a1b02ee..7cc905f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,6 +12,9 @@ defaults: jobs: + build: + env: + DISPLAY: :0 test-python: runs-on: ubuntu-22.04 strategy: @@ -36,6 +39,10 @@ jobs: run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } execute sudo apt install -y libmpv1 xvfb + - name: 'Start Xvfb' + run: | + echo -e "\033[0;34msudo /usr/bin/Xvfb $DISPLAY -screen 0 1920x1080x24 &\033[0m"; + sudo /usr/bin/Xvfb $DISPLAY -screen 0 1920x1080x24 & - name: 'Create Virtual Environment' run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } From af0a8c6bff09bc75d4f1f6d7638714350b2eb9d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 18:32:56 +0200 Subject: [PATCH 12/71] Fix environment variable --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7cc905f..4185ea3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,15 +12,15 @@ defaults: jobs: - build: - env: - DISPLAY: :0 test-python: runs-on: ubuntu-22.04 strategy: matrix: python-version: [ '3.10' ] # '3.7', '3.8', '3.9' name: 'Python' + build: + env: + DISPLAY: :0 steps: - uses: actions/checkout@v2 - name: 'Install Python' From 328bf1d0dc9cc2ad6ecf90b3df657e80cc4dd50f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 18:33:34 +0200 Subject: [PATCH 13/71] Fix environment variable --- .github/workflows/tests.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4185ea3..9fca786 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,9 +18,8 @@ jobs: matrix: python-version: [ '3.10' ] # '3.7', '3.8', '3.9' name: 'Python' - build: - env: - DISPLAY: :0 + env: + DISPLAY: :0 steps: - uses: actions/checkout@v2 - name: 'Install Python' From 6a71ee0b9afbb54094aa3da6bab2b5d32eced9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 18:51:38 +0200 Subject: [PATCH 14/71] Comment error prone test --- tests/test_mpv.py | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/tests/test_mpv.py b/tests/test_mpv.py index d0590e3..573919c 100755 --- a/tests/test_mpv.py +++ b/tests/test_mpv.py @@ -652,27 +652,24 @@ class TestLifecycle(unittest.TestCase): m.terminate() self.disp.stop() - def test_wait_for_property_event_overflow(self): - self.disp = Xvfb() - self.disp.start() - handler = mock.Mock() - m = mpv.MPV(vo=testvo) - m.play(TESTVID) - with self.assertRaises(mpv.EventOverflowError): - # level_sensitive=false needed to prevent get_property on dead - # handle - with m.prepare_and_wait_for_property('mute', cond=lambda val: time.sleep(0.001)): - for i in range(10000): - try: - # We really have to try hard to fill up the queue here. Simple async commands will not work, - # since then command_async will throw a memory error first. Property changes also do not work, - # since they are only processsed when the event loop is idle. This here works reliably. - m.command_async('script-message', 'foo', 'bar') - except: - pass - self.disp.stop() - - + # def test_wait_for_property_event_overflow(self): + # self.disp = Xvfb() + # self.disp.start() + # m = mpv.MPV(vo=testvo) + # m.play(TESTVID) + # with self.assertRaises(mpv.EventOverflowError): + # # level_sensitive=false needed to prevent get_property on dead + # # handle + # with m.prepare_and_wait_for_property('mute', cond=lambda val: time.sleep(0.001)): + # for i in range(10000): + # try: + # # We really have to try hard to fill up the queue here. Simple async commands will not work, + # # since then command_async will throw a memory error first. Property changes also do not work, + # # since they are only processsed when the event loop is idle. This here works reliably. + # m.command_async('script-message', 'foo', 'bar') + # except: + # pass + # self.disp.stop() def test_wait_for_event_shutdown(self): self.disp = Xvfb() From 0e19248d690eb7263c2ee31e0d5841bfea1889c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 18:54:01 +0200 Subject: [PATCH 15/71] Use two blank lines between classes and functions --- tests/test_mpv.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_mpv.py b/tests/test_mpv.py index 573919c..bfd882c 100755 --- a/tests/test_mpv.py +++ b/tests/test_mpv.py @@ -43,12 +43,14 @@ TESTVID = os.path.join(os.path.dirname(__file__), 'test.webm') TESTSRT = os.path.join(os.path.dirname(__file__), 'sub_test.srt') MPV_ERRORS = [ l(ec) for ec, l in mpv.ErrorCode.EXCEPTION_DICT.items() if l ] + def timed_print(): start_time = time.time() def do_print(level, prefix, text): td = time.time() - start_time print('{:.3f} [{}] {}: {}'.format(td, level, prefix, text), flush=True) + class MpvTestCase(unittest.TestCase): def setUp(self): self.disp = Xvfb() @@ -59,6 +61,7 @@ class MpvTestCase(unittest.TestCase): self.m.terminate() self.disp.stop() + class TestProperties(MpvTestCase): @contextmanager def swallow_mpv_errors(self, exception_exceptions=[]): @@ -288,6 +291,7 @@ class ObservePropertyTest(MpvTestCase): mock.call('slang', ['ru'])], any_order=True) + class KeyBindingTest(MpvTestCase): def test_register_direct_cmd(self): self.m.register_key_binding('a', 'playlist-clear') @@ -434,6 +438,7 @@ class KeyBindingTest(MpvTestCase): handler1.assert_has_calls([]) handler2.assert_has_calls([ mock.call() ]) + class TestStreams(unittest.TestCase): def test_python_stream(self): handler = mock.Mock() @@ -529,6 +534,7 @@ class TestStreams(unittest.TestCase): m.terminate() disp.stop() + class TestLifecycle(unittest.TestCase): def test_create_destroy(self): thread_names = lambda: [ t.name for t in threading.enumerate() ] @@ -756,7 +762,6 @@ class CommandTests(MpvTestCase): callback.assert_any_call(None, None) - class RegressionTests(MpvTestCase): def test_unobserve_property_runtime_error(self): From 5d9cd2b25219690fa98fe62b58dbc323b4a04462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 18:57:05 +0200 Subject: [PATCH 16/71] Remove unused variables --- tests/test_mpv.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_mpv.py b/tests/test_mpv.py index bfd882c..ad71818 100755 --- a/tests/test_mpv.py +++ b/tests/test_mpv.py @@ -625,7 +625,6 @@ class TestLifecycle(unittest.TestCase): def test_wait_for_event(self): self.disp = Xvfb() self.disp.start() - handler = mock.Mock() m = mpv.MPV(vo=testvo) m.play(TESTVID) result = Future() @@ -648,7 +647,6 @@ class TestLifecycle(unittest.TestCase): def test_wait_for_property_shutdown(self): self.disp = Xvfb() self.disp.start() - handler = mock.Mock() m = mpv.MPV(vo=testvo) m.play(TESTVID) with self.assertRaises(mpv.ShutdownError): @@ -690,7 +688,6 @@ class TestLifecycle(unittest.TestCase): def test_wait_for_shutdown(self): self.disp = Xvfb() self.disp.start() - handler = mock.Mock() m = mpv.MPV(vo=testvo) m.play(TESTVID) with self.assertRaises(mpv.ShutdownError): From 9626d790f5e176255a350f5656b71a0e663bf1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 18:58:05 +0200 Subject: [PATCH 17/71] Remove starter Run: - python -m unittest or - python -m pytest --- tests/test_mpv.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_mpv.py b/tests/test_mpv.py index ad71818..9a2a9a7 100755 --- a/tests/test_mpv.py +++ b/tests/test_mpv.py @@ -812,7 +812,3 @@ class RegressionTests(MpvTestCase): m.slang = 'ru' m.terminate() # needed for synchronization of event thread handler.assert_has_calls([mock.call('slang', ['jp']), mock.call('slang', ['ru'])]) - - -if __name__ == '__main__': - unittest.main() From d027abcc3a1c193b661f24ba9ba3fde4889b9478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 19:02:27 +0200 Subject: [PATCH 18/71] Upgrade pip --- .github/workflows/tests.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9fca786..5d61529 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,10 +26,6 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - name: 'Install pip' - run: | - function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } - execute python -m pip install --upgrade pip - name: 'Update Packages' run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } @@ -47,6 +43,7 @@ jobs: function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } execute python -m venv venv execute source venv/bin/activate + execute python -m pip install --upgrade pip execute python -m pip install wheel execute python -m pip install -r tests/requirements.txt - name: 'Run Python Tests' From fcd2c354ae879b3351a244dde87426fd38558640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 19:17:23 +0200 Subject: [PATCH 19/71] Use xvfb-run to run tests --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5d61529..5dfcb58 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,5 +49,5 @@ jobs: - name: 'Run Python Tests' run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } - source venv/bin/activate - execute python -m pytest + execute source venv/bin/activate + execute xvfb-run python -m pytest From b12203d2d2cfcd7f7868eb8e5bde1c7fa48e12b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 19:19:34 +0200 Subject: [PATCH 20/71] Skip unreliable test in CI --- .github/workflows/tests.yml | 2 ++ tests/test_mpv.py | 38 +++++++++++++++++++------------------ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5dfcb58..e262bba 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,6 +20,8 @@ jobs: name: 'Python' env: DISPLAY: :0 + PY_MPV_SKIP_TESTS: >- + test_wait_for_property_event_overflow steps: - uses: actions/checkout@v2 - name: 'Install Python' diff --git a/tests/test_mpv.py b/tests/test_mpv.py index 9a2a9a7..c2702b5 100755 --- a/tests/test_mpv.py +++ b/tests/test_mpv.py @@ -42,6 +42,7 @@ else: TESTVID = os.path.join(os.path.dirname(__file__), 'test.webm') TESTSRT = os.path.join(os.path.dirname(__file__), 'sub_test.srt') MPV_ERRORS = [ l(ec) for ec, l in mpv.ErrorCode.EXCEPTION_DICT.items() if l ] +SKIP_TESTS = os.environ.get('PY_MPV_SKIP_TESTS', '').split() def timed_print(): @@ -656,24 +657,25 @@ class TestLifecycle(unittest.TestCase): m.terminate() self.disp.stop() - # def test_wait_for_property_event_overflow(self): - # self.disp = Xvfb() - # self.disp.start() - # m = mpv.MPV(vo=testvo) - # m.play(TESTVID) - # with self.assertRaises(mpv.EventOverflowError): - # # level_sensitive=false needed to prevent get_property on dead - # # handle - # with m.prepare_and_wait_for_property('mute', cond=lambda val: time.sleep(0.001)): - # for i in range(10000): - # try: - # # We really have to try hard to fill up the queue here. Simple async commands will not work, - # # since then command_async will throw a memory error first. Property changes also do not work, - # # since they are only processsed when the event loop is idle. This here works reliably. - # m.command_async('script-message', 'foo', 'bar') - # except: - # pass - # self.disp.stop() + @unittest.skipIf('test_wait_for_property_event_overflow' in SKIP_TESTS, reason="kills X-Server first") + def test_wait_for_property_event_overflow(self): + self.disp = Xvfb() + self.disp.start() + m = mpv.MPV(vo=testvo) + m.play(TESTVID) + with self.assertRaises(mpv.EventOverflowError): + # level_sensitive=false needed to prevent get_property on dead + # handle + with m.prepare_and_wait_for_property('mute', cond=lambda val: time.sleep(0.001)): + for i in range(10000): + try: + # We really have to try hard to fill up the queue here. Simple async commands will not work, + # since then command_async will throw a memory error first. Property changes also do not work, + # since they are only processsed when the event loop is idle. This here works reliably. + m.command_async('script-message', 'foo', 'bar') + except: + pass + self.disp.stop() def test_wait_for_event_shutdown(self): self.disp = Xvfb() From 543bc9b9583b561077559102fe4296c9e0e19f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 19:25:08 +0200 Subject: [PATCH 21/71] Use newer actions --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e262bba..cfba59e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,9 +23,9 @@ jobs: PY_MPV_SKIP_TESTS: >- test_wait_for_property_event_overflow steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: 'Install Python' - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: 'Update Packages' From 7a428a22e05b5e80d057c2fdcea0b688ae1a7325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 19:25:22 +0200 Subject: [PATCH 22/71] Rename job --- .github/workflows/tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cfba59e..8f8fb4f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,8 +12,9 @@ defaults: jobs: - test-python: + test-linux: runs-on: ubuntu-22.04 + name: 'Linux - Python' strategy: matrix: python-version: [ '3.10' ] # '3.7', '3.8', '3.9' From 427603bd81fa6c791ca0e780f46ef4dee149924c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 19:25:33 +0200 Subject: [PATCH 23/71] Run on Python 3.7 - 3.10 --- .github/workflows/tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f8fb4f..54f9aa8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,8 +17,7 @@ jobs: name: 'Linux - Python' strategy: matrix: - python-version: [ '3.10' ] # '3.7', '3.8', '3.9' - name: 'Python' + python-version: [ '3.7', '3.8', '3.9', '3.10' ] env: DISPLAY: :0 PY_MPV_SKIP_TESTS: >- From 49a26a663d96d5f6a5e317e40439b66f81218d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 19:30:11 +0200 Subject: [PATCH 24/71] Add Windows test --- .github/workflows/tests.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 54f9aa8..ef33234 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,3 +53,38 @@ jobs: function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } execute source venv/bin/activate execute xvfb-run python -m pytest + + test-windows: + runs-on: windows-latest + name: 'Windows - Python' + strategy: + matrix: + python-version: [ '3.10' ] # '3.7', '3.8', '3.9' + steps: + - uses: actions/checkout@v3 + - name: 'Install Python' + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: 'Setup Build Environment' + run: | + function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } + execute python -m venv venv + execute source venv/Scripts/activate + execute python -m pip install --upgrade pip + execute python -m pip install wheel + execute python -m pip install -r tests/requirements.txt + - name: 'Install libmpv' + run: | + function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } + + ARTIFACT="mpv-dev-x86_64-20220619-git-c1a46ec.7z" + URL="https://sourceforge.net/projects/mpv-player-windows/files/libmpv/$ARTIFACT" + + execute curl -L -O "$URL" + execute 7z x "$ARTIFACT" + - name: 'Run Python Tests' + run: | + function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } + execute source venv/Scripts/activate + execute python -m pytest From 1ed91e44e336dc238d95ac192b1c9f87749e46a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 19:32:16 +0200 Subject: [PATCH 25/71] Skip Python 3.7 --- .github/workflows/tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ef33234..5b3b553 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,8 @@ jobs: name: 'Linux - Python' strategy: matrix: - python-version: [ '3.7', '3.8', '3.9', '3.10' ] + # '3.7' -> E ImportError: cannot import name 'InvalidStateError' from 'concurrent.futures' (/opt/hostedtoolcache/Python/3.7.13/x64/lib/python3.7/concurrent/futures/__init__.py) + python-version: [ '3.8', '3.9', '3.10' ] env: DISPLAY: :0 PY_MPV_SKIP_TESTS: >- From 976b7e685e70b071f99a09be9bab26c114ee618a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 19:35:13 +0200 Subject: [PATCH 26/71] Make mpv-2.dll available --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5b3b553..6023730 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -84,6 +84,7 @@ jobs: execute curl -L -O "$URL" execute 7z x "$ARTIFACT" + execute mv mpv-2.dll tests - name: 'Run Python Tests' run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } From bb7d0a332dd4f7268965400efb2cc52915c5eb4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 19:38:48 +0200 Subject: [PATCH 27/71] Run Windows tests on Python 3.8 - 3.10 --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6023730..78a5f1a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -60,7 +60,7 @@ jobs: name: 'Windows - Python' strategy: matrix: - python-version: [ '3.10' ] # '3.7', '3.8', '3.9' + python-version: [ '3.8', '3.9', '3.10' ] steps: - uses: actions/checkout@v3 - name: 'Install Python' @@ -75,7 +75,7 @@ jobs: execute python -m pip install --upgrade pip execute python -m pip install wheel execute python -m pip install -r tests/requirements.txt - - name: 'Install libmpv' + - name: 'Provide libmpv' run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } From 6f07273482fad7036d83cb0bc80153061798224d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 19:45:07 +0200 Subject: [PATCH 28/71] Cleanup pipeline --- .github/workflows/tests.yml | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 78a5f1a..7e0c067 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,6 @@ jobs: name: 'Linux - Python' strategy: matrix: - # '3.7' -> E ImportError: cannot import name 'InvalidStateError' from 'concurrent.futures' (/opt/hostedtoolcache/Python/3.7.13/x64/lib/python3.7/concurrent/futures/__init__.py) python-version: [ '3.8', '3.9', '3.10' ] env: DISPLAY: :0 @@ -32,18 +31,21 @@ jobs: - name: 'Update Packages' run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } + execute sudo apt update -y && sudo apt upgrade -y - name: 'Install Dependencies' run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } + execute sudo apt install -y libmpv1 xvfb - name: 'Start Xvfb' run: | echo -e "\033[0;34msudo /usr/bin/Xvfb $DISPLAY -screen 0 1920x1080x24 &\033[0m"; sudo /usr/bin/Xvfb $DISPLAY -screen 0 1920x1080x24 & - - name: 'Create Virtual Environment' + - name: 'Setup Test Environment' run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } + execute python -m venv venv execute source venv/bin/activate execute python -m pip install --upgrade pip @@ -52,6 +54,7 @@ jobs: - name: 'Run Python Tests' run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } + execute source venv/bin/activate execute xvfb-run python -m pytest @@ -67,14 +70,6 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: 'Setup Build Environment' - run: | - function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } - execute python -m venv venv - execute source venv/Scripts/activate - execute python -m pip install --upgrade pip - execute python -m pip install wheel - execute python -m pip install -r tests/requirements.txt - name: 'Provide libmpv' run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } @@ -85,8 +80,18 @@ jobs: execute curl -L -O "$URL" execute 7z x "$ARTIFACT" execute mv mpv-2.dll tests + - name: 'Setup Test Environment' + run: | + function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } + + execute python -m venv venv + execute source venv/Scripts/activate + execute python -m pip install --upgrade pip + execute python -m pip install wheel + execute python -m pip install -r tests/requirements.txt - name: 'Run Python Tests' run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } + execute source venv/Scripts/activate execute python -m pytest From 3c25b1d9f390e7feda3514f480658c6ef6e9063a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 9 Jul 2022 19:58:34 +0200 Subject: [PATCH 29/71] Fix package upgrade --- .github/workflows/tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7e0c067..8a0d066 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,7 +32,8 @@ jobs: run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } - execute sudo apt update -y && sudo apt upgrade -y + execute sudo apt update -y + execute sudo apt upgrade -y - name: 'Install Dependencies' run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } From 85aaed090f05198ce84f2c8c653ebe44c4288f8d Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 11 Jul 2022 13:49:46 +0200 Subject: [PATCH 30/71] tests: Add strategic sleeps to reduce likelihood of race condition during test teardown --- tests/test_mpv.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_mpv.py b/tests/test_mpv.py index c2702b5..38b0222 100755 --- a/tests/test_mpv.py +++ b/tests/test_mpv.py @@ -577,8 +577,8 @@ class TestLifecycle(unittest.TestCase): mock.call({'event': 'start-file', 'playlist_entry_id': 1}), mock.call({'event': 'end-file', 'reason': 'error', 'playlist_entry_id': 1, 'file_error': 'no audio or video data played'}) ], any_order=True) + time.sleep(1) handler.reset_mock() - m.terminate() handler.assert_not_called() @@ -600,6 +600,7 @@ class TestLifecycle(unittest.TestCase): t.start() time.sleep(1) m.terminate() + time.sleep(1) t.join() self.disp.stop() assert result.result() @@ -620,6 +621,7 @@ class TestLifecycle(unittest.TestCase): m.mute = True t.join() m.terminate() + time.sleep(1) handler.assert_called() self.disp.stop() @@ -655,6 +657,7 @@ class TestLifecycle(unittest.TestCase): # handle with m.prepare_and_wait_for_property('mute', level_sensitive=False): m.terminate() + time.sleep(1) self.disp.stop() @unittest.skipIf('test_wait_for_property_event_overflow' in SKIP_TESTS, reason="kills X-Server first") @@ -675,6 +678,8 @@ class TestLifecycle(unittest.TestCase): m.command_async('script-message', 'foo', 'bar') except: pass + m.terminate() + time.sleep(1) self.disp.stop() def test_wait_for_event_shutdown(self): From 2330fcd416f0dcfa84a548163c6d83a557d0826f Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 16 Aug 2022 16:44:27 +0200 Subject: [PATCH 31/71] Fix typo in repr format string fixes #231 --- mpv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpv.py b/mpv.py index d450a4a..f1e4fa3 100644 --- a/mpv.py +++ b/mpv.py @@ -292,7 +292,7 @@ class MpvEventID(c_int): FILE_LOADED, CLIENT_MESSAGE, VIDEO_RECONFIG, AUDIO_RECONFIG, SEEK, PLAYBACK_RESTART, PROPERTY_CHANGE) def __repr__(self): - return f'' + return f'' @classmethod def from_str(kls, s): From 30d5ca1f0255184b500cbb43d9b3556a70038c53 Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 16 Aug 2022 16:45:29 +0200 Subject: [PATCH 32/71] Update setup.cfg for new syntax fixes #230 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 5aef279..ddb7da9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ [metadata] -description-file = README.rst +description_file = README.rst From 5fbdccc89f60c9e3c2967a1966fac57f6566fa54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis-Philippe=20V=C3=A9ronneau?= Date: Sun, 27 Nov 2022 16:11:22 -0500 Subject: [PATCH 33/71] Migrate away from setup.py and use PEP 612-style pyproject.toml Closes #240. --- pyproject.toml | 39 +++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 -- setup.py | 40 ---------------------------------------- 3 files changed, 39 insertions(+), 42 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100755 setup.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..05f6090 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,39 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +py-modules = ['mpv'] + +[project] +name = "python-mpv" +version = "1.0.1" +description = "A python interface to the mpv media player" +readme = "README.rst" +authors = [{name = "jaseg", email = "github@jaseg.net"}] +license = {text = "GPLv3+"} +requires-python = ">=3.7" +keywords = ['mpv', 'library', 'video', 'audio', 'player', 'display', 'multimedia'] +classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Environment :: X11 Applications', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', + 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)', + 'Natural Language :: English', + 'Operating System :: POSIX', + 'Programming Language :: C', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Topic :: Multimedia :: Sound/Audio :: Players', + 'Topic :: Multimedia :: Video :: Display' +] + +[project.urls] +homepage = "https://github.com/jaseg/python-mpv" + +[project.optional-dependencies] +screenshot_raw = ["Pillow"] +test = ['xvfbwrapper'] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ddb7da9..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description_file = README.rst diff --git a/setup.py b/setup.py deleted file mode 100755 index eface0a..0000000 --- a/setup.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 - -from setuptools import setup -from pathlib import Path - -setup( - name = 'python-mpv', - version = '1.0.1', - py_modules = ['mpv'], - description = 'A python interface to the mpv media player', - long_description = (Path(__file__).parent / 'README.rst').read_text(), - long_description_content_type = 'text/x-rst', - url = 'https://github.com/jaseg/python-mpv', - author = 'jaseg', - author_email = 'github@jaseg.net', - license = 'GPLv2+', - extras_require = { - 'screenshot_raw': ['Pillow'] - }, - tests_require = ['xvfbwrapper'], - test_suite = 'tests', - keywords = ['mpv', 'library', 'video', 'audio', 'player', 'display', - 'multimedia'], - python_requires='>=3.7', - classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Environment :: X11 Applications', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', - 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)', - 'Natural Language :: English', - 'Operating System :: POSIX', - 'Programming Language :: C', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Topic :: Multimedia :: Sound/Audio :: Players', - 'Topic :: Multimedia :: Video :: Display'] -) From 2f117e0ec78a7497278e0f2521d03fa88e961a00 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 26 Feb 2023 12:48:17 +0100 Subject: [PATCH 34/71] Update package metadata --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 05f6090..67a2565 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,8 @@ name = "python-mpv" version = "1.0.1" description = "A python interface to the mpv media player" readme = "README.rst" -authors = [{name = "jaseg", email = "github@jaseg.net"}] -license = {text = "GPLv3+"} +authors = [{name = "jaseg", email = "mpv@jaseg.de"}] +license = {text = "GPLv2+ or LGPLv2.1+"} requires-python = ">=3.7" keywords = ['mpv', 'library', 'video', 'audio', 'player', 'display', 'multimedia'] classifiers = [ From 6625a3900da1bd15fa554b77f87d3f75b43bc7e0 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 26 Feb 2023 13:07:23 +0100 Subject: [PATCH 35/71] Update license header with dual-license information Closes #206 --- mpv.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/mpv.py b/mpv.py index f1e4fa3..97c73da 100644 --- a/mpv.py +++ b/mpv.py @@ -4,16 +4,18 @@ # Python MPV library module # Copyright (C) 2017-2022 Sebastian Götte # -# This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public -# License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later -# version. +# python-mpv inherits the underlying libmpv's license, which can be either GPLv2 or later (default) or LGPLv2.1 or +# later. For details, see the mpv copyright page here: https://github.com/mpv-player/mpv/blob/master/Copyright # -# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied -# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# You may copy, modify, and redistribute this file under the terms of the GNU General Public License version 2 (or, at +# your option, any later version), or the GNU Lesser General Public License as published by the Free Software +# Foundation; either version 2.1 of the License, or (at your option) any later version. # -# You should have received a copy of the GNU General Public License along with this program; if not, write to the Free -# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License and the GNU +# Lesser General Public License for more details. # +# You can find copies of the GPLv2 and LGPLv2.1 licenses in the project repository's LICENSE.GPL and LICENSE.LGPL files. from ctypes import * import ctypes.util From 244c0ef31650a8e02418e94d625af81146e90dcf Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 26 Feb 2023 14:06:41 +0100 Subject: [PATCH 36/71] Version v1.0.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 67a2565..769a2c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ py-modules = ['mpv'] [project] name = "python-mpv" -version = "1.0.1" +version = "v1.0.2" description = "A python interface to the mpv media player" readme = "README.rst" authors = [{name = "jaseg", email = "mpv@jaseg.de"}] From d7664eb6716cb35a116b402da952dcb5ae0f01bd Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 26 Feb 2023 15:17:38 +0100 Subject: [PATCH 37/71] Fix release script for new packaging workflow --- do_release.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/do_release.sh b/do_release.sh index d5cdea4..df301f2 100755 --- a/do_release.sh +++ b/do_release.sh @@ -10,8 +10,8 @@ if [ -n "$(git diff --name-only --cached)" ]; then exit 2 fi -sed -i "s/^\\(\\s*version\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*,\\s*\\)$/\\1"$VER"\\2/" setup.py -git add setup.py +sed -i "s/^\\(\\s*version\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*\\)$/\\1"$VER"\\2/" pyproject.toml +git add pyproject.toml git commit -m "Version $VER" --no-edit -env QUBES_GPG_DOMAIN=gpg git -c gpg.program=qubes-gpg-client -c user.signingkey=E36F75307F0A0EC2D145FF5CED7A208EEEC76F2D -c user.email=python-mpv@jaseg.de tag -s "v$VER" -m "Version $VER" +git -c user.signingkey=E36F75307F0A0EC2D145FF5CED7A208EEEC76F2D -c user.email=python-mpv@jaseg.de tag -s "v$VER" -m "Version $VER" git push --tags origin From f9a655e7ca39a29d71baa177bac94e7be04f6936 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 26 Feb 2023 19:46:59 +0100 Subject: [PATCH 38/71] Add cross-thread exception handling for event loop and stream callbacks --- mpv.py | 128 +++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 92 insertions(+), 36 deletions(-) diff --git a/mpv.py b/mpv.py index 97c73da..6918f97 100644 --- a/mpv.py +++ b/mpv.py @@ -20,6 +20,7 @@ from ctypes import * import ctypes.util import threading +import queue import os import sys from warnings import warn @@ -880,6 +881,7 @@ class MPV(object): self.register_stream_protocol('python', self._python_stream_open) self._python_streams = {} self._python_stream_catchall = None + self._exception_futures = set() self.overlay_ids = set() self.overlays = {} if loglevel is not None or log_handler is not None: @@ -891,6 +893,17 @@ class MPV(object): else: self._event_thread = None + @contextmanager + def _enqueue_exceptions(self): + try: + yield + except Exception as e: + try: + fut = next(iter(self._exception_futures)) + fut.set_exception(e) + except StopIteration: + warn(f'Unhandled exception on python-mpv event loop: {e}\n{traceback.format_exc()}', RuntimeWarning) + def _loop(self): for event in _event_generator(self._event_handle): try: @@ -901,45 +914,51 @@ class MPV(object): self._core_shutdown = True for callback in self._event_callbacks: - callback(event) + with self._enqueue_exceptions(): + callback(event) if eid == MpvEventID.PROPERTY_CHANGE: pc = event.data name, value, _fmt = pc.name, pc.value, pc.format for handler in self._property_handlers[name]: - handler(name, value) + with self._enqueue_exceptions(): + handler(name, value) if eid == MpvEventID.LOG_MESSAGE and self._log_handler is not None: ev = event.data - self._log_handler(ev.level, ev.prefix, ev.text) + with self._enqueue_exceptions(): + self._log_handler(ev.level, ev.prefix, ev.text) if eid == MpvEventID.CLIENT_MESSAGE: # {'event': {'args': ['key-binding', 'foo', 'u-', 'g']}, 'reply_userdata': 0, 'error': 0, 'event_id': 16} target, *args = event.data.args target = target.decode("utf-8") if target in self._message_handlers: - self._message_handlers[target](*args) + with self._enqueue_exceptions(): + self._message_handlers[target](*args) if eid == MpvEventID.COMMAND_REPLY: key = event.reply_userdata callback = self._command_reply_callbacks.pop(key, None) if callback: - callback(ErrorCode.exception_for_ec(event.error), event.data) + with self._enqueue_exceptions(): + callback(ErrorCode.exception_for_ec(event.error), event.data) if eid == MpvEventID.QUEUE_OVERFLOW: # cache list, since error handlers will unregister themselves for cb in list(self._command_reply_callbacks.values()): - cb(EventOverflowError('libmpv event queue has flown over because events have not been processed fast enough'), None) + with self._enqueue_exceptions(): + cb(EventOverflowError('libmpv event queue has flown over because events have not been processed fast enough'), None) if eid == MpvEventID.SHUTDOWN: _mpv_destroy(self._event_handle) for cb in list(self._command_reply_callbacks.values()): - cb(ShutdownError('libmpv core has been shutdown'), None) + with self._enqueue_exceptions(): + cb(ShutdownError('libmpv core has been shutdown'), None) return except Exception as e: - print('Exception inside python-mpv event loop:', file=sys.stderr) - traceback.print_exc() + warn(f'Unhandled {e} inside python-mpv event loop!\n{traceback.format_exc()}', RuntimeWarning) @property def core_shutdown(self): @@ -953,35 +972,35 @@ class MPV(object): if self._core_shutdown: raise ShutdownError('libmpv core has been shutdown') - def wait_until_paused(self, timeout=None): + def wait_until_paused(self, timeout=None, catch_errors=True): """Waits until playback of the current title is paused or done. Raises a ShutdownError if the core is shutdown while waiting.""" - self.wait_for_property('core-idle', timeout=timeout) + self.wait_for_property('core-idle', timeout=timeout, catch_errors=catch_errors) - def wait_for_playback(self, timeout=None): + def wait_for_playback(self, timeout=None, catch_errors=True): """Waits until playback of the current title is finished. Raises a ShutdownError if the core is shutdown while waiting. """ - self.wait_for_event('end_file', timeout=timeout) + self.wait_for_event('end_file', timeout=timeout, catch_errors=catch_errors) - def wait_until_playing(self, timeout=None): + def wait_until_playing(self, timeout=None, catch_errors=True): """Waits until playback of the current title has started. Raises a ShutdownError if the core is shutdown while waiting.""" - self.wait_for_property('core-idle', lambda idle: not idle, timeout=timeout) + self.wait_for_property('core-idle', lambda idle: not idle, timeout=timeout, catch_errors=catch_errors) - def wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None): + def wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None, catch_errors=True): """Waits until ``cond`` evaluates to a truthy value on the named property. This can be used to wait for properties such as ``idle_active`` indicating the player is done with regular playback and just idling around. Raises a ShutdownError when the core is shutdown while waiting. """ - with self.prepare_and_wait_for_property(name, cond, level_sensitive, timeout=timeout) as result: + with self.prepare_and_wait_for_property(name, cond, level_sensitive, timeout=timeout, catch_errors=catch_errors) as result: pass return result.result() - def wait_for_shutdown(self, timeout=None): + def wait_for_shutdown(self, timeout=None, catch_errors=True): '''Wait for core to shutdown (e.g. through quit() or terminate()).''' try: - self.wait_for_event(None, timeout=timeout) + self.wait_for_event(None, timeout=timeout, catch_errors=catch_errors) except ShutdownError: return @@ -999,7 +1018,7 @@ class MPV(object): return shutdown_handler.unregister_mpv_events @contextmanager - def prepare_and_wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None): + def prepare_and_wait_for_property(self, name, cond=lambda val: val, level_sensitive=True, timeout=None, catch_errors=True): """Context manager that waits until ``cond`` evaluates to a truthy value on the named property. See prepare_and_wait_for_event for usage. Raises a ShutdownError when the core is shutdown while waiting. Re-raises any errors inside ``cond``. @@ -1023,6 +1042,9 @@ class MPV(object): try: result.set_running_or_notify_cancel() + if catch_errors: + self._exception_futures.add(result) + yield result rv = cond(getattr(self, name.replace('-', '_'))) @@ -1035,18 +1057,19 @@ class MPV(object): finally: err_unregister() self.unobserve_property(name, observer) + self._exception_futures.discard(result) - def wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None): + def wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None, catch_errors=True): """Waits for the indicated event(s). If cond is given, waits until cond(event) is true. Raises a ShutdownError if the core is shutdown while waiting. This also happens when 'shutdown' is in event_types. Re-raises any error inside ``cond``. """ - with self.prepare_and_wait_for_event(*event_types, cond=cond, timeout=timeout) as result: + with self.prepare_and_wait_for_event(*event_types, cond=cond, timeout=timeout, catch_errors=catch_errors) as result: pass return result.result() @contextmanager - def prepare_and_wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None): + def prepare_and_wait_for_event(self, *event_types, cond=lambda evt: True, timeout=None, catch_errors=True): """Context manager that waits for the indicated event(s) like wait_for_event after running. If cond is given, waits until cond(event) is true. Raises a ShutdownError if the core is shutdown while waiting. This also happens when 'shutdown' is in event_types. Re-raises any error inside ``cond``. @@ -1081,13 +1104,18 @@ class MPV(object): try: result.set_running_or_notify_cancel() + if catch_errors: + self._exception_futures.add(result) + yield result + self.check_core_alive() result.result(timeout) finally: err_unregister() target_handler.unregister_mpv_events() + self._exception_futures.discard(result) def __del__(self): if self.handle: @@ -1772,32 +1800,60 @@ class MPV(object): frontend = open_fn(uri.decode('utf-8')) except ValueError: return ErrorCode.LOADING_FAILED + except Exception as e: + try: + fut = next(iter(self._exception_futures)) + fut.set_exception(e) + except StopIteration: + warnings.warn(f'Unhandled exception {e} inside stream open callback for URI {uri}\n{traceback.format_exc()}') - def read_backend(_userdata, buf, bufsize): - data = frontend.read(bufsize) - for i in range(len(data)): - buf[i] = data[i] - return len(data) + return ErrorCode.LOADING_FAILED cb_info.contents.cookie = None + + def read_backend(_userdata, buf, bufsize): + with self._enqueue_exceptions(): + data = frontend.read(bufsize) + for i in range(len(data)): + buf[i] = data[i] + return len(data) read = cb_info.contents.read = StreamReadFn(read_backend) - close = cb_info.contents.close = StreamCloseFn(lambda _userdata: frontend.close()) + + def close_backend(_userdata): + with self._enqueue_exceptions(): + del self._stream_protocol_frontends[proto][uri] + if hasattr(frontend, 'close'): + frontend.close() + close = cb_info.contents.close = StreamCloseFn(close_backend) seek, size, cancel = None, None, None - if hasattr(frontend, 'seek'): - seek = cb_info.contents.seek = StreamSeekFn(lambda _userdata, offx: frontend.seek(offx)) - if hasattr(frontend, 'size') and frontend.size is not None: - size = cb_info.contents.size = StreamSizeFn(lambda _userdata: frontend.size) - if hasattr(frontend, 'cancel'): - cancel = cb_info.contents.cancel = StreamCancelFn(lambda _userdata: frontend.cancel()) - # keep frontend and callbacks in memory forever (TODO) + if hasattr(frontend, 'seek'): + def seek_backend(_userdata, offx): + with self._enqueue_exceptions(): + return frontend.seek(offx) + seek = cb_info.contents.seek = StreamSeekFn(seek_backend) + + if hasattr(frontend, 'size') and frontend.size is not None: + def size_backend(_userdata): + with self._enqueue_exceptions(): + return frontend.size + size = cb_info.contents.size = StreamSizeFn(size_backend) + + if hasattr(frontend, 'cancel'): + def cancel_backend(_userdata): + with self._enqueue_exceptions(): + frontend.cancel() + cancel = cb_info.contents.cancel = StreamCancelFn(cancel_backend) + + # keep frontend and callbacks in memory until closed frontend._registered_callbacks = [read, close, seek, size, cancel] self._stream_protocol_frontends[proto][uri] = frontend return 0 if proto in self._stream_protocol_cbs: raise KeyError('Stream protocol already registered') + # keep backend in memory forever self._stream_protocol_cbs[proto] = [open_backend] _mpv_stream_cb_add_ro(self.handle, proto.encode('utf-8'), c_void_p(), open_backend) From 7343604f10d8e057d084955d4620815cc1b49f2c Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 26 Feb 2023 20:15:27 +0100 Subject: [PATCH 39/71] Add tests and fix error handling for stream callbacks --- mpv.py | 28 ++++++++---- tests/test_mpv.py | 109 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 9 deletions(-) diff --git a/mpv.py b/mpv.py index 6918f97..350456c 100644 --- a/mpv.py +++ b/mpv.py @@ -898,10 +898,13 @@ class MPV(object): try: yield except Exception as e: - try: - fut = next(iter(self._exception_futures)) - fut.set_exception(e) - except StopIteration: + for fut in self._exception_futures: + try: + fut.set_exception(e) + break + except InvalidStateError: + pass + else: warn(f'Unhandled exception on python-mpv event loop: {e}\n{traceback.format_exc()}', RuntimeWarning) def _loop(self): @@ -1087,7 +1090,6 @@ class MPV(object): @self.event_callback(*event_types) def target_handler(evt): - try: rv = cond(evt) if rv: @@ -1801,12 +1803,17 @@ class MPV(object): except ValueError: return ErrorCode.LOADING_FAILED except Exception as e: - try: - fut = next(iter(self._exception_futures)) - fut.set_exception(e) - except StopIteration: + for fut in self._exception_futures: + try: + fut.set_exception(e) + break + except InvalidStateError: + pass + else: warnings.warn(f'Unhandled exception {e} inside stream open callback for URI {uri}\n{traceback.format_exc()}') + + return ErrorCode.LOADING_FAILED cb_info.contents.cookie = None @@ -1817,6 +1824,7 @@ class MPV(object): for i in range(len(data)): buf[i] = data[i] return len(data) + return -1 read = cb_info.contents.read = StreamReadFn(read_backend) def close_backend(_userdata): @@ -1832,12 +1840,14 @@ class MPV(object): def seek_backend(_userdata, offx): with self._enqueue_exceptions(): return frontend.seek(offx) + return ErrorCode.GENERIC seek = cb_info.contents.seek = StreamSeekFn(seek_backend) if hasattr(frontend, 'size') and frontend.size is not None: def size_backend(_userdata): with self._enqueue_exceptions(): return frontend.size + return 0 size = cb_info.contents.size = StreamSizeFn(size_backend) if hasattr(frontend, 'cancel'): diff --git a/tests/test_mpv.py b/tests/test_mpv.py index 38b0222..5f1f979 100755 --- a/tests/test_mpv.py +++ b/tests/test_mpv.py @@ -535,6 +535,115 @@ class TestStreams(unittest.TestCase): m.terminate() disp.stop() + def test_stream_open_exception(self): + disp = Xvfb() + disp.start() + m = mpv.MPV(vo=testvo, video=False) + + @m.register_stream_protocol('raiseerror') + def open_fn(uri): + raise SystemError() + + waiting = threading.Semaphore() + result = Future() + def run(): + result.set_running_or_notify_cancel() + try: + waiting.release() + m.wait_for_playback() + result.set_result(False) + except SystemError: + result.set_result(True) + except Exception: + result.set_result(False) + + t = threading.Thread(target=run, daemon=True) + t.start() + + with waiting: + time.sleep(0.2) + m.play('raiseerror://foo') + + m.wait_for_playback(catch_errors=False) + try: + assert result.result() + finally: + m.terminate() + disp.stop() + + def test_python_stream_exception(self): + disp = Xvfb() + disp.start() + m = mpv.MPV(vo=testvo) + + @m.python_stream('foo') + def foo_gen(): + with open(TESTVID, 'rb') as f: + yield f.read(100) + raise SystemError() + + waiting = threading.Semaphore() + result = Future() + def run(): + result.set_running_or_notify_cancel() + try: + waiting.release() + m.wait_for_playback() + result.set_result(False) + except SystemError: + result.set_result(True) + except Exception: + result.set_result(False) + + t = threading.Thread(target=run, daemon=True) + t.start() + + with waiting: + time.sleep(0.2) + m.play('python://foo') + + m.wait_for_playback(catch_errors=False) + try: + assert result.result() + finally: + m.terminate() + disp.stop() + + def test_stream_open_forward(self): + disp = Xvfb() + disp.start() + m = mpv.MPV(vo=testvo, video=False) + + @m.register_stream_protocol('raiseerror') + def open_fn(uri): + raise ValueError() + + waiting = threading.Semaphore() + result = Future() + def run(): + result.set_running_or_notify_cancel() + try: + waiting.release() + m.wait_for_playback() + result.set_result(True) + except Exception: + result.set_result(False) + + t = threading.Thread(target=run, daemon=True) + t.start() + + with waiting: + time.sleep(0.2) + m.play('raiseerror://foo') + + m.wait_for_playback(catch_errors=False) + try: + assert result.result() + finally: + m.terminate() + disp.stop() + + class TestLifecycle(unittest.TestCase): def test_create_destroy(self): From d34b6252f7c3a02e9bcde8f690c0c8e941ef898d Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 27 Feb 2023 10:26:42 +0100 Subject: [PATCH 40/71] Move to new old pypi project name --- README.rst | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 1f61efe..fdf220e 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ Installation .. code:: bash - pip install python-mpv + pip install mpv ...though you can also realistically just copy `mpv.py`_ into your project as it's all nicely contained in one file. diff --git a/pyproject.toml b/pyproject.toml index 769a2c5..e5cd96b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" py-modules = ['mpv'] [project] -name = "python-mpv" +name = "mpv" version = "v1.0.2" description = "A python interface to the mpv media player" readme = "README.rst" From 80f5ea64a1313698ba1b1e555d63f30f17ce935c Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 27 Feb 2023 10:30:27 +0100 Subject: [PATCH 41/71] Version v1.0.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e5cd96b..6d1bc3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ py-modules = ['mpv'] [project] name = "mpv" -version = "v1.0.2" +version = "v1.0.3" description = "A python interface to the mpv media player" readme = "README.rst" authors = [{name = "jaseg", email = "mpv@jaseg.de"}] From 5a1a2734f2238cd201b4c94d878eb9daf31c3b63 Mon Sep 17 00:00:00 2001 From: jaseg Date: Thu, 30 Mar 2023 13:57:01 +0200 Subject: [PATCH 42/71] Add __version__ field to module Closes #248. --- do_release.sh | 11 +++++++++-- mpv.py | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/do_release.sh b/do_release.sh index df301f2..fb50fcb 100755 --- a/do_release.sh +++ b/do_release.sh @@ -3,6 +3,12 @@ [ $# -eq 1 ] || exit 2 VER="$1" + +echo "$VER" | grep '^[0-9]\+\.[0-9]\+\.[0-9]\+$' || { + echo "Call this script as ./do_release.sh [version] where version has format 1.2.3, without \"v\" prefix." + exit 2 +} + echo "Creating version $VER" if [ -n "$(git diff --name-only --cached)" ]; then @@ -10,8 +16,9 @@ if [ -n "$(git diff --name-only --cached)" ]; then exit 2 fi -sed -i "s/^\\(\\s*version\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*\\)$/\\1"$VER"\\2/" pyproject.toml -git add pyproject.toml +sed -i "s/^\\(\\s*version\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*\\)$/\\1v"$VER"\\2/" pyproject.toml +sed -i "s/^\\(\\s*version\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*\\)$/\\1"$VER"\\2/" mpv.py +git add pyproject.toml mpv.py git commit -m "Version $VER" --no-edit git -c user.signingkey=E36F75307F0A0EC2D145FF5CED7A208EEEC76F2D -c user.email=python-mpv@jaseg.de tag -s "v$VER" -m "Version $VER" git push --tags origin diff --git a/mpv.py b/mpv.py index 350456c..0de35a4 100644 --- a/mpv.py +++ b/mpv.py @@ -17,6 +17,8 @@ # # You can find copies of the GPLv2 and LGPLv2.1 licenses in the project repository's LICENSE.GPL and LICENSE.LGPL files. +__version__ = '1.0.3' + from ctypes import * import ctypes.util import threading From 3036f64b68207d74a06efcfbc2fdac91a6a70bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Sun, 30 Apr 2023 17:02:29 +0200 Subject: [PATCH 43/71] Replace xvfbwrapper with PyVirtualDisplay Fixes #249 --- pyproject.toml | 2 +- tests/requirements.txt | 2 +- tests/test_mpv.py | 32 ++++++++++++++++---------------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6d1bc3d..a72cd8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,4 +36,4 @@ homepage = "https://github.com/jaseg/python-mpv" [project.optional-dependencies] screenshot_raw = ["Pillow"] -test = ['xvfbwrapper'] +test = ['PyVirtualDisplay'] diff --git a/tests/requirements.txt b/tests/requirements.txt index 4a9b79c..ded363c 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,2 +1,2 @@ -xvfbwrapper>=0.2.9 +PyVirtualDisplay>=3.0 pytest>=7.1.2 diff --git a/tests/test_mpv.py b/tests/test_mpv.py index 5f1f979..74999ad 100755 --- a/tests/test_mpv.py +++ b/tests/test_mpv.py @@ -31,11 +31,11 @@ import mpv if os.name == 'nt': - Xvfb = mock.Mock() + Display = mock.Mock() testvo='gpu' else: - from xvfbwrapper import Xvfb + from pyvirtualdisplay import Display testvo='x11' @@ -54,7 +54,7 @@ def timed_print(): class MpvTestCase(unittest.TestCase): def setUp(self): - self.disp = Xvfb() + self.disp = Display() self.disp.start() self.m = mpv.MPV(vo=testvo, loglevel='debug', log_handler=timed_print()) @@ -444,7 +444,7 @@ class TestStreams(unittest.TestCase): def test_python_stream(self): handler = mock.Mock() - disp = Xvfb() + disp = Display() disp.start() m = mpv.MPV(vo=testvo) def cb(evt): @@ -502,7 +502,7 @@ class TestStreams(unittest.TestCase): stream_mock.seek = mock.Mock(return_value=0) stream_mock.read = mock.Mock(return_value=b'') - disp = Xvfb() + disp = Display() disp.start() m = mpv.MPV(vo=testvo, video=False) def cb(evt): @@ -536,7 +536,7 @@ class TestStreams(unittest.TestCase): disp.stop() def test_stream_open_exception(self): - disp = Xvfb() + disp = Display() disp.start() m = mpv.MPV(vo=testvo, video=False) @@ -572,7 +572,7 @@ class TestStreams(unittest.TestCase): disp.stop() def test_python_stream_exception(self): - disp = Xvfb() + disp = Display() disp.start() m = mpv.MPV(vo=testvo) @@ -610,7 +610,7 @@ class TestStreams(unittest.TestCase): disp.stop() def test_stream_open_forward(self): - disp = Xvfb() + disp = Display() disp.start() m = mpv.MPV(vo=testvo, video=False) @@ -692,7 +692,7 @@ class TestLifecycle(unittest.TestCase): handler.assert_not_called() def test_wait_for_property_negative(self): - self.disp = Xvfb() + self.disp = Display() self.disp.start() m = mpv.MPV(vo=testvo) m.play(TESTVID) @@ -715,7 +715,7 @@ class TestLifecycle(unittest.TestCase): assert result.result() def test_wait_for_property_positive(self): - self.disp = Xvfb() + self.disp = Display() self.disp.start() handler = mock.Mock() m = mpv.MPV(vo=testvo) @@ -735,7 +735,7 @@ class TestLifecycle(unittest.TestCase): self.disp.stop() def test_wait_for_event(self): - self.disp = Xvfb() + self.disp = Display() self.disp.start() m = mpv.MPV(vo=testvo) m.play(TESTVID) @@ -757,7 +757,7 @@ class TestLifecycle(unittest.TestCase): assert result.result() def test_wait_for_property_shutdown(self): - self.disp = Xvfb() + self.disp = Display() self.disp.start() m = mpv.MPV(vo=testvo) m.play(TESTVID) @@ -771,7 +771,7 @@ class TestLifecycle(unittest.TestCase): @unittest.skipIf('test_wait_for_property_event_overflow' in SKIP_TESTS, reason="kills X-Server first") def test_wait_for_property_event_overflow(self): - self.disp = Xvfb() + self.disp = Display() self.disp.start() m = mpv.MPV(vo=testvo) m.play(TESTVID) @@ -792,7 +792,7 @@ class TestLifecycle(unittest.TestCase): self.disp.stop() def test_wait_for_event_shutdown(self): - self.disp = Xvfb() + self.disp = Display() self.disp.start() m = mpv.MPV(vo=testvo) m.play(TESTVID) @@ -802,7 +802,7 @@ class TestLifecycle(unittest.TestCase): self.disp.stop() def test_wait_for_shutdown(self): - self.disp = Xvfb() + self.disp = Display() self.disp.start() m = mpv.MPV(vo=testvo) m.play(TESTVID) @@ -814,7 +814,7 @@ class TestLifecycle(unittest.TestCase): def test_log_handler(self): handler = mock.Mock() - self.disp = Xvfb() + self.disp = Display() self.disp.start() m = mpv.MPV(vo=testvo, log_handler=handler) m.play(TESTVID) From f6d1269531de44f8f92fa0b03a92d82089f266ce Mon Sep 17 00:00:00 2001 From: sdaqo Date: Mon, 1 May 2023 15:03:10 +0200 Subject: [PATCH 44/71] Add 'self' to mouse function --- mpv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpv.py b/mpv.py index 0de35a4..18dec07 100644 --- a/mpv.py +++ b/mpv.py @@ -1448,7 +1448,7 @@ class MPV(object): """Mapped mpv discnav command, see man mpv(1).""" self.command('discnav', command) - def mouse(x, y, button=None, mode='single'): + def mouse(self, x, y, button=None, mode='single'): """Mapped mpv mouse command, see man mpv(1).""" if button is None: self.command('mouse', x, y, mode) From 2d787a977dc46ef94e5c569dab4358835891ca25 Mon Sep 17 00:00:00 2001 From: Carsen Yates Date: Mon, 3 Jul 2023 08:58:19 -0700 Subject: [PATCH 45/71] Add field "playlist_entry_id" to MpvEventEndFile --- mpv.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mpv.py b/mpv.py index 18dec07..51eaae8 100644 --- a/mpv.py +++ b/mpv.py @@ -443,9 +443,12 @@ class MpvEventLogMessage(Structure): return lazy_decoder(self._text) class MpvEventEndFile(Structure): - _fields_ = [('reason', c_int), - ('error', c_int)] - + _fields_ = [ + ('reason', c_int), + ('error', c_int), + ('playlist_entry_id', c_ulonglong), + ] + EOF = 0 RESTARTED = 1 ABORTED = 2 From 03947f272b63a0cfacd5c9163198436bcc17f8a6 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 22 Jul 2023 14:37:52 +0200 Subject: [PATCH 46/71] Add missing fields to MpvEventEndFile --- mpv.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mpv.py b/mpv.py index 51eaae8..27fed4f 100644 --- a/mpv.py +++ b/mpv.py @@ -447,6 +447,8 @@ class MpvEventEndFile(Structure): ('reason', c_int), ('error', c_int), ('playlist_entry_id', c_ulonglong), + ('playlist_insert_id', c_ulonglong), + ('playlist_insert_num_entries', c_int), ] EOF = 0 From fa3b0fdad1f4ed4367f0085a301186fa6f9be51c Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 22 Jul 2023 16:03:55 +0200 Subject: [PATCH 47/71] Version 1.0.4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a72cd8d..03c8c4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ py-modules = ['mpv'] [project] name = "mpv" -version = "v1.0.3" +version = "v1.0.4" description = "A python interface to the mpv media player" readme = "README.rst" authors = [{name = "jaseg", email = "mpv@jaseg.de"}] From 0bfd64d1f1923ca1039ca34f3ff9f8830bc47102 Mon Sep 17 00:00:00 2001 From: naglis <827324+naglis@users.noreply.github.com> Date: Tue, 22 Aug 2023 16:09:02 +0300 Subject: [PATCH 48/71] Fix typo in README --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index fdf220e..3876dd0 100644 --- a/README.rst +++ b/README.rst @@ -70,7 +70,7 @@ Threading ~~~~~~~~~ The ``mpv`` module starts one thread for event handling, since MPV sends events that must be processed quickly. The -event queue has a fixed maxmimum size and some operations can cause a large number of events to be sent. +event queue has a fixed maximum size and some operations can cause a large number of events to be sent. If you want to handle threading yourself, you can pass ``start_event_thread=False`` to the ``MPV`` constructor and manually call the ``MPV`` object's ``_loop`` function. If you have some strong need to not use threads and use some From 93cf60e109b592fcca07f98864d05e5503c59d05 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 18 Nov 2023 11:44:55 +0100 Subject: [PATCH 49/71] Add libmpv-2.dll to windows library search list --- mpv.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/mpv.py b/mpv.py index 27fed4f..ac024b7 100644 --- a/mpv.py +++ b/mpv.py @@ -35,12 +35,9 @@ import traceback if os.name == 'nt': # Note: mpv-2.dll with API version 2 corresponds to mpv v0.35.0. Most things should work with the fallback, too. - dll = ctypes.util.find_library('mpv-2.dll') or ctypes.util.find_library('mpv-1.dll') + dll = ctypes.util.find_library('mpv-2.dll') or ctypes.util.find_library('libmpv-2.dll') or ctypes.util.find_library('mpv-1.dll') if dll is None: - raise OSError('Cannot find mpv-1.dll or mpv-2.dll in your system %PATH%. One way to deal with this is to ship ' - 'the dll with your script and put the directory your script is in into %PATH% before ' - '"import mpv": os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] ' - 'If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].') + raise OSError('Cannot find mpv-1.dll, mpv-2.dll or libmpv-2.dll in your system %PATH%. One way to deal with this is to ship the dll with your script and put the directory your script is in into %PATH% before "import mpv": os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].') backend = CDLL(dll) fs_enc = 'utf-8' else: @@ -52,10 +49,7 @@ else: sofile = ctypes.util.find_library('mpv') if sofile is None: - raise OSError("Cannot find libmpv in the usual places. Depending on your distro, you may try installing an " - "mpv-devel or mpv-libs package. If you have libmpv around but this script can't find it, consult " - "the documentation for ctypes.util.find_library which this script uses to look up the library " - "filename.") + raise OSError("Cannot find libmpv in the usual places. Depending on your distro, you may try installing an mpv-devel or mpv-libs package. If you have libmpv around but this script can't find it, consult the documentation for ctypes.util.find_library which this script uses to look up the library filename.") backend = CDLL(sofile) fs_enc = sys.getfilesystemencoding() From b42f79a4f1be505d95be7fd13333ff96fa624aa4 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 18 Nov 2023 11:57:02 +0100 Subject: [PATCH 50/71] Make release script properly update version in mpv.py --- do_release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/do_release.sh b/do_release.sh index fb50fcb..b1ae0f3 100755 --- a/do_release.sh +++ b/do_release.sh @@ -17,7 +17,7 @@ if [ -n "$(git diff --name-only --cached)" ]; then fi sed -i "s/^\\(\\s*version\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*\\)$/\\1v"$VER"\\2/" pyproject.toml -sed -i "s/^\\(\\s*version\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*\\)$/\\1"$VER"\\2/" mpv.py +sed -i "s/^\\(\\s*__version__\\s*=\\s*['\"]\\)[^'\"]*\\(['\"]\\s*\\)$/\\1"$VER"\\2/" mpv.py git add pyproject.toml mpv.py git commit -m "Version $VER" --no-edit git -c user.signingkey=E36F75307F0A0EC2D145FF5CED7A208EEEC76F2D -c user.email=python-mpv@jaseg.de tag -s "v$VER" -m "Version $VER" From c52a07e86279f8a892aee4b2adae2a03481f7bce Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 18 Nov 2023 11:57:40 +0100 Subject: [PATCH 51/71] Version 1.0.5 --- mpv.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mpv.py b/mpv.py index ac024b7..b2f6868 100644 --- a/mpv.py +++ b/mpv.py @@ -17,7 +17,7 @@ # # You can find copies of the GPLv2 and LGPLv2.1 licenses in the project repository's LICENSE.GPL and LICENSE.LGPL files. -__version__ = '1.0.3' +__version__ = '1.0.5' from ctypes import * import ctypes.util diff --git a/pyproject.toml b/pyproject.toml index 03c8c4f..49de9d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ py-modules = ['mpv'] [project] name = "mpv" -version = "v1.0.4" +version = "v1.0.5" description = "A python interface to the mpv media player" readme = "README.rst" authors = [{name = "jaseg", email = "mpv@jaseg.de"}] From 141ec7d372c8d536c208c39bb89132d69aa1b36c Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 22 Jan 2024 11:48:47 +0100 Subject: [PATCH 52/71] mpv.py: Add play_context convenience function --- mpv.py | 37 +++++++++++++++++++++++++++++++++++++ tests/test_mpv.py | 20 ++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/mpv.py b/mpv.py index b2f6868..19f3581 100644 --- a/mpv.py +++ b/mpv.py @@ -1944,6 +1944,43 @@ class MPV(object): return cb return register + @contextmanager + def play_context(self): + """ Context manager for streaming bytes straight into libmpv. + + This is a convenience wrapper around python_stream. play_context returns a write method, which you can use in + the body of the context manager to feed libmpv bytes. All bytes you feed in with write() in the body of a single + call of this context manager are treated as one single file. A queue is used internally, so this function is + thread-safe. The queue is unlimited, so it cannot block and is safe to call from async code. You can use this + function to stream chunked data, e.g. from the network. + + Use it like this: + + with m.play_context() as write: + with open(TESTVID, 'rb') as f: + while (chunk := f.read(65536)): # Get some chunks of bytes + write(chunk) + """ + q = queue.Queue() + + frame = sys._getframe() + stream_name = f'__python_mpv_play_generator_{hash(frame)}' + EOF = frame # Get some unique object as EOF marker + @self.python_stream(stream_name) + def reader(): + while (chunk := q.get()) is not EOF: + if chunk: + yield chunk + reader.unregister() + + def write(chunk): + q.put(chunk) + + # Start playback before yielding, the first call to reader() will block until write is called at least once. + self.play(f'python://{stream_name}') + yield write + q.put(EOF) + def python_stream_catchall(self, cb): """ Register a catch-all python stream to be called when no name matches can be found. Use this decorator on a function that takes a name argument and returns a (generator, size) tuple (with size being None if unknown). diff --git a/tests/test_mpv.py b/tests/test_mpv.py index 74999ad..9cc688d 100755 --- a/tests/test_mpv.py +++ b/tests/test_mpv.py @@ -643,6 +643,26 @@ class TestStreams(unittest.TestCase): m.terminate() disp.stop() + def test_play_context(self): + handler = mock.Mock() + + disp = Display() + disp.start() + m = mpv.MPV(vo=testvo) + def cb(evt): + handler(evt.as_dict(decoder=mpv.lazy_decoder)) + m.register_event_callback(cb) + + with m.play_context() as write: + with open(TESTVID, 'rb') as f: + write(f.read(100)) + write(f.read(1000)) + write(f.read()) + + m.wait_for_playback() + handler.assert_any_call({'event': 'end-file', 'reason': 'eof', 'playlist_entry_id': 1}) + m.terminate() + disp.stop() class TestLifecycle(unittest.TestCase): From 4e76f01ecc4c55a8dfd923a44bebd671907f944b Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 22 Jan 2024 11:52:42 +0100 Subject: [PATCH 53/71] mpv.py: Add play_bytes convenience function --- mpv.py | 11 +++++++++++ tests/test_mpv.py | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/mpv.py b/mpv.py index 19f3581..1efdeb1 100644 --- a/mpv.py +++ b/mpv.py @@ -1981,6 +1981,17 @@ class MPV(object): yield write q.put(EOF) + def play_bytes(self, data): + frame = sys._getframe() + stream_name = f'__python_mpv_play_generator_{hash(frame)}' + + @self.python_stream(stream_name) + def reader(): + yield data + reader.unregister() # unregister itself + + self.play(f'python://{stream_name}') + def python_stream_catchall(self, cb): """ Register a catch-all python stream to be called when no name matches can be found. Use this decorator on a function that takes a name argument and returns a (generator, size) tuple (with size being None if unknown). diff --git a/tests/test_mpv.py b/tests/test_mpv.py index 9cc688d..0935a32 100755 --- a/tests/test_mpv.py +++ b/tests/test_mpv.py @@ -664,6 +664,24 @@ class TestStreams(unittest.TestCase): m.terminate() disp.stop() + def test_play_bytes(self): + handler = mock.Mock() + + disp = Display() + disp.start() + m = mpv.MPV(vo=testvo) + def cb(evt): + handler(evt.as_dict(decoder=mpv.lazy_decoder)) + m.register_event_callback(cb) + + with open(TESTVID, 'rb') as f: + m.play_bytes(f.read()) + + m.wait_for_playback() + handler.assert_any_call({'event': 'end-file', 'reason': 'eof', 'playlist_entry_id': 1}) + m.terminate() + disp.stop() + class TestLifecycle(unittest.TestCase): def test_create_destroy(self): From b6ab5a4ab00ee4fff35e50963732bcf87dd5329f Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 22 Jan 2024 11:55:03 +0100 Subject: [PATCH 54/71] play_bytes: Add docstring. --- mpv.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mpv.py b/mpv.py index 1efdeb1..a4ea245 100644 --- a/mpv.py +++ b/mpv.py @@ -1982,6 +1982,7 @@ class MPV(object): q.put(EOF) def play_bytes(self, data): + """ Play the given bytes object as a single file. """ frame = sys._getframe() stream_name = f'__python_mpv_play_generator_{hash(frame)}' From d26f801cecc7a87b88ab2687228ce3c540d8e998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Mon, 29 Jan 2024 16:52:42 +0100 Subject: [PATCH 55/71] Update CI pipeline (#271) * Trigger on pull request * run on ubuntu-latest * run on 3.11 + 3.12 also * upgrade actions * upgrade libmpv * use libmpv-v3 on Windows * disable fail-fast behavior * replace pytest with unittest Using pytest, we run into access violation errors on Windows. They disappear when we use the unittest module. --- .github/workflows/tests.yml | 30 +++++++++++++++++++----------- tests/requirements.txt | 1 - 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8a0d066..52eb072 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,6 +4,8 @@ name: 'Tests' on: push: branches: [ '**' ] + pull_request: + branches: [ '**' ] defaults: @@ -13,19 +15,20 @@ defaults: jobs: test-linux: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest name: 'Linux - Python' strategy: matrix: - python-version: [ '3.8', '3.9', '3.10' ] + python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] + fail-fast: false env: DISPLAY: :0 PY_MPV_SKIP_TESTS: >- test_wait_for_property_event_overflow steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: 'Install Python' - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: 'Update Packages' @@ -57,30 +60,35 @@ jobs: function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } execute source venv/bin/activate - execute xvfb-run python -m pytest + execute xvfb-run python -m unittest test-windows: runs-on: windows-latest name: 'Windows - Python' strategy: matrix: - python-version: [ '3.8', '3.9', '3.10' ] + python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] + fail-fast: false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: 'Install Python' - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: 'Provide libmpv' run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } - ARTIFACT="mpv-dev-x86_64-20220619-git-c1a46ec.7z" + ARTIFACT="mpv-dev-x86_64-v3-20240121-git-a39f9b6.7z" + SHA1="0764a4b899a7ebb1476e5b491897c5e2eed8a07f" URL="https://sourceforge.net/projects/mpv-player-windows/files/libmpv/$ARTIFACT" execute curl -L -O "$URL" + echo -e "\033[0;34mecho -n $SHA1 $ARTIFACT > $ARTIFACT.sha1\033[0m" + echo -n "$SHA1 $ARTIFACT" > "$ARTIFACT.sha1" + execute sha1sum --check "$ARTIFACT.sha1" execute 7z x "$ARTIFACT" - execute mv mpv-2.dll tests + execute mv libmpv-2.dll tests - name: 'Setup Test Environment' run: | function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } @@ -95,4 +103,4 @@ jobs: function execute() { echo -e "\033[0;34m$*\033[0m"; "$@"; } execute source venv/Scripts/activate - execute python -m pytest + execute python -m unittest diff --git a/tests/requirements.txt b/tests/requirements.txt index ded363c..eabe145 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,2 +1 @@ PyVirtualDisplay>=3.0 -pytest>=7.1.2 From d96eaf7e64f1063ae16f88abfaa14c2997b3291d Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 20 Apr 2024 12:48:58 +0200 Subject: [PATCH 56/71] Fix loadfile for mpv v0.38.0 mpv v0.38.0 added an argument to the loadfile command. Unfortunately the parsing logic isn't very smart, and now mis-interprets the old argument format, and breaks literally everything written against older versions that used the `options` kv dict. This commit adds a kludge that uses the right variant depending on the mpv version. --- mpv.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/mpv.py b/mpv.py index a4ea245..9297004 100644 --- a/mpv.py +++ b/mpv.py @@ -893,6 +893,8 @@ class MPV(object): self._event_thread.start() else: self._event_thread = None + if (m := re.search(r'(\d+)\.(\d+)\.(\d+)', self.mpv_version)): + self.mpv_version_tuple = tuple(map(int, m.groups())) @contextmanager def _enqueue_exceptions(self): @@ -1324,9 +1326,16 @@ class MPV(object): def _encode_options(options): return ','.join('{}={}'.format(_py_to_mpv(str(key)), str(val)) for key, val in options.items()) - def loadfile(self, filename, mode='replace', **options): + def loadfile(self, filename, mode='replace', index=None, **options): """Mapped mpv loadfile command, see man mpv(1).""" - self.command('loadfile', filename.encode(fs_enc), mode, MPV._encode_options(options)) + if self.mpv_version_tuple >= (0, 38, 0): + if index is None: + index = -1 + self.command('loadfile', filename.encode(fs_enc), mode, index, MPV._encode_options(options)) + else: + if index is not None: + warn(f'The index argument to the loadfile command is only supported on mpv >= 0.38.0') + self.command('loadfile', filename.encode(fs_enc), mode, MPV._encode_options(options)) def loadlist(self, playlist, mode='replace'): """Mapped mpv loadlist command, see man mpv(1).""" From 16093073b058cd2bc67ba3de857f2b4f187c32e1 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 20 Apr 2024 12:51:15 +0200 Subject: [PATCH 57/71] Version 1.0.6 --- mpv.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mpv.py b/mpv.py index 9297004..9856dc2 100644 --- a/mpv.py +++ b/mpv.py @@ -17,7 +17,7 @@ # # You can find copies of the GPLv2 and LGPLv2.1 licenses in the project repository's LICENSE.GPL and LICENSE.LGPL files. -__version__ = '1.0.5' +__version__ = '1.0.6' from ctypes import * import ctypes.util diff --git a/pyproject.toml b/pyproject.toml index 49de9d8..b15441f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ py-modules = ['mpv'] [project] name = "mpv" -version = "v1.0.5" +version = "v1.0.6" description = "A python interface to the mpv media player" readme = "README.rst" authors = [{name = "jaseg", email = "mpv@jaseg.de"}] From 4dcfba9d40d99ec95a02fdce3349a29f10fb8917 Mon Sep 17 00:00:00 2001 From: jaseg <136313+jaseg@users.noreply.github.com> Date: Thu, 16 May 2024 13:35:47 +0200 Subject: [PATCH 58/71] README: Clarify Python version support --- README.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 3876dd0..1341efc 100644 --- a/README.rst +++ b/README.rst @@ -32,12 +32,10 @@ On Windows you can place libmpv anywhere in your ``%PATH%`` (e.g. next to ``pyth into ctypes, which is different to the one Windows uses internally. Consult `this stackoverflow post `__ for details. -Python >= 3.7 (officially) -.......................... -The ``main`` branch officially only supports recent python releases (3.5 onwards), but there is the somewhat outdated -but functional `py2compat branch`_ providing Python 2 compatibility. +Python >= 3.9 +............. +We only support python stable releases from the last couple of years. We only test the current stable python release. If you find a compatibility issue with an older python version that still has upstream support (that is less than about four years old), feel free to open an issue_ and we'll have a look. -.. _`py2compat branch`: https://github.com/jaseg/python-mpv/tree/py2compat .. _`issue`: https://github.com/jaseg/python-mpv/issues .. _`pull request`: https://github.com/jaseg/python-mpv/pulls From 5bb298ad11b7ad7dbeeddfd87546c0d34b28397c Mon Sep 17 00:00:00 2001 From: jaseg <136313+jaseg@users.noreply.github.com> Date: Thu, 16 May 2024 13:36:49 +0200 Subject: [PATCH 59/71] pyproject.toml: Update python version classifiers, bump min to 3.9 --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b15441f..5aff76a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ description = "A python interface to the mpv media player" readme = "README.rst" authors = [{name = "jaseg", email = "mpv@jaseg.de"}] license = {text = "GPLv2+ or LGPLv2.1+"} -requires-python = ">=3.7" +requires-python = ">=3.9" keywords = ['mpv', 'library', 'video', 'audio', 'player', 'display', 'multimedia'] classifiers = [ 'Development Status :: 5 - Production/Stable', @@ -23,10 +23,10 @@ classifiers = [ 'Natural Language :: English', 'Operating System :: POSIX', 'Programming Language :: C', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Multimedia :: Sound/Audio :: Players', 'Topic :: Multimedia :: Video :: Display' ] From f1621b629df60c29945392b5739ac97f33e31bdf Mon Sep 17 00:00:00 2001 From: jaseg Date: Fri, 21 Jun 2024 16:14:37 +0200 Subject: [PATCH 60/71] Fix race condition in property observer code leading to futures.InvalidStateError Previously, prepare_and_wait_for_property was slightly confused on the lifetime of that future. This closes #282 --- mpv.py | 33 +++++++++++++++++++-------------- tests/test_mpv.py | 22 +++++++++++++++++++++- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/mpv.py b/mpv.py index 9856dc2..75567c1 100644 --- a/mpv.py +++ b/mpv.py @@ -1036,30 +1036,38 @@ class MPV(object): rv = cond(val) if rv: result.set_result(rv) + + except InvalidStateError: + pass + except Exception as e: try: result.set_exception(e) - except InvalidStateError: + except: pass - except InvalidStateError: - pass - self.observe_property(name, observer) - err_unregister = self._set_error_handler(result) try: result.set_running_or_notify_cancel() + + self.observe_property(name, observer) + err_unregister = self._set_error_handler(result) if catch_errors: self._exception_futures.add(result) yield result - rv = cond(getattr(self, name.replace('-', '_'))) - if level_sensitive and rv: - result.set_result(rv) + if level_sensitive: + rv = cond(getattr(self, name.replace('-', '_'))) + if rv: + result.set_result(rv) + return + + self.check_core_alive() + result.result(timeout) + + except InvalidStateError: + pass - else: - self.check_core_alive() - result.result(timeout) finally: err_unregister() self.unobserve_property(name, observer) @@ -1821,9 +1829,6 @@ class MPV(object): pass else: warnings.warn(f'Unhandled exception {e} inside stream open callback for URI {uri}\n{traceback.format_exc()}') - - - return ErrorCode.LOADING_FAILED cb_info.contents.cookie = None diff --git a/tests/test_mpv.py b/tests/test_mpv.py index 0935a32..6a962f0 100755 --- a/tests/test_mpv.py +++ b/tests/test_mpv.py @@ -23,7 +23,7 @@ from contextlib import contextmanager import os.path import os import time -from concurrent.futures import Future +from concurrent.futures import Future, InvalidStateError os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] @@ -915,6 +915,25 @@ class CommandTests(MpvTestCase): class RegressionTests(MpvTestCase): + def test_wait_for_property_concurrency(self): + players = [mpv.MPV(vo=testvo, loglevel='debug', log_handler=timed_print()) for i in range(2)] + + try: + for _ in range(150): + for player in players: + player.play('tests/test.webm') + for player in players: + player.wait_for_property('seekable') + for player in players: + player.seek(0, reference='absolute', precision='exact') + + except InvalidStateError: + self.fail('InvalidStateError thrown from wait_for_property') + + finally: + for player in players: + player.terminate() + def test_unobserve_property_runtime_error(self): """ Ensure a `RuntimeError` is not thrown within @@ -966,3 +985,4 @@ class RegressionTests(MpvTestCase): m.slang = 'ru' m.terminate() # needed for synchronization of event thread handler.assert_has_calls([mock.call('slang', ['jp']), mock.call('slang', ['ru'])]) + From e1ae4f7da6d1858ab5af7d003c24fd6ca1f8fed5 Mon Sep 17 00:00:00 2001 From: jaseg Date: Fri, 21 Jun 2024 16:23:54 +0200 Subject: [PATCH 61/71] Version 1.0.7 --- mpv.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mpv.py b/mpv.py index 75567c1..8941aed 100644 --- a/mpv.py +++ b/mpv.py @@ -17,7 +17,7 @@ # # You can find copies of the GPLv2 and LGPLv2.1 licenses in the project repository's LICENSE.GPL and LICENSE.LGPL files. -__version__ = '1.0.6' +__version__ = '1.0.7' from ctypes import * import ctypes.util diff --git a/pyproject.toml b/pyproject.toml index 5aff76a..dbea61f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ py-modules = ['mpv'] [project] name = "mpv" -version = "v1.0.6" +version = "v1.0.7" description = "A python interface to the mpv media player" readme = "README.rst" authors = [{name = "jaseg", email = "mpv@jaseg.de"}] From f4086d4bb44603dcbaac51a162b280f760cbde2c Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 15 Jul 2024 14:52:59 +0200 Subject: [PATCH 62/71] Add API to set dict-valued properties --- mpv.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mpv.py b/mpv.py index 8941aed..94eeb85 100644 --- a/mpv.py +++ b/mpv.py @@ -2064,7 +2064,10 @@ class MPV(object): def _set_property(self, name, value): self.check_core_alive() ename = name.encode('utf-8') - if isinstance(value, (list, set, dict)): + if isinstance(value, dict): + _1, _2, _3, pointer = _make_node_str_map(value) + _mpv_set_property(self.handle, ename, MpvFormat.NODE, pointer) + elif isinstance(value, (list, set)): _1, _2, _3, pointer = _make_node_str_list(value) _mpv_set_property(self.handle, ename, MpvFormat.NODE, pointer) else: From 775fc4a868fa8a84b6e1830a42cba4847a72e961 Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 16 Jul 2024 11:10:36 +0200 Subject: [PATCH 63/71] Add test for dict-valued properties --- tests/test_mpv.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_mpv.py b/tests/test_mpv.py index 6a962f0..a59ade1 100755 --- a/tests/test_mpv.py +++ b/tests/test_mpv.py @@ -210,6 +210,11 @@ class TestProperties(MpvTestCase): # See comment in test_property_decoding_invalid_utf8 self.m.osd.alang + def test_dict_valued_property(self): + nasty_stuff = '\xe2\x80\x8e Mozilla/5.0 Foobar \xe2\x80\x8e \xe2\x80\x81' + self.m.ytdl_raw_options = {'user-agent': nasty_stuff} + self.assertEqual(self.m.ytdl_raw_options, {'user-agent': nasty_stuff}) + def test_option_read(self): self.m.loop = 'inf' self.m.play(TESTVID) From ef3f47c3ec8e276d4e25fd9ec2a7f06afd7df1ea Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 14 Aug 2024 10:48:37 +0200 Subject: [PATCH 64/71] Fix quit and quit_watch_later commands --- mpv.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mpv.py b/mpv.py index 94eeb85..fa6666f 100644 --- a/mpv.py +++ b/mpv.py @@ -1375,11 +1375,17 @@ class MPV(object): def quit(self, code=None): """Mapped mpv quit command, see man mpv(1).""" - self.command('quit', code) + if code is not None: + self.command('quit', code) + else: + self.command('quit') def quit_watch_later(self, code=None): """Mapped mpv quit_watch_later command, see man mpv(1).""" - self.command('quit_watch_later', code) + if code is not None: + self.command('quit_watch_later', code) + else: + self.command('quit_watch_later') def stop(self, keep_playlist=False): """Mapped mpv stop command, see man mpv(1).""" From 3d09f5199e73dd010b22e81709452cc0117d73e7 Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 14 Aug 2024 10:55:51 +0200 Subject: [PATCH 65/71] Windows: Improve DLL loading error messages --- mpv.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mpv.py b/mpv.py index fa6666f..d784db5 100644 --- a/mpv.py +++ b/mpv.py @@ -38,7 +38,15 @@ if os.name == 'nt': dll = ctypes.util.find_library('mpv-2.dll') or ctypes.util.find_library('libmpv-2.dll') or ctypes.util.find_library('mpv-1.dll') if dll is None: raise OSError('Cannot find mpv-1.dll, mpv-2.dll or libmpv-2.dll in your system %PATH%. One way to deal with this is to ship the dll with your script and put the directory your script is in into %PATH% before "import mpv": os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].') - backend = CDLL(dll) + # flags argument: LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR + # cf. https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexa + try: + backend = CDLL(dll, 0x00001000 | 0x00000100) + except Exception as e: + if not os.path.isabs(dll): + raise OSError(f'ctypes.find_library found mpv.dll at {dll}, but ctypes.CDLL could not load it. It looks like find_library found mpv.dll under a relative path entry in %PATH%. Please make sure all paths in %PATH% are absolute. Instead of trying to load mpv.dll from the current working directory, put it somewhere next to your script and add that path to %PATH% using os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"]') from e + else: + raise OSError(f'ctypes.find_library found mpv.dll at {dll}, but ctypes.CDLL could not load it.') from e fs_enc = 'utf-8' else: import locale From 16cd0b338e50419713b2d7bd8fea956cb593c303 Mon Sep 17 00:00:00 2001 From: jaseg Date: Wed, 14 Aug 2024 11:05:01 +0200 Subject: [PATCH 66/71] Windows: Look for mpv.dll next to mpv.py --- README.rst | 5 +++-- mpv.py | 26 +++++++++++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 1341efc..db57f22 100644 --- a/README.rst +++ b/README.rst @@ -29,8 +29,9 @@ submit a `pull request`_ on github. On Windows you can place libmpv anywhere in your ``%PATH%`` (e.g. next to ``python.exe``) or next to this module's ``mpv.py``. Before falling back to looking in the mpv module's directory, python-mpv uses the DLL search order built -into ctypes, which is different to the one Windows uses internally. Consult `this stackoverflow post -`__ for details. +into ctypes, which is different to the one Windows uses internally. You can modify `%PATH%` before importing python-mpv +to modify where python-mpv looks for the DLL. Consult `this stackoverflow post `__ +for details. Python >= 3.9 ............. diff --git a/mpv.py b/mpv.py index d784db5..cd843a2 100644 --- a/mpv.py +++ b/mpv.py @@ -2,7 +2,7 @@ # vim: ts=4 sw=4 et # # Python MPV library module -# Copyright (C) 2017-2022 Sebastian Götte +# Copyright (C) 2017-2024 Sebastian Götte # # python-mpv inherits the underlying libmpv's license, which can be either GPLv2 or later (default) or LGPLv2.1 or # later. For details, see the mpv copyright page here: https://github.com/mpv-player/mpv/blob/master/Copyright @@ -24,6 +24,7 @@ import ctypes.util import threading import queue import os +import os.path import sys from warnings import warn from functools import partial, wraps @@ -35,19 +36,30 @@ import traceback if os.name == 'nt': # Note: mpv-2.dll with API version 2 corresponds to mpv v0.35.0. Most things should work with the fallback, too. - dll = ctypes.util.find_library('mpv-2.dll') or ctypes.util.find_library('libmpv-2.dll') or ctypes.util.find_library('mpv-1.dll') - if dll is None: - raise OSError('Cannot find mpv-1.dll, mpv-2.dll or libmpv-2.dll in your system %PATH%. One way to deal with this is to ship the dll with your script and put the directory your script is in into %PATH% before "import mpv": os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].') - # flags argument: LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR - # cf. https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexa + names = ['mpv-2.dll', 'libmpv-2.dll', 'mpv-1.dll'] + for name in names: + dll = ctypes.util.find_library(name) + if dll: + break + else: + for name in names: + dll = os.path.join(os.path.dirname(__file__), name) + if os.path.isfile(dll): + break + else: + raise OSError('Cannot find mpv-1.dll, mpv-2.dll or libmpv-2.dll in your system %PATH%. One way to deal with this is to ship the dll with your script and put the directory your script is in into %PATH% before "import mpv": os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].') + try: + # flags argument: LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR + # cf. https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexa backend = CDLL(dll, 0x00001000 | 0x00000100) except Exception as e: - if not os.path.isabs(dll): + if not os.path.isabs(dll): # can only be find_library, not the "look next to mpv.py" thing raise OSError(f'ctypes.find_library found mpv.dll at {dll}, but ctypes.CDLL could not load it. It looks like find_library found mpv.dll under a relative path entry in %PATH%. Please make sure all paths in %PATH% are absolute. Instead of trying to load mpv.dll from the current working directory, put it somewhere next to your script and add that path to %PATH% using os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"]') from e else: raise OSError(f'ctypes.find_library found mpv.dll at {dll}, but ctypes.CDLL could not load it.') from e fs_enc = 'utf-8' + else: import locale lc, enc = locale.getlocale(locale.LC_NUMERIC) From e6e9313af38dc4e6f650ac893b712eaa058b0b5a Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 25 Aug 2024 13:45:13 +0200 Subject: [PATCH 67/71] Tests: Fix race condition in test_wait_for_property_concurrency --- tests/test_mpv.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_mpv.py b/tests/test_mpv.py index a59ade1..9c42332 100755 --- a/tests/test_mpv.py +++ b/tests/test_mpv.py @@ -49,7 +49,8 @@ def timed_print(): start_time = time.time() def do_print(level, prefix, text): td = time.time() - start_time - print('{:.3f} [{}] {}: {}'.format(td, level, prefix, text), flush=True) + print('{:.3f} [{}] {}: {}'.format(td, level, prefix, text.strip()), flush=True) + return do_print class MpvTestCase(unittest.TestCase): @@ -922,11 +923,10 @@ class RegressionTests(MpvTestCase): def test_wait_for_property_concurrency(self): players = [mpv.MPV(vo=testvo, loglevel='debug', log_handler=timed_print()) for i in range(2)] - try: for _ in range(150): for player in players: - player.play('tests/test.webm') + player.loadfile('tests/test.webm', loop='inf') for player in players: player.wait_for_property('seekable') for player in players: From 1bc7e2525e166e963bf9449c07a1c13135cf85f6 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 21 Dec 2024 14:31:14 +0100 Subject: [PATCH 68/71] Use callback id() instead of frame hash() to identify anonymous python streams Frame hashes are not unique since the frame isn't kept around for the life time of the stream. Fixes #292. --- mpv.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/mpv.py b/mpv.py index cd843a2..03b7828 100644 --- a/mpv.py +++ b/mpv.py @@ -1956,6 +1956,10 @@ class MPV(object): Any given name can only be registered once. The catch-all can also only be registered once. To unregister a stream, call the .unregister function set on the callback. + If name is None (the default), a name and corresponding python:// URI are automatically generated. You can + access the name through the .stream_name property set on the callback, and the stream URI for passing into + mpv.play(...) through the .stream_uri property. + The generator signals EOF by returning, manually raising StopIteration or by yielding b'', an empty bytes object. @@ -1972,16 +1976,25 @@ class MPV(object): reader.unregister() """ def register(cb): + nonlocal name + if name is None: + name = f'__python_mpv_anonymous_python_stream_{id(cb)}__' + if name in self._python_streams: raise KeyError('Python stream name "{}" is already registered'.format(name)) + self._python_streams[name] = (cb, size) def unregister(): if name not in self._python_streams or\ self._python_streams[name][0] is not cb: # This is just a basic sanity check raise RuntimeError('Python stream has already been unregistered') del self._python_streams[name] + cb.unregister = unregister + cb.stream_name = name + cb.stream_uri = f'python://{name}' return cb + return register @contextmanager @@ -2003,10 +2016,8 @@ class MPV(object): """ q = queue.Queue() - frame = sys._getframe() - stream_name = f'__python_mpv_play_generator_{hash(frame)}' - EOF = frame # Get some unique object as EOF marker - @self.python_stream(stream_name) + EOF = object() # Get some unique object as EOF marker + @self.python_stream() def reader(): while (chunk := q.get()) is not EOF: if chunk: @@ -2017,21 +2028,19 @@ class MPV(object): q.put(chunk) # Start playback before yielding, the first call to reader() will block until write is called at least once. - self.play(f'python://{stream_name}') + self.play(reader.stream_uri) yield write q.put(EOF) def play_bytes(self, data): """ Play the given bytes object as a single file. """ - frame = sys._getframe() - stream_name = f'__python_mpv_play_generator_{hash(frame)}' - @self.python_stream(stream_name) + @self.python_stream() def reader(): yield data reader.unregister() # unregister itself - self.play(f'python://{stream_name}') + self.play(reader.stream_uri) def python_stream_catchall(self, cb): """ Register a catch-all python stream to be called when no name matches can be found. Use this decorator on a From 12850b34bd3b64704f8abd30341a647a73719267 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 21 Dec 2024 14:47:38 +0100 Subject: [PATCH 69/71] Add support for libmpv's new args to key binding handlers This changes the API, check your code if you use key bindings. --- mpv.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/mpv.py b/mpv.py index 03b7828..c227b33 100644 --- a/mpv.py +++ b/mpv.py @@ -1698,9 +1698,10 @@ class MPV(object): def _binding_name(callback_or_cmd): return 'py_kb_{:016x}'.format(hash(callback_or_cmd)&0xffffffffffffffff) - def on_key_press(self, keydef, mode='force'): + def on_key_press(self, keydef, mode='force', repetition=False): """Function decorator to register a simplified key binding. The callback is called whenever the key given is - *pressed*. + *pressed*. When the ``repetition=True`` is passed, the callback is called again repeatedly while the key is held + down. To unregister the callback function, you can call its ``unregister_mpv_key_bindings`` attribute:: @@ -1720,8 +1721,8 @@ class MPV(object): def register(fun): @self.key_binding(keydef, mode) @wraps(fun) - def wrapper(state='p-', name=None, char=None): - if state[0] in ('d', 'p'): + def wrapper(state='p-', name=None, char=None, *_): + if state[0] in ('d', 'p') or (repetition and state[0] == 'r'): fun() return wrapper return register @@ -1729,8 +1730,11 @@ class MPV(object): def key_binding(self, keydef, mode='force'): """Function decorator to register a low-level key binding. - The callback function signature is ``fun(key_state, key_name)`` where ``key_state`` is either ``'U'`` for "key - up" or ``'D'`` for "key down". + The callback function signature is ``fun(key_state, key_name, key_char, scale, arg)``. + + The key_state contains up to three chars, corresponding to the regex ``[udr]([m-][c-]?)?``. ``[udr]`` means + "key up", "key down", or "repetition" for when the key is held down. "m" indicates mouse events, and "c" + indicates key up events resulting from a logical cancellation. For details check out the mpv man page. The keydef format is: ``[Shift+][Ctrl+][Alt+][Meta+]`` where ```` is either the literal character the key produces (ASCII or Unicode character), or a symbolic name (as printed by ``mpv --input-keylist``). @@ -1785,12 +1789,12 @@ class MPV(object): raise TypeError('register_key_binding expects either an str with an mpv command or a python callable.') self.command('enable-section', binding_name, 'allow-hide-cursor+allow-vo-dragging') - def _handle_key_binding_message(self, binding_name, key_state, key_name=None, key_char=None): + def _handle_key_binding_message(self, binding_name, key_state, key_name=None, key_char=None, scale=None, arg=None, *_): binding_name = binding_name.decode('utf-8') key_state = key_state.decode('utf-8') key_name = key_name.decode('utf-8') if key_name is not None else None key_char = key_char.decode('utf-8') if key_char is not None else None - self._key_binding_handlers[binding_name](key_state, key_name, key_char) + self._key_binding_handlers[binding_name](key_state, key_name, key_char, scale, arg) def unregister_key_binding(self, keydef): """Unregister a key binding by keydef.""" From 0c33c933dd05f05d7e11ca199e2faa44e1d583a4 Mon Sep 17 00:00:00 2001 From: jaseg Date: Fri, 25 Apr 2025 11:48:30 +0200 Subject: [PATCH 70/71] Version 1.0.8 --- mpv.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mpv.py b/mpv.py index c227b33..1d3f469 100644 --- a/mpv.py +++ b/mpv.py @@ -17,7 +17,7 @@ # # You can find copies of the GPLv2 and LGPLv2.1 licenses in the project repository's LICENSE.GPL and LICENSE.LGPL files. -__version__ = '1.0.7' +__version__ = '1.0.8' from ctypes import * import ctypes.util diff --git a/pyproject.toml b/pyproject.toml index dbea61f..f68661e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ py-modules = ['mpv'] [project] name = "mpv" -version = "v1.0.7" +version = "v1.0.8" description = "A python interface to the mpv media player" readme = "README.rst" authors = [{name = "jaseg", email = "mpv@jaseg.de"}] From b26b72e183bcbcf968dfaa192c819acf38580713 Mon Sep 17 00:00:00 2001 From: jaseg Date: Fri, 25 Apr 2025 11:54:22 +0200 Subject: [PATCH 71/71] Fix typo in MPV.osd_overlay Closes #293 --- mpv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpv.py b/mpv.py index 1d3f469..f9bf561 100644 --- a/mpv.py +++ b/mpv.py @@ -1529,7 +1529,7 @@ class MPV(object): self.command('overlay_remove', overlay_id) def osd_overlay(self, overlay_id, data, res_x=0, res_y=720, z=0, hidden=False): - self.command('osd_overlay', id=overlay_id, data=data, res_x=res_x, res_y=res_Y, z=z, hidden=hidden, + self.command('osd_overlay', id=overlay_id, data=data, res_x=res_x, res_y=res_y, z=z, hidden=hidden, format='ass-events') def osd_overlay_remove(self, overlay_id):