Repository: streamlink/streamlink

Issue:
plugins.hls: recognize URLs with uppercase ".M3U8"
### Checklist

- [X] This is a bug report and not [a different kind of issue](https://github.com/streamlink/streamlink/issues/new/choose)
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed bug reports](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22bug%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

streamlink 6.2.1+29.gc82a8535

### Description

Currently a URL with upper case M3U8, e.g. `https://example.com/live.M3U8` would not be recognized as an HLS URL.



### Debug log

```text
>streamlink https://example.com/live.M3U8 best --loglevel=debug 
[cli][debug] OS:         Windows 10
[cli][debug] Python:     3.11.1
[cli][debug] OpenSSL:    OpenSSL 1.1.1q  5 Jul 2022
[cli][debug] Streamlink: 6.2.1+29.gc82a8535
[cli][debug] Dependencies:
[cli][debug]  certifi: 2023.5.7
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.2
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.16.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.22.0
[cli][debug]  trio-websocket: 0.9.2
[cli][debug]  typing-extensions: 4.4.0
[cli][debug]  urllib3: 1.26.15
[cli][debug]  websocket-client: 1.5.1
[cli][debug] Arguments:
[cli][debug]  url=https://example.com/live.M3U8
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --player=mpv.exe
[cli][debug]  --stream-segment-threads=10
[cli][debug]  --hls-segment-queue-threshold=0.0
error: No plugin can handle URL: https://example.com/live.M3U8
```



Discussion:

Resolving patch:
diff --git a/src/streamlink/plugins/dash.py b/src/streamlink/plugins/dash.py
--- a/src/streamlink/plugins/dash.py
+++ b/src/streamlink/plugins/dash.py
@@ -15,6 +15,7 @@
 ))
 @pluginmatcher(priority=LOW_PRIORITY, pattern=re.compile(
     r"(?P<url>\S+\.mpd(?:\?\S*)?)(?:\s(?P<params>.+))?$",
+    re.IGNORECASE,
 ))
 class MPEGDASH(Plugin):
     @classmethod
diff --git a/src/streamlink/plugins/hls.py b/src/streamlink/plugins/hls.py
--- a/src/streamlink/plugins/hls.py
+++ b/src/streamlink/plugins/hls.py
@@ -15,6 +15,7 @@
 ))
 @pluginmatcher(priority=LOW_PRIORITY, pattern=re.compile(
     r"(?P<url>\S+\.m3u8(?:\?\S*)?)(?:\s(?P<params>.+))?$",
+    re.IGNORECASE,
 ))
 class HLSPlugin(Plugin):
     def _get_streams(self):


Test patch:
diff --git a/tests/plugins/test_dash.py b/tests/plugins/test_dash.py
--- a/tests/plugins/test_dash.py
+++ b/tests/plugins/test_dash.py
@@ -14,6 +14,7 @@ class TestPluginCanHandleUrlMPEGDASH(PluginCanHandleUrl):
     should_match_groups = [
         # implicit DASH URLs
         ("example.com/foo.mpd", {"url": "example.com/foo.mpd"}),
+        ("example.com/foo.MPD", {"url": "example.com/foo.MPD"}),
         ("example.com/foo.mpd?bar", {"url": "example.com/foo.mpd?bar"}),
         ("http://example.com/foo.mpd", {"url": "http://example.com/foo.mpd"}),
         ("http://example.com/foo.mpd?bar", {"url": "http://example.com/foo.mpd?bar"}),
@@ -33,6 +34,7 @@ class TestPluginCanHandleUrlMPEGDASH(PluginCanHandleUrl):
     should_not_match = [
         # implicit DASH URLs must have their path end with ".mpd"
         "example.com/mpd",
+        "example.com/MPD",
         "example.com/mpd abc=def",
         "example.com/foo.mpd,bar",
         "example.com/foo.mpd,bar abc=def",
@@ -44,6 +46,7 @@ class TestPluginCanHandleUrlMPEGDASH(PluginCanHandleUrl):
 
 @pytest.mark.parametrize(("url", "priority"), [
     ("http://example.com/foo.mpd", LOW_PRIORITY),
+    ("http://example.com/foo.MPD", LOW_PRIORITY),
     ("dash://http://example.com/foo.mpd", NORMAL_PRIORITY),
     ("dash://http://example.com/bar", NORMAL_PRIORITY),
     ("http://example.com/bar", NO_PRIORITY),
diff --git a/tests/plugins/test_hls.py b/tests/plugins/test_hls.py
--- a/tests/plugins/test_hls.py
+++ b/tests/plugins/test_hls.py
@@ -15,6 +15,7 @@ class TestPluginCanHandleUrlHLSPlugin(PluginCanHandleUrl):
     should_match_groups = [
         # implicit HLS URLs
         ("example.com/foo.m3u8", {"url": "example.com/foo.m3u8"}),
+        ("example.com/foo.M3U8", {"url": "example.com/foo.M3U8"}),
         ("example.com/foo.m3u8?bar", {"url": "example.com/foo.m3u8?bar"}),
         ("http://example.com/foo.m3u8", {"url": "http://example.com/foo.m3u8"}),
         ("http://example.com/foo.m3u8?bar", {"url": "http://example.com/foo.m3u8?bar"}),
@@ -38,6 +39,7 @@ class TestPluginCanHandleUrlHLSPlugin(PluginCanHandleUrl):
     should_not_match = [
         # implicit HLS URLs must have their path end with ".m3u8"
         "example.com/m3u8",
+        "example.com/M3U8",
         "example.com/m3u8 abc=def",
         "example.com/foo.m3u8,bar",
         "example.com/foo.m3u8,bar abc=def",
@@ -50,6 +52,7 @@ class TestPluginCanHandleUrlHLSPlugin(PluginCanHandleUrl):
 
 @pytest.mark.parametrize(("url", "priority"), [
     ("http://example.com/foo.m3u8", LOW_PRIORITY),
+    ("http://example.com/foo.M3U8", LOW_PRIORITY),
     ("hls://http://example.com/foo.m3u8", NORMAL_PRIORITY),
     ("hls://http://example.com/bar", NORMAL_PRIORITY),
     ("hlsvariant://http://example.com/foo.m3u8", NORMAL_PRIORITY),