From 0b2a15b685c66fdd9e8fd6e7ae2fff3928b1678c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Nov 2026 20:34:52 +0000 Subject: [PATCH 1/8] chore(deps): bump actions/checkout from 4 to 7 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] (cherry picked from commit 9e440162e3b2703ccb536a85286068c2a61f03e8) --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2519e8f..047be9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: # 3.10 = Ubuntu 22.04, 3.11 = Debian 12 (production targets); 3.12 = current. python-version: ["3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: @@ -54,7 +54,7 @@ jobs: name: Shell scripts runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Syntax check run: | for script in install.sh scripts/streamvault_vps.sh bin/streamvault; do From 188f5bb6073bfa92c8caced0648c6fa555de20f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:08:27 +0000 Subject: [PATCH 2/8] chore(deps): Update pycryptodome requirement from >=3.20.0 to >=4.4.0 Updates the requirements on [pycryptodome](https://github.com/Legrandin/pycryptodome) to permit the latest version. - [Release notes](https://github.com/Legrandin/pycryptodome/releases) - [Changelog](https://github.com/Legrandin/pycryptodome/blob/master/Changelog.rst) - [Commits](https://github.com/Legrandin/pycryptodome/compare/v3.20.0...v4.4.0) --- updated-dependencies: - dependency-name: pycryptodome dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] (cherry picked from commit a7766edc9ff4fc3ab0bbc216a1d45b91eadba376) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8ec16de..2fb96c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,6 @@ redis>=5.0.0 limits>=3.10.0 markupsafe>=2.1.0 gunicorn>=21.2.0; sys_platform != "win32" -pycryptodome>=3.20.0 +pycryptodome>=4.4.0 numpy>=1.26.0,<3.0.0 boto3>=1.34.0 From 7c4809ebc75402da61ccd7c325679fb1f04f076a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:08:25 +0000 Subject: [PATCH 3/8] chore(deps): Update streamlink requirement from >=8.0.0 to >=8.6.3 Updates the requirements on [streamlink](https://github.com/streamlink/streamlink) to permit the latest version. - [Release notes](https://github.com/streamlink/streamlink/releases) - [Changelog](https://github.com/streamlink/streamlink/blob/master/CHANGELOG.md) - [Commits](https://github.com/streamlink/streamlink/compare/8.0.0...8.6.3) --- updated-dependencies: - dependency-name: streamlink dependency-version: 8.6.3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] (cherry picked from commit 1f62406c8b975644e4998b59abe93954559650ed) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2fb96c4..b46172d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ Flask-Babel>=4.0.0,<5 Werkzeug>=3.0.0,<3.2.0 Jinja2>=3.1.2 requests>=2.31.0 -streamlink>=8.0.0 +streamlink>=8.6.3 yt-dlp>=2024.1.0 redis>=5.0.0 limits>=3.10.0 From f705b855f32954917a0d114311bb64fbc41a16e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:08:29 +0000 Subject: [PATCH 4/8] chore(deps): Update markupsafe requirement from >=2.1.0 to >=3.0.4 Updates the requirements on [markupsafe](https://github.com/pallets/markupsafe) to permit the latest version. - [Release notes](https://github.com/pallets/markupsafe/releases) - [Changelog](https://github.com/pallets/markupsafe/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/markupsafe/compare/2.1.0...3.0.4) --- updated-dependencies: - dependency-name: markupsafe dependency-version: 3.0.4 dependency-type: direct:production ... Signed-off-by: dependabot[bot] (cherry picked from commit d60b3d762a185b4f55bf8f5d41c2e75920551f06) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b46172d..18b28bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ streamlink>=8.6.3 yt-dlp>=2024.1.0 redis>=5.0.0 limits>=3.10.0 -markupsafe>=2.1.0 +markupsafe>=3.0.4 gunicorn>=21.2.0; sys_platform != "win32" pycryptodome>=4.4.0 numpy>=1.26.0,<3.0.0 From 7276cf1ce4d9050eb715cd10ce8097360b448c8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Nov 2026 15:08:29 +0000 Subject: [PATCH 5/8] chore(deps): Update numpy requirement Updates the requirements on [numpy](https://github.com/numpy/numpy) to permit the latest version. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v1.26.0...v2.2.6) --- updated-dependencies: - dependency-name: numpy dependency-version: 2.2.6 dependency-type: direct:production ... Signed-off-by: dependabot[bot] (cherry picked from commit 0eafe4c844d13354f2304864de942657834dc226) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 18b28bf..35d2e5c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,5 +12,5 @@ limits>=3.10.0 markupsafe>=3.0.4 gunicorn>=21.2.0; sys_platform != "win32" pycryptodome>=4.4.0 -numpy>=1.26.0,<3.0.0 +numpy>=2.2.6,<3.0.0 boto3>=1.34.0 From 2646f79fd096179c9745c7daf2fd1369ac9d03df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:18:39 +0000 Subject: [PATCH 6/8] chore(deps): update redis requirement from >=5.0.0 to >=8.1.0 Updates the requirements on [redis](https://github.com/redis/redis-py) to permit the latest version. - [Release notes](https://github.com/redis/redis-py/releases) - [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES) - [Commits](https://github.com/redis/redis-py/compare/v5.0.0...v8.1.0) --- updated-dependencies: - dependency-name: redis dependency-version: 8.1.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] (cherry picked from commit d85da27ccd23b4593692b236c66009350adfe674) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 35d2e5c..269a4dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ Jinja2>=3.1.2 requests>=2.31.0 streamlink>=8.6.3 yt-dlp>=2024.1.0 -redis>=5.0.0 +redis>=8.1.0 limits>=3.10.0 markupsafe>=3.0.4 gunicorn>=21.2.0; sys_platform != "win32" From 5babb336517560b846e563568d62d147b40f72a3 Mon Sep 17 00:00:00 2001 From: Blackwell Code <161588956+Blackwellcode@users.noreply.github.com> Date: Thu, 12 Nov 2026 16:18:30 +0100 Subject: [PATCH 7/8] fix(redis): pin RESP2 and no connect retry for redis-py 8 redis-py 8 defaults to RESP3 and retries the connect; an unreachable Redis took 4.23 s instead of 2.01 s before falling back to local mode (measured on Windows). Pinning both keeps the old behaviour and older servers working. Co-Authored-By: Claude Opus 5.5 --- core/redis_client.py | 7 +++++ tests/test_redis_client.py | 57 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 tests/test_redis_client.py diff --git a/core/redis_client.py b/core/redis_client.py index 4f8309e..525b762 100644 --- a/core/redis_client.py +++ b/core/redis_client.py @@ -45,6 +45,8 @@ def get_redis(): _LAST_CONNECT_TRY = now try: import redis + from redis.backoff import ExponentialBackoff + from redis.retry import Retry pool = redis.ConnectionPool( host=REDIS_HOST, port=REDIS_PORT, @@ -53,6 +55,11 @@ def get_redis(): socket_timeout=3.0, socket_connect_timeout=3.0, retry_on_timeout=True, + # redis-py 8 changed the defaults (RESP3, retries on connect). Pin both so an + # unreachable Redis still falls back to local mode quickly and older + # servers keep working. + retry=Retry(ExponentialBackoff(cap=0.5, base=0.1), 0), + protocol=2, max_connections=50, decode_responses=True, ) diff --git a/tests/test_redis_client.py b/tests/test_redis_client.py new file mode 100644 index 0000000..b2d6201 --- /dev/null +++ b/tests/test_redis_client.py @@ -0,0 +1,57 @@ +"""Redis connection settings that must survive dependency upgrades.""" + +import unittest +from unittest import mock + +from core import redis_client + + +class RedisPoolSettingsTests(unittest.TestCase): + def setUp(self): + self._saved = ( + redis_client._REDIS_CLIENT, + redis_client._REDIS_AVAILABLE, + redis_client._LAST_CONNECT_TRY, + ) + redis_client._REDIS_CLIENT = None + redis_client._REDIS_AVAILABLE = None + redis_client._LAST_CONNECT_TRY = 0 + + def tearDown(self): + ( + redis_client._REDIS_CLIENT, + redis_client._REDIS_AVAILABLE, + redis_client._LAST_CONNECT_TRY, + ) = self._saved + + def _pool_kwargs(self): + import redis + + with mock.patch.object(redis, "ConnectionPool") as pool_cls, mock.patch.object( + redis, "Redis" + ) as redis_cls: + redis_cls.return_value.ping.return_value = True + client = redis_client.get_redis() + self.assertIs(client, redis_cls.return_value) + return pool_cls.call_args.kwargs + + def test_pool_pins_resp2_for_older_servers(self): + self.assertEqual(self._pool_kwargs()["protocol"], 2) + + def test_pool_does_not_retry_the_connect(self): + # An unreachable Redis must fall back to local mode after one attempt. + retry = self._pool_kwargs()["retry"] + self.assertEqual(retry._retries, 0) + + def test_unreachable_redis_falls_back_to_none(self): + import redis + + with mock.patch.object(redis, "Redis") as redis_cls: + redis_cls.return_value.ping.side_effect = redis.ConnectionError("refused") + with self.assertLogs(redis_client.logger, level="WARNING"): + self.assertIsNone(redis_client.get_redis()) + self.assertFalse(redis_client._REDIS_AVAILABLE) + + +if __name__ == "__main__": + unittest.main() From f2152c36041536f6693efa43eeb6c7fc92891ebd Mon Sep 17 00:00:00 2001 From: Blackwell Code <161588956+Blackwellcode@users.noreply.github.com> Date: Thu, 12 Nov 2026 16:23:26 +0100 Subject: [PATCH 8/8] fix(redis): keep the redis-py 5 retry policy, changelog for the new minimum versions One retry on timeouts only (as redis-py 5), connection errors are not retried. The test now builds the real pool and checks the connection's protocol and retry behaviour instead of a mocked kwarg. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 +++++ core/redis_client.py | 22 ++++++++++++++----- tests/test_redis_client.py | 45 ++++++++++++++++++++++++-------------- 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c0583c..70d9959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ All notable changes to this project are documented here. The format follows ### Breaking changes +- **New minimum versions, reinstall the requirements** after updating + (`pip install -r requirements.txt` in the virtualenv): redis ≥ 8.1.0, + streamlink ≥ 8.6.3, markupsafe ≥ 3.0.4, pycryptodome ≥ 4.4.0, + numpy ≥ 2.2.6. StreamVault keeps redis-py 5 behaviour: RESP2 (Redis servers + older than 6 keep working) and one retry on timeouts only, so an unreachable + Redis still falls back to local mode after a single connect attempt. - **Sessions expire after 30 days** (`VOD_SESSION_MAX_AGE_SEC`, `0` restores the old unlimited behaviour). Logout ends the session in this browser; the new "log out everywhere" action (`POST /logout/all`) and every password diff --git a/core/redis_client.py b/core/redis_client.py index 525b762..28ea239 100644 --- a/core/redis_client.py +++ b/core/redis_client.py @@ -22,6 +22,17 @@ REDIS_DB = int(os.environ.get("REDIS_DB", "0")) REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD", None) + +def _legacy_retry(): + """Retry policy of redis-py 5: one immediate retry, on timeouts only.""" + import socket + + from redis.backoff import NoBackoff + from redis.exceptions import TimeoutError as RedisTimeoutError + from redis.retry import Retry + + return Retry(NoBackoff(), 1, supported_errors=(RedisTimeoutError, socket.timeout)) + KEY_PREFIX = "streamvault:" TASK_QUEUE_KEY = f"{KEY_PREFIX}tasks:pending" TASK_INFLIGHT_KEY = f"{KEY_PREFIX}tasks:inflight" @@ -45,8 +56,6 @@ def get_redis(): _LAST_CONNECT_TRY = now try: import redis - from redis.backoff import ExponentialBackoff - from redis.retry import Retry pool = redis.ConnectionPool( host=REDIS_HOST, port=REDIS_PORT, @@ -55,10 +64,11 @@ def get_redis(): socket_timeout=3.0, socket_connect_timeout=3.0, retry_on_timeout=True, - # redis-py 8 changed the defaults (RESP3, retries on connect). Pin both so an - # unreachable Redis still falls back to local mode quickly and older - # servers keep working. - retry=Retry(ExponentialBackoff(cap=0.5, base=0.1), 0), + # redis-py 8 changed the defaults: RESP3, and connection errors are + # retried too. Pin the redis-py 5 behaviour: RESP2 for older servers, + # one retry on timeouts only, so an unreachable Redis still falls + # back to local mode after a single connect attempt. + retry=_legacy_retry(), protocol=2, max_connections=50, decode_responses=True, diff --git a/tests/test_redis_client.py b/tests/test_redis_client.py index b2d6201..28fa813 100644 --- a/tests/test_redis_client.py +++ b/tests/test_redis_client.py @@ -1,8 +1,11 @@ """Redis connection settings that must survive dependency upgrades.""" +import socket import unittest from unittest import mock +import redis + from core import redis_client @@ -24,28 +27,38 @@ def tearDown(self): redis_client._LAST_CONNECT_TRY, ) = self._saved - def _pool_kwargs(self): - import redis - - with mock.patch.object(redis, "ConnectionPool") as pool_cls, mock.patch.object( - redis, "Redis" - ) as redis_cls: + def _connection(self): + """Build the real pool get_redis() builds and return one (unconnected) connection.""" + with mock.patch.object(redis, "Redis") as redis_cls: redis_cls.return_value.ping.return_value = True - client = redis_client.get_redis() - self.assertIs(client, redis_cls.return_value) - return pool_cls.call_args.kwargs + redis_client.get_redis() + pool = redis_cls.call_args.kwargs["connection_pool"] + self.assertIsInstance(pool, redis.ConnectionPool) + return pool.make_connection() def test_pool_pins_resp2_for_older_servers(self): - self.assertEqual(self._pool_kwargs()["protocol"], 2) + self.assertEqual(self._connection().protocol, 2) - def test_pool_does_not_retry_the_connect(self): - # An unreachable Redis must fall back to local mode after one attempt. - retry = self._pool_kwargs()["retry"] - self.assertEqual(retry._retries, 0) + def test_timeouts_are_retried_once(self): + retry = self._connection().retry + self.assertEqual(retry.get_retries(), 1) + self.assertIn(redis.exceptions.TimeoutError, retry._supported_errors) + self.assertIn(socket.timeout, retry._supported_errors) - def test_unreachable_redis_falls_back_to_none(self): - import redis + def test_connection_errors_are_not_retried(self): + # An unreachable Redis must fall back to local mode after one connect attempt. + retry = self._connection().retry + attempts = [] + def refuse(): + attempts.append(1) + raise redis.exceptions.ConnectionError("refused") + + with self.assertRaises(redis.exceptions.ConnectionError): + retry.call_with_retry(refuse, lambda error: None) + self.assertEqual(len(attempts), 1) + + def test_unreachable_redis_falls_back_to_none(self): with mock.patch.object(redis, "Redis") as redis_cls: redis_cls.return_value.ping.side_effect = redis.ConnectionError("refused") with self.assertLogs(redis_client.logger, level="WARNING"):