Repository: streamlink/streamlink
Pull request #3871 (2021-07-16 13:48:26)

plugins.tv4play: fix plugin URL regex
I tried to run for example the news stream from tv4play but it did not work. I know it worked in the past. So my guess is that they changed their URL convention recently.

Here is a current URL from their website: https://www.tv4play.se/program/nyheterna/live/13351122

The current plugin regex in master is: tv4play\.se/program/[^?/]+

But in order to support their new URL convention it needed to be updated to: tv4play\.se/program/[^?/]+/[^?/]+

Based on all URLs I can find on the tv4play website all follow this new pattern of /program/XX/YY/<video_id>

I updated the regex and the tests. Ran them and installed. Now it works.

Here are the changes.


Patch:
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index ea3a5c51..8f35b977 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -1,8 +1,8 @@
 Deprecations
 ============
 
-2.3.0
------
+streamlink 2.3.0
+----------------
 
 Plugin.can_handle_url() and Plugin.priority()
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -84,8 +84,8 @@ message will be written to the info log output for the first plugin that gets re
    ``NoStreamsError`` when there are no streams to be found on that particular URL.
 
 
-2.2.0
------
+streamlink 2.2.0
+----------------
 
 Config file paths
 ^^^^^^^^^^^^^^^^^
diff --git a/script/makeinstaller.sh b/script/makeinstaller.sh
index f3e5d82b..9825a2f5 100755
--- a/script/makeinstaller.sh
+++ b/script/makeinstaller.sh
@@ -92,13 +92,13 @@ format=bundled
 packages=pkg_resources
          iso639
 pypi_wheels=certifi==2021.5.30
-            charset-normalizer==2.0.1
-            idna==3.2
+            chardet==4.0.0
+            idna==2.10
             iso3166==1.0.1
             isodate==0.6.0
             pycryptodome==3.10.1
             PySocks==1.7.1
-            requests==2.26.0
+            requests==2.25.1
             six==1.16.0
             urllib3==1.26.6
             websocket-client==1.1.0
diff --git a/setup.py b/setup.py
index 37e627c9..8511e7e4 100644
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,10 @@ import versioneer
 
 data_files = []
 deps = [
-    "requests>=2.26.0,<3.0",
+    # Temporarily set requests to 2.25.1 on Windows to fix issues with randomly failing tests
+    # Don't force an older requests version on non-Windows systems due to packaging reasons
+    "requests>=2.26.0,<3.0 ; platform_system!='Windows'",
+    "requests==2.25.1      ; platform_system=='Windows'",
     "isodate",
     "websocket-client>=0.58.0",
     # Support for SOCKS proxies
diff --git a/src/streamlink/plugin/api/useragents.py b/src/streamlink/plugin/api/useragents.py
index de1ca6c6..c11bcf13 100644
--- a/src/streamlink/plugin/api/useragents.py
+++ b/src/streamlink/plugin/api/useragents.py
@@ -1,11 +1,11 @@
-ANDROID = "Mozilla/5.0 (Linux; Android 11) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.88 Mobile Safari/537.36"
-CHROME = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"
-CHROME_OS = "Mozilla/5.0 (X11; CrOS x86_64 13904.41.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.81 Safari/537.36"
-FIREFOX = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0"
+ANDROID = "Mozilla/5.0 (Linux; Android 11) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36"
+CHROME = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
+CHROME_OS = "Mozilla/5.0 (X11; CrOS x86_64 13904.77.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.147 Safari/537.36"
+FIREFOX = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0"
 IE_11 = "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"
 IPHONE = "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1"
-OPERA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36 OPR/76.0.4017.177"
-SAFARI = "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Safari/605.1.15"
+OPERA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 OPR/77.0.4054.203"
+SAFARI = "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15"
 
 # Backwards compatibility
 EDGE = CHROME
diff --git a/src/streamlink/plugins/tv4play.py b/src/streamlink/plugins/tv4play.py
index 6aa176d1..d53fc39f 100644
--- a/src/streamlink/plugins/tv4play.py
+++ b/src/streamlink/plugins/tv4play.py
@@ -12,7 +12,7 @@ log = logging.getLogger(__name__)
 @pluginmatcher(re.compile(r"""
     https?://(?:www\.)?
     (?:
-        tv4play\.se/program/[^?/]+
+        tv4play\.se/program/[^?/]+/[^?/]+
         |
         fotbollskanalen\.se/video
     )
diff --git a/src/streamlink/plugins/youtube.py b/src/streamlink/plugins/youtube.py
index 9a472ad9..6d6e9ef0 100644
--- a/src/streamlink/plugins/youtube.py
+++ b/src/streamlink/plugins/youtube.py
@@ -129,12 +129,19 @@ class YouTube(Plugin):
                 "videoId": str,
                 "author": str,
                 "title": str,
-                validate.optional("isLiveContent"): validate.transform(bool)
+                validate.optional("isLive"): validate.transform(bool),
+                validate.optional("isLiveContent"): validate.transform(bool),
+                validate.optional("isLiveDvrEnabled"): validate.transform(bool),
+                validate.optional("isLowLatencyLiveStream"): validate.transform(bool),
+                validate.optional("isPrivate"): validate.transform(bool),
             }},
             validate.get("videoDetails"),
-            validate.union_get("videoId", "author", "title", "isLiveContent")
         )
-        return validate.validate(schema, data)
+        videoDetails = validate.validate(schema, data)
+        log.trace(f"videoDetails = {videoDetails!r}")
+        return validate.validate(
+            validate.union_get("videoId", "author", "title", "isLive"),
+            videoDetails)
 
     @classmethod
     def _schema_streamingdata(cls, data):


Test patch:
diff --git a/tests/plugins/test_tv4play.py b/tests/plugins/test_tv4play.py
index f1bd77a5..12f14018 100644
--- a/tests/plugins/test_tv4play.py
+++ b/tests/plugins/test_tv4play.py
@@ -6,8 +6,8 @@ class TestPluginCanHandleUrlTV4Play(PluginCanHandleUrl):
     __plugin__ = TV4Play
 
     should_match = [
-        'https://www.tv4play.se/program/fridas-vm-resa/10000884',
-        'https://www.tv4play.se/program/monk/3938989',
-        'https://www.tv4play.se/program/nyheterna/10378590',
+        'https://www.tv4play.se/program/robinson/del-26-sasong-2021/13299862',
+        'https://www.tv4play.se/program/sverige-mot-norge/del-1-sasong-1/12490380',
+        'https://www.tv4play.se/program/nyheterna/live/10378590',
         'https://www.fotbollskanalen.se/video/10395484/ghoddos-fullbordar-vandningen---ger-ofk-ledningen/',
     ]
diff --git a/tests/plugins/test_twitch.py b/tests/plugins/test_twitch.py
index 451fd5b9..5f0a39e9 100644
--- a/tests/plugins/test_twitch.py
+++ b/tests/plugins/test_twitch.py
@@ -5,7 +5,6 @@ from unittest.mock import MagicMock, call, patch
 import requests_mock
 
 from streamlink import Streamlink
-from streamlink.compat import is_win32
 from streamlink.plugin import PluginError
 from streamlink.plugins.twitch import Twitch, TwitchHLSStream, TwitchHLSStreamReader, TwitchHLSStreamWriter
 from tests.mixins.stream_hls import EventedHLSStreamWriter, Playlist, Segment as _Segment, Tag, TestMixinStreamHLS
@@ -76,7 +75,6 @@ class _TwitchHLSStream(TwitchHLSStream):
     __reader__ = _TwitchHLSStreamReader
 
 
-@unittest.skipIf(is_win32, "temporarily skip EventedHLSStreamWriter related tests on Windows")
 @patch("streamlink.stream.hls.HLSStreamWorker.wait", MagicMock(return_value=True))
 class TestTwitchHLSStream(TestMixinStreamHLS, unittest.TestCase):
     __stream__ = _TwitchHLSStream
diff --git a/tests/streams/test_hls.py b/tests/streams/test_hls.py
index 8a3c461c..ac3d4753 100644
--- a/tests/streams/test_hls.py
+++ b/tests/streams/test_hls.py
@@ -7,7 +7,6 @@ import requests_mock
 from Crypto.Cipher import AES
 from Crypto.Util.Padding import pad
 
-from streamlink.compat import is_win32
 from streamlink.session import Streamlink
 from streamlink.stream import hls
 from tests.mixins.stream_hls import EventedHLSStreamWriter, Playlist, Segment, Tag, TestMixinStreamHLS
@@ -145,7 +144,6 @@ class TestHLSStream(TestMixinStreamHLS, unittest.TestCase):
         self.assertTrue(self.called(map2, once=True), "Downloads second map only once")
 
 
-@unittest.skipIf(is_win32, "temporarily skip EventedHLSStreamWriter related tests on Windows")
 @patch("streamlink.stream.hls.HLSStreamWorker.wait", Mock(return_value=True))
 class TestHLSStreamEncrypted(TestMixinStreamHLS, unittest.TestCase):
     __stream__ = EventedHLSStream
diff --git a/tests/streams/test_hls_filtered.py b/tests/streams/test_hls_filtered.py
index c98490e0..73b487db 100644
--- a/tests/streams/test_hls_filtered.py
+++ b/tests/streams/test_hls_filtered.py
@@ -2,7 +2,6 @@ import unittest
 from threading import Event
 from unittest.mock import MagicMock, call, patch
 
-from streamlink.compat import is_win32
 from streamlink.stream.hls import HLSStream, HLSStreamReader
 from tests.mixins.stream_hls import EventedHLSStreamWriter, Playlist, Segment, TestMixinStreamHLS
 
@@ -24,7 +23,6 @@ class _TestSubjectHLSStream(HLSStream):
     __reader__ = _TestSubjectHLSReader
 
 
-@unittest.skipIf(is_win32, "temporarily skip EventedHLSStreamWriter related tests on Windows")
 @patch("streamlink.stream.hls.HLSStreamWorker.wait", MagicMock(return_value=True))
 class TestFilteredHLSStream(TestMixinStreamHLS, unittest.TestCase):
     __stream__ = _TestSubjectHLSStream