Repository: streamlink/streamlink

Issue:
session.plugins: No longer logs when plugin is overridden
### 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

6.6.1+2.g61cec0e8

### Description

Streamlink no longer logs anything when a plugin is overridden/sideloaded.
It used to log `[session][debug] Plugin twitch is being overridden by ...`
The code seemingly [still exists](https://github.com/streamlink/streamlink/blob/61cec0e88e1a49ee09c39b78fa0396c1da3239ea/src/streamlink/session/plugins.py#L183), but it doesn't get called.

You can see the sideloaded plugin was properly loaded by the log message, but there was no notice by streamlink:
```shell
[hakkin@hakkin-pc ~]$ ls ~/.local/share/streamlink/plugins/
__pycache__  twitch.py
[hakkin@hakkin-pc ~]$ streamlink -o=/dev/null --loglevel=debug --logfile=log.txt https://www.twitch.tv/twitchmedia21 best
...
[plugins.twitch][info] This log is from the overridden plugin!
...
```

### Debug log

```text
[cli][debug] OS:         Linux-6.6.16-1-lts-x86_64-with-glibc2.39
[cli][debug] Python:     3.11.7
[cli][debug] OpenSSL:    OpenSSL 3.2.1 30 Jan 2024
[cli][debug] Streamlink: 6.6.1+2.g61cec0e8
[cli][debug] Dependencies:
[cli][debug]  certifi: 2024.2.2
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 5.1.0
[cli][debug]  pycountry: 23.12.11
[cli][debug]  pycryptodome: 3.20.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.24.0
[cli][debug]  trio-websocket: 0.11.1
[cli][debug]  typing-extensions: 4.9.0
[cli][debug]  urllib3: 2.2.1
[cli][debug]  websocket-client: 1.7.0
[cli][debug] Arguments:
[cli][debug]  url=https://www.twitch.tv/twitchmedia21
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --logfile=log.txt
[cli][debug]  --output=/dev/null
[plugins.twitch][info] This log is from the overridden plugin!
[cli][info] Found matching plugin twitch for URL https://www.twitch.tv/twitchmedia21
[plugins.twitch][debug] Getting live HLS streams for twitchmedia21
[plugins.twitch][debug] {'adblock': False, 'geoblock_reason': '', 'hide_ads': False, 'server_ads': True, 'show_ads': True}
[utils.l10n][debug] Language code: en_US
[cli][info] Available streams: audio_only, 160p (worst), 360p, 480p, 720p, 720p60, 1080p60 (best)
[cli][info] Opening stream: 1080p60 (hls)
[cli][info] Writing output to
/dev/null
[cli][debug] Checking file output
[stream.hls][debug] Reloading playlist
[cli][debug] Pre-buffering 8192 bytes
[stream.hls][debug] First Sequence: 0; Last Sequence: 3
[stream.hls][debug] Start offset: 0; Duration: None; Start Sequence: 1; End Sequence: None
[stream.hls][debug] Adding segment 1 to queue
[stream.hls][debug] Adding segment 2 to queue
[stream.hls][debug] Adding segment 3 to queue
[stream.hls][debug] Writing segment 1 to output
[stream.hls][debug] Segment 1 complete
[cli][debug] Writing stream to output
[stream.hls][debug] Writing segment 2 to output
[stream.hls][debug] Segment 2 complete
[stream.hls][debug] Writing segment 3 to output
[stream.hls][debug] Segment 3 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 4 to queue
[stream.hls][debug] Adding segment 5 to queue
[stream.hls][debug] Adding segment 6 to queue
[stream.hls][debug] Writing segment 4 to output
[stream.hls][debug] Segment 4 complete
[stream.hls][debug] Writing segment 5 to output
[stream.hls][debug] Segment 5 complete
[stream.hls][debug] Writing segment 6 to output
[stream.hls][debug] Segment 6 complete
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[stream.segmented][debug] Closing writer thread
[cli][info] Stream ended
Interrupted! Exiting...
[cli][info] Closing currently open stream...
```



Discussion:
Thanks for the bug report



Patch:
diff --git a/src/streamlink/session/plugins.py b/src/streamlink/session/plugins.py
index 4526081a769..5ef32d6a97d 100644
--- a/src/streamlink/session/plugins.py
+++ b/src/streamlink/session/plugins.py
@@ -179,7 +179,7 @@ def _load_plugins_from_path(self, path: Union[Path, str]) -> Dict[str, Type[Plug
             if lookup is None:
                 continue
             mod, plugin = lookup
-            if name in self._plugins:
+            if name in self._plugins or name in self._matchers:
                 log.info(f"Plugin {name} is being overridden by {mod.__file__}")
             plugins[name] = plugin
 


Test patch:
diff --git a/tests/session/test_plugins.py b/tests/session/test_plugins.py
index f7d311e9be9..2b86657bec6 100644
--- a/tests/session/test_plugins.py
+++ b/tests/session/test_plugins.py
@@ -131,6 +131,11 @@ def test_load_path_testplugins(self, caplog: pytest.LogCaptureFixture, session:
         assert session.plugins["testplugin"].__module__ == "streamlink.plugins.testplugin"
         assert caplog.record_tuples == []
 
+    def test_load_path_testplugins_override(self, caplog: pytest.LogCaptureFixture, session: Streamlink):
+        assert session.plugins.load_path(PATH_TESTPLUGINS)
+        assert "testplugin" in session.plugins
+        assert caplog.record_tuples == []
+
         assert session.plugins.load_path(PATH_TESTPLUGINS_OVERRIDE)
         assert "testplugin" in session.plugins
         assert session.plugins.get_names() == ["testplugin"]
@@ -144,6 +149,26 @@ def test_load_path_testplugins(self, caplog: pytest.LogCaptureFixture, session:
             ),
         ]
 
+    def test_load_path_testplugins_override_matchers(self, caplog: pytest.LogCaptureFixture, session: Streamlink):
+        assert _TestPlugin.matchers
+        session.plugins._matchers.update({"testplugin": _TestPlugin.matchers})
+
+        assert "testplugin" not in session.plugins
+        assert session.plugins.get_names() == ["testplugin"]
+        assert caplog.record_tuples == []
+
+        assert session.plugins.load_path(PATH_TESTPLUGINS)
+        assert session.plugins.get_names() == ["testplugin"]
+        assert session.plugins["testplugin"].__name__ == "TestPlugin"
+        assert session.plugins["testplugin"].__module__ == "streamlink.plugins.testplugin"
+        assert [(record.name, record.levelname, record.message) for record in caplog.records] == [
+            (
+                "streamlink.session",
+                "info",
+                f"Plugin testplugin is being overridden by {PATH_TESTPLUGINS / 'testplugin.py'}",
+            ),
+        ]
+
     def test_importerror(self, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, session: Streamlink):
         monkeypatch.setattr("importlib.machinery.FileFinder.find_spec", Mock(return_value=None))
         assert not session.plugins.load_path(PATH_TESTPLUGINS)