Repository: streamlink/streamlink
Pull request #3324 (2020-11-08 05:22:30)

plugins: turn mux-subtitles into a global argument
Closes #3282 

Split into two commits. See the commit messages for the descriptions.

----

Since the argparse docs extension had to be refactored, here's a demonstration of the overall CLI docs changes, to make sure that nothing else changed:

```bash
diff <(curl -s https://streamlink.github.io/latest/cli.html) <(cat ./docs/_build/html/cli.html)
```

```diff
10c10
<   <title>Command-Line Interface &mdash; Streamlink 1.7.0+52.gf0b35b6 documentation</title>
---
>   <title>Command-Line Interface &mdash; Streamlink 1.7.0+54.g49ce436 documentation</title>
31c31
<     <link rel="top" title="Streamlink 1.7.0+52.gf0b35b6 documentation" href="index.html"/>
---
>     <link rel="top" title="Streamlink 1.7.0+54.g49ce436 documentation" href="index.html"/>
56c56
<   <span class="wy-side-nav-versions-current"><i class="fa fa-book"></i>1.7.0+52.gf0b35b6</span>
---
>   <span class="wy-side-nav-versions-current"><i class="fa fa-book"></i>1.7.0+54.g49ce436</span>
1382a1383,1390
> <dl class="option">
> <dt id="cmdoption-mux-subtitles">
> <code class="descname">--mux-subtitles</code><code class="descclassname"></code><a class="headerlink" href="#cmdoption-mux-subtitles" title="Permalink to this definition">¶</a></dt>
> <dd><p>Automatically mux available subtitles into the output stream.</p>
> <p>Needs to be supported by the used plugin.</p>
> <p><strong>Supported plugins:</strong> funimationnow, pluzz, rtve, svtplay, vimeo</p>
> </dd></dl>
> 
1610,1616d1617
< <dt id="cmdoption-funimation-mux-subtitles">
< <code class="descname">--funimation-mux-subtitles</code><code class="descclassname"></code><a class="headerlink" href="#cmdoption-funimation-mux-subtitles" title="Permalink to this definition">¶</a></dt>
< <dd><p>Enable automatically including available subtitles in to the output
< stream.</p>
< </dd></dl>
< 
< <dl class="option">
1688,1699d1688
< <dt id="cmdoption-pluzz-mux-subtitles">
< <code class="descname">--pluzz-mux-subtitles</code><code class="descclassname"></code><a class="headerlink" href="#cmdoption-pluzz-mux-subtitles" title="Permalink to this definition">¶</a></dt>
< <dd><p>Automatically mux available subtitles in to the output stream.</p>
< </dd></dl>
< 
< <dl class="option">
< <dt id="cmdoption-rtve-mux-subtitles">
< <code class="descname">--rtve-mux-subtitles</code><code class="descclassname"></code><a class="headerlink" href="#cmdoption-rtve-mux-subtitles" title="Permalink to this definition">¶</a></dt>
< <dd><p>Automatically mux available subtitles in to the output stream.</p>
< </dd></dl>
< 
< <dl class="option">
1742,1747d1730
< <dt id="cmdoption-svtplay-mux-subtitles">
< <code class="descname">--svtplay-mux-subtitles</code><code class="descclassname"></code><a class="headerlink" href="#cmdoption-svtplay-mux-subtitles" title="Permalink to this definition">¶</a></dt>
< <dd><p>Automatically mux available subtitles in to the output stream.</p>
< </dd></dl>
< 
< <dl class="option">
1813,1818d1795
< <dt id="cmdoption-vimeo-mux-subtitles">
< <code class="descname">--vimeo-mux-subtitles</code><code class="descclassname"></code><a class="headerlink" href="#cmdoption-vimeo-mux-subtitles" title="Permalink to this definition">¶</a></dt>
< <dd><p>Automatically mux available subtitles in to the output stream.</p>
< </dd></dl>
< 
< <dl class="option">
1925c1902
<             VERSION:'1.7.0+52.gf0b35b6',
---
>             VERSION:'1.7.0+54.g49ce436',
```


back-to: might be useful if you also add this plugin option to the log output

`plugin.global_arguments`

https://github.com/streamlink/streamlink/blob/49ce43614e7c364775fe640500048cba8dab399f/src/streamlink_cli/main.py#L557-L564

maybe not important for `mux-subtitles`,
but if we decide to add `purge-credentials` `username` `password` in the future as global args it would be better with a log output.
bastimeyer: That would require refactoring the `Argument` class or adding a separate class for the global arguments, because the arguments of the global arguments list are just references and will be logged differently (incorrectly). And arguments with sensitive data make this even more complicated, because the global argument which is being referenced needs to be looked up.

Regarding the other arguments which you've mentioned, I'm not sure if making them global is a good idea, because they depend on the input URL what's being done, and this can be confusing or lead to user issues when used incorrectly, especially purge-credentials. Global username/email/password/token/etc arguments could work, but since there's no unique scheme/pattern, that would require having a lot of global arguments and the list of supported plugins would be confusing. The mux-subtitles argument works in a global context because it's an independent thing.
bastimeyer: Added the `is_global` parameter to the `Argument` class and refactored the `streamlink_cli.main` methods. The global plugin arguments will now correctly be debug-logged (what @back-to mentioned), just like the regular plugin arguments. Documentation-wise, nothing has changed from the previous implementation.

Just noticed some changes I've missed which should have been reverted. Will force-push again in a minute...

Patch:
diff --git a/docs/_applications.rst b/docs/_applications.rst
index 767a79b8..6231b80c 100644
--- a/docs/_applications.rst
+++ b/docs/_applications.rst
@@ -1,5 +1,3 @@
-.. _applications:
-
 :orphan:
 
 
diff --git a/docs/api.rst b/docs/api.rst
index 80c13123..5669468c 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -1,5 +1,3 @@
-.. _api:
-
 API Reference
 =============
 
@@ -46,8 +44,6 @@ All streams inherit from the :class:`Stream` class.
     :members:
 
 
-.. _api-stream-subclasses:
-
 Stream subclasses
 ^^^^^^^^^^^^^^^^^
 
diff --git a/docs/api_guide.rst b/docs/api_guide.rst
index 797b685f..7afc9f5b 100644
--- a/docs/api_guide.rst
+++ b/docs/api_guide.rst
@@ -1,9 +1,7 @@
-.. _api_guide:
-
 API Guide
 =========
 
-This API is what powers the :ref:`cli` but is also available to developers that wish
+This API is what powers the :ref:`cli <cli:Command-Line Interface>` but is also available to developers that wish
 to make use of the data Streamlink can retrieve in their own application.
 
 
@@ -15,7 +13,7 @@ The simplest use of the Streamlink API looks like this:
 .. code-block:: python
 
     >>> import streamlink
-    >>> streams = streamlink.streams("http://twitch.tv/day9tv")
+    >>> streams = streamlink.streams("https://twitch.tv/day9tv")
 
 This simply attempts to find a plugin and use it to extract streams from
 the URL. This works great in simple cases but if you want more
@@ -26,13 +24,13 @@ The returned value is a dict containing :class:`Stream <stream.Stream>` objects:
 .. code-block:: python
 
     >>> streams
-    {'best': <HLSStream('http://video11.fra01.hls.twitch.tv/ ...')>,
-     'high': <HLSStream('http://video11.fra01.hls.twitch.tv/ ...')>,
-     'low': <HLSStream('http://video11.fra01.hls.twitch.tv/ ...')>,
-     'medium': <HLSStream('http://video11.fra01.hls.twitch.tv/ ...')>,
-     'mobile': <HLSStream('http://video11.fra01.hls.twitch.tv/ ...')>,
-     'source': <HLSStream('http://video11.fra01.hls.twitch.tv/ ...')>,
-     'worst': <HLSStream('http://video11.fra01.hls.twitch.tv/ ...')>}
+    {'best': <HLSStream('https://video11.fra01.hls.twitch.tv/ ...')>,
+     'high': <HLSStream('https://video11.fra01.hls.twitch.tv/ ...')>,
+     'low': <HLSStream('https://video11.fra01.hls.twitch.tv/ ...')>,
+     'medium': <HLSStream('https://video11.fra01.hls.twitch.tv/ ...')>,
+     'mobile': <HLSStream('https://video11.fra01.hls.twitch.tv/ ...')>,
+     'source': <HLSStream('https://video11.fra01.hls.twitch.tv/ ...')>,
+     'worst': <HLSStream('https://video11.fra01.hls.twitch.tv/ ...')>}
 
 
 If no plugin for the URL is found, a :exc:`NoPluginError` will be raised.
@@ -60,8 +58,8 @@ If an error occurs while opening a stream, a :exc:`StreamError` will be raised.
 Inspecting streams
 ------------------
 
-It's also possible to inspect streams internal parameters, see
-:ref:`api-stream-subclasses` to see what attributes are available
+It's also possible to inspect streams internal parameters, go to
+:ref:`Stream subclasses <api:Stream subclasses>` to see what attributes are available
 for inspection for each stream type.
 
 For example this is a :class:`HLSStream <stream.HLSStream>` object which
@@ -70,7 +68,7 @@ contains a `url` attribute.
 .. code-block:: python
 
     >>> stream.url
-    'http://video38.ams01.hls.twitch.tv/hls11/ ...'
+    'https://video38.ams01.hls.twitch.tv/hls11/ ...'
 
 
 Session object
@@ -89,7 +87,7 @@ You can then extract streams like this:
 
 .. code-block:: python
 
-    >>> streams = session.streams("http://twitch.tv/day9tv")
+    >>> streams = session.streams("https://twitch.tv/day9tv")
 
 or set options like this:
 
@@ -111,6 +109,6 @@ This example uses the `PyGObject`_ module to playback a stream using the
 `GStreamer`_ framework.
 
 .. _PyGObject: https://wiki.gnome.org/action/show/Projects/PyGObject
-.. _GStreamer: http://gstreamer.freedesktop.org/
+.. _GStreamer: https://gstreamer.freedesktop.org/
 
 .. literalinclude:: ../examples/gst-player.py
diff --git a/docs/cli.rst b/docs/cli.rst
index 40cc7dba..6058c89c 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -1,5 +1,3 @@
-.. _cli:
-
 Command-Line Interface
 ======================
 
@@ -8,13 +6,13 @@ Tutorial
 
 Streamlink is a command-line application, which means that the commands described
 here should be typed into a terminal. On Windows, you have to open either the
-`command prompt`_ or the `PowerShell`_, on macOS open the `Terminal`_ app
+`Command Prompt`_, `PowerShell`_ or `Windows Terminal`_, on macOS open the `Terminal <macOS-Terminal>`_ app,
 and if you're on Linux or BSD you probably already know the drill.
 
 The way Streamlink works is that it's only a means to extract and transport
 the streams, and the playback is done by an external video player. Streamlink
 works best with `VLC`_ or `mpv`_, which are also cross-platform, but other players
-may be compatible too, see the :ref:`Players` page for a complete overview.
+may be compatible too, see the :ref:`Players <players:Players>` page for a complete overview.
 
 Now to get into actually using Streamlink, let's say you want to watch the
 stream located on twitch.tv/day9tv, you start off by telling Streamlink
@@ -69,20 +67,19 @@ specify ``worst`` to get the lowest quality.
 Now that you have a basic grasp of how Streamlink works, you may want to look
 into customizing it to your own needs, such as:
 
-- Creating a :ref:`configuration file <cli-streamlinkrc>` of options you
+- Creating a :ref:`configuration file <cli:Configuration file>` of options you
   want to use
-- Setting up your player to :ref:`cache some data <issues-player_caching>`
+- Setting up your player to :ref:`cache some data <issues:Streams are buffering/lagging>`
   before playing the stream to help avoiding buffering issues
 
 
-.. _command prompt: http://windows.microsoft.com/en-us/windows/command-prompt-faq#1TC=windows-8
-.. _PowerShell: http://www.microsoft.com/powershell
-.. _Terminal: http://en.wikipedia.org/wiki/Terminal_(OS_X)
-.. _VLC: http://videolan.org/
-.. _mpv: http://mpv.io/
-
+.. _Command Prompt: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/windows-commands
+.. _PowerShell: https://docs.microsoft.com/en-us/powershell/
+.. _Windows Terminal: https://docs.microsoft.com/en-us/windows/terminal/get-started
+.. _macOS Terminal: https://support.apple.com/guide/terminal/welcome/mac
+.. _VLC: https://videolan.org/
+.. _mpv: https://mpv.io/
 
-.. _cli-streamlinkrc:
 
 Configuration file
 ------------------
@@ -118,7 +115,7 @@ Syntax
 ^^^^^^
 
 The config file is a simple text file and should contain one
-:ref:`command-line option <cli-options>` (omitting the dashes) per
+:ref:`command-line option <cli:Command-line usage>` (omitting the dashes) per
 line in the format::
 
   option=value
@@ -166,7 +163,7 @@ Unix-like (POSIX) - $XDG_CONFIG_HOME/streamlink/config\ **.twitch**
 Windows           %APPDATA%\\streamlink\\streamlinkrc\ **.youtube**
 ================= ====================================================
 
-Have a look at the :ref:`list of plugins <plugin_matrix>` to see
+Have a look at the :ref:`list of plugins <plugin_matrix:Plugins>` to see
 the name of each built-in plugin.
 
 
@@ -184,7 +181,7 @@ You can login like this:
 
 .. sourcecode:: console
 
-    $ streamlink --crunchyroll-username=xxxx --crunchyroll-password=xxx http://crunchyroll.com/a-crunchyroll-episode-link
+    $ streamlink --crunchyroll-username=xxxx --crunchyroll-password=xxx https://crunchyroll.com/a-crunchyroll-episode-link
 
 .. note::
 
@@ -195,7 +192,7 @@ asking your username and password again.
 
 Nevertheless, these credentials are valid for a limited amount of time, so it
 might be a good idea to save your username and password in your
-:ref:`configuration file <cli-streamlinkrc>` anyway.
+:ref:`configuration file <cli:Configuration file>` anyway.
 
 .. warning::
 
@@ -220,8 +217,6 @@ For this, the plugin provides the :option:`--crunchyroll-purge-credentials`
 option, which removes your saved session and credentials and tries to log
 in again using your username and password.
 
-.. _cli-funimationnow:
-
 Authenticating with FunimationNow
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 Like Crunchyroll, the FunimationNow plugin requires authenticating with a premium account to access some
@@ -239,7 +234,7 @@ For example:
     There are multiple ways to retrieve the required cookie.  For more
     information on browser cookies, please consult the following:
 
-    - `What are cookies? <http://www.whatarecookies.com/view.asp>`_
+    - `What are cookies? <https://en.wikipedia.org/wiki/HTTP_cookie>`_
 
 Sideloading plugins
 -------------------
@@ -324,7 +319,6 @@ Progressive HTTP, HTTPS, etc   httpstream:// [1]_
 
 .. [1] supports local files using the file:// protocol
 .. [2] Dynamic Adaptive Streaming over HTTP
-.. _cli-options:
 
 Proxy Support
 -------------
diff --git a/docs/conf.py b/docs/conf.py
index 6310b629..c54b1632 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -16,7 +16,16 @@ sys.path.insert(0, os.path.abspath('.'))
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'ext_argparse', 'ext_github', 'ext_releaseref', 'recommonmark']
+extensions = [
+    'sphinx.ext.autodoc',
+    'sphinx.ext.autosectionlabel',
+    'ext_argparse',
+    'ext_github',
+    'ext_releaseref',
+    'recommonmark'
+]
+
+autosectionlabel_prefix_document = True
 
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
diff --git a/docs/ext_argparse.py b/docs/ext_argparse.py
index 6a9ed17b..b81f2c74 100644
--- a/docs/ext_argparse.py
+++ b/docs/ext_argparse.py
@@ -2,9 +2,6 @@
 
 Inspired by sphinxcontrib.autoprogram but with a few differences:
 
-- Instead of relying on private argparse structures uses hooking
-  to extract information from a argparse parser.
-
 - Contains some simple pre-processing on the help messages to make
   the Sphinx version a bit prettier.
 
@@ -13,7 +10,6 @@ Inspired by sphinxcontrib.autoprogram but with a few differences:
 import argparse
 import re
 
-from collections import namedtuple
 from textwrap import dedent
 
 from docutils import nodes
@@ -23,9 +19,6 @@ from docutils.statemachine import ViewList
 from sphinx.util.nodes import nested_parse_with_titles
 
 
-_ArgumentParser = argparse.ArgumentParser
-_Argument = namedtuple("Argument", ["args", "options"])
-
 _block_re = re.compile(r":\n{2}\s{2}")
 _default_re = re.compile(r"Default is (.+)\.\n")
 _note_re = re.compile(r"Note: (.*)(?:\n\n|\n*$)", re.DOTALL)
@@ -34,27 +27,8 @@ _option_re = re.compile(r"(?:^|(?<=\s))(--\w[\w-]*\w)\b")
 _prog_re = re.compile(r"%\(prog\)s")
 
 
-class ArgumentParser(object):
-    def __init__(self, *args, **kwargs):
-        self.args = args
-        self.kwargs = kwargs
-        self.groups = []
-        self.arguments = []
-
-    def add_argument(self, *args, **options):
-        if not options.get("help") == argparse.SUPPRESS:
-            self.arguments.append(_Argument(args, options))
-
-    def add_argument_group(self, *args, **options):
-        group = ArgumentParser(*args, **options)
-        self.groups.append(group)
-        return group
-
-
 def get_parser(module_name, attr):
-    argparse.ArgumentParser = ArgumentParser
     module = __import__(module_name, globals(), locals(), [attr])
-    argparse.ArgumentParser = _ArgumentParser
     parser = getattr(module, attr)
     return parser if not(callable(parser)) else parser.__call__()
 
@@ -110,34 +84,45 @@ class ArgparseDirective(Directive):
         return indent(help)
 
     def generate_group_rst(self, group):
-        for arg in group.arguments:
-            help = arg.options.get("help")
-            metavar = arg.options.get("metavar")
+        for action in group._group_actions:
+            # don't document suppressed parameters
+            if action.help == argparse.SUPPRESS:
+                continue
 
+            metavar = action.metavar
             if isinstance(metavar, tuple):
                 metavar = " ".join(metavar)
 
-            if metavar:
-                options = []
-                for a in arg.args:
-                    if a.startswith("-"):
-                        if arg.options.get("nargs") == "?":
-                            metavar = "[{0}]".format(metavar)
-                        options.append("{0} {1}".format(a, metavar))
-                    else:
-                        options.append(metavar)
+            options = []
+            # parameter(s) with metavar
+            if action.option_strings and metavar:
+                for arg in action.option_strings:
+                    # optional parameter value
+                    if action.nargs == "?":
+                        metavar = f"[{metavar}]"
+                    options.append(f"{arg} {metavar}")
+            # positional parameter
+            elif metavar:
+                options.append(metavar)
+            # parameter(s) without metavar
             else:
-                options = arg.args
+                options += action.option_strings
 
-            yield ".. option:: {0}".format(", ".join(options))
+            yield f".. option:: {', '.join(options)}"
             yield ""
-            for line in self.process_help(help).split("\n"):
+            for line in self.process_help(action.help).split("\n"):
                 yield line
             yield ""
+            if hasattr(action, "plugins") and len(action.plugins) > 0:
+                yield f"    **Supported plugins:** {', '.join(action.plugins)}"
+                yield ""
 
     def generate_parser_rst(self, parser):
-        for group in parser.groups:
-            title = group.args[0]
+        for group in parser._action_groups:
+            # Exclude empty groups
+            if len(group._group_actions) == 0:
+                continue
+            title = group.title
             yield ""
             yield title
             yield "^" * len(title)
@@ -150,9 +135,9 @@ class ArgparseDirective(Directive):
         parser = get_parser(module, attr)
 
         self._available_options = []
-        for group in parser.groups:
-            for arg in group.arguments:
-                self._available_options += arg.args
+        for action in parser._actions:
+            # positional parameters have an empty option_strings list
+            self._available_options += action.option_strings or [action.dest]
 
         node = nodes.section()
         node.document = self.state.document
diff --git a/docs/index.rst b/docs/index.rst
index 1c31123a..69abeb9e 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,11 +1,11 @@
 Overview
 --------
 
-Streamlink is a :ref:`command-line utility <cli>` which pipes video streams
+Streamlink is a :ref:`command-line utility <cli:Command-Line Interface>` which pipes video streams
 from various services into a video player, such as `VLC`_.
 The main purpose of Streamlink is to avoid resource-heavy and unoptimized websites,
 while still allowing the user to enjoy various streamed content.
-There is also an :ref:`API <api_guide>` available for developers who want access
+There is also an :ref:`API <api_guide:API Guide>` available for developers who want access
 to the stream data.
 
 This project was forked from Livestreamer, which is no longer maintained.
@@ -33,7 +33,7 @@ to be easily added. Most of the big streaming services are supported, such as:
 - `Dailymotion.com <https://www.dailymotion.com/live>`_
 
 ... and many more. A full list of plugins currently included can be found
-on the :ref:`plugin_matrix` page.
+on the :ref:`Plugins <plugin_matrix:Plugins>` page.
 
 Quickstart
 ----------
@@ -53,7 +53,7 @@ For more in-depth usage and install instructions, please refer to the `User guid
 User guide
 ----------
 
-Streamlink is made up of two parts, a :ref:`cli` and a library :ref:`API <api>`.
+Streamlink is made up of two parts, a :ref:`cli <cli:Command-Line Interface>` and a library :ref:`API <api:API Reference>`.
 See their respective sections for more information on how to use them.
 
 .. toctree::
diff --git a/docs/install.rst b/docs/install.rst
index 59382d5b..4c40e26e 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -1,5 +1,3 @@
-.. _install:
-
 .. |br| raw:: html
 
   <br />
@@ -140,9 +138,9 @@ Distribution                         Installing
 .. _Debian (stable): https://packages.debian.org/unstable/streamlink
 .. _Fedora: https://apps.fedoraproject.org/packages/python-streamlink
 .. _Gentoo Linux: https://packages.gentoo.org/package/net-misc/streamlink
-.. _NetBSD (pkgsrc): http://pkgsrc.se/multimedia/streamlink
+.. _NetBSD (pkgsrc): https://pkgsrc.se/multimedia/streamlink
 .. _NixOS: https://github.com/NixOS/nixpkgs/tree/master/pkgs/applications/video/streamlink
-.. _OpenBSD: http://openports.se/multimedia/streamlink
+.. _OpenBSD: https://openports.se/multimedia/streamlink
 .. _Solus: https://dev.solus-project.com/source/streamlink/
 .. _Ubuntu: http://ppa.launchpad.net/nilarimogard/webupd8/ubuntu/pool/main/s/streamlink/
 .. _Void: https://github.com/void-linux/void-packages/tree/master/srcpkgs/streamlink
@@ -328,8 +326,8 @@ With these two environment variables it is possible to use `pycrypto`_ instead o
 
 .. _Python: https://www.python.org/
 .. _python-setuptools: https://pypi.org/project/setuptools/
-.. _python-requests: http://python-requests.org/
-.. _RTMPDump: http://rtmpdump.mplayerhq.hu/
+.. _python-requests: https://requests.readthedocs.io/en/master/
+.. _RTMPDump: https://rtmpdump.mplayerhq.hu/
 .. _pycountry: https://pypi.org/project/pycountry/
 .. _pycrypto: https://www.dlitz.net/software/pycrypto/
 .. _pycryptodome: https://pycryptodome.readthedocs.io/en/latest/
@@ -357,6 +355,17 @@ Release                              Notes
                                      See the `commit log`_ for a list of changes since the last stable release.
 ==================================== ====================================
 
+.. note::
+
+    **The Streamlink installer for Windows is currently based on Python 3.8.** |br|
+    Versions of Windows prior to 7 are not supported.
+
+    Be aware that the packages for `Chocolatey`_ and the `Windows Package Manager`_ are just wrappers
+    around the stable installer and thus depend on Windows 7+ as well.
+
+    Alternatively, :ref:`Streamlink can be installed via python-pip <install:PyPI package and source code>`
+    in a :ref:`compatible <install:Dependencies>` Python environment.
+
 .. _Stable release:
 .. _GitHub releases page: https://github.com/streamlink/streamlink/releases/latest
 .. _Development build:
diff --git a/docs/issues.rst b/docs/issues.rst
index 21a6eb4f..ee9abf51 100644
--- a/docs/issues.rst
+++ b/docs/issues.rst
@@ -1,10 +1,6 @@
-.. _issues:
-
 Common issues
 =============
 
-.. _issues-player_caching:
-
 Streams are buffering/lagging
 -----------------------------
 
diff --git a/docs/players.rst b/docs/players.rst
index 1ddf64ac..7c83da4e 100644
--- a/docs/players.rst
+++ b/docs/players.rst
@@ -1,6 +1,3 @@
-.. _players:
-
-
 Players
 =======
 
@@ -29,13 +26,13 @@ modes.
 ===================================================== ========== ========== ====
 Name                                                  Stdin Pipe Named Pipe HTTP
 ===================================================== ========== ========== ====
-`Daum Pot Player <http://potplayer.daum.net>`_        Yes        No         Yes [1]_
-`MPC-HC <http://mpc-hc.org/>`_                        Yes [2]_   No         Yes [1]_
-`MPlayer <http://mplayerhq.hu>`_                      Yes        Yes        Yes
-`mpv <http://mpv.io>`_                                Yes        Yes        Yes
-`QuickTime <http://apple.com/quicktime>`_             No         No         No
-`VLC media player <http://videolan.org>`_             Yes [3]_   Yes        Yes
-OMXPlayer                                             No         Yes        Yes [4]_
+`Daum Pot Player`_                                    Yes        No         Yes [1]_
+`MPC-HC`_                                             Yes [2]_   No         Yes [1]_
+`MPlayer`_                                            Yes        Yes        Yes
+`mpv`_                                                Yes        Yes        Yes
+`OMXPlayer`_                                          No         Yes        Yes [4]_
+`QuickTime`_                                          No         No         No
+`VLC media player`_                                   Yes [3]_   Yes        Yes
 ===================================================== ========== ========== ====
 
 .. [1] :option:`--player-continuous-http` must be used.
@@ -58,6 +55,14 @@ OMXPlayer                                             No         Yes        Yes
        Other stream types may not work as expected, it is recommended that
        :option:`--player-fifo` be used.
 
+.. _Daum Pot Player: https://potplayer.daum.net
+.. _MPC-HC: https://mpc-hc.org/
+.. _MPlayer: https://mplayerhq.hu
+.. _mpv: https://mpv.io
+.. _OMXPlayer: https://www.raspberrypi.org/documentation/raspbian/applications/omxplayer.md
+.. _QuickTime: https://apple.com/quicktime
+.. _VLC media player: https://videolan.org
+
 
 Known issues and workarounds
 ----------------------------
@@ -81,7 +86,7 @@ reported to work.
 
 MPlayer tries to play Twitch streams at the wrong FPS
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-This is a bug in MPlayer, using the MPlayer fork `mpv <http://mpv.io>`_ instead
+This is a bug in MPlayer, using the MPlayer fork `mpv`_ instead
 is recommended.
 
 VLC hangs when buffering and no playback starts
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 06548f0f..4c826f9f 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -1,6 +1,3 @@
-.. _plugin_matrix:
-
-
 Plugins
 =======
 
@@ -82,7 +79,7 @@ euronews                euronews.com         Yes   No
 facebook                facebook.com         Yes   Yes
 filmon                  filmon.com           Yes   Yes   Only SD quality streams.
 foxtr                   fox.com.tr           Yes   No
-funimationnow           - funimation.com     --    Yes   :ref:`Requires session cookies <cli-funimationnow>`
+funimationnow           - funimation.com     --    Yes   :ref:`Requires session cookies <cli:Authenticating with FunimationNow>`
                         - funimationnow.uk
 galatasaraytv           galatasaray.com      Yes   No
 gardenersworld          gardenersworld.com   --    Yes
@@ -97,7 +94,7 @@ hitbox                  - hitbox.tv          Yes   Yes
 huajiao                 huajiao.com          Yes   No
 huomao                  - huomao.com         Yes   Yes
                         - huomao.tv
-huya                    huya.com             Yes   No    Temporarily only HLS streams available.
+huya                    huya.com             Yes   No
 idf1                    idf1.fr              Yes   Yes
 ine                     ine.com              ---   Yes
 invintus                player.invintus.com  Yes   Yes
diff --git a/src/streamlink/options.py b/src/streamlink/options.py
index 5a208c16..4c3f57a9 100644
--- a/src/streamlink/options.py
+++ b/src/streamlink/options.py
@@ -52,7 +52,7 @@ class Argument(object):
 
     """
     def __init__(self, name, required=False, requires=None, prompt=None, sensitive=False, argument_name=None,
-                 dest=None, **options):
+                 dest=None, is_global=False, **options):
         """
         :param name: name of the argument, without -- or plugin name prefixes, eg. ``"password"``, ``"mux-subtitles"``, etc.
         :param required (bool): if the argument is required for the plugin
@@ -73,12 +73,13 @@ class Argument(object):
         self.prompt = prompt
         self.sensitive = sensitive
         self._default = options.get("default")
+        self.is_global = is_global
 
     def _name(self, plugin):
         return self._argument_name or _normalise_argument_name("{0}-{1}".format(plugin, self.name))
 
     def argument_name(self, plugin):
-        return "--" + self._name(plugin)
+        return f"--{self.name if self.is_global else self._name(plugin)}"
 
     def namespace_dest(self, plugin):
         return _normalise_option_name(self._name(plugin))
diff --git a/src/streamlink/plugins/funimationnow.py b/src/streamlink/plugins/funimationnow.py
index 49b8fc4c..c7391e0e 100644
--- a/src/streamlink/plugins/funimationnow.py
+++ b/src/streamlink/plugins/funimationnow.py
@@ -176,15 +176,7 @@ class FunimationNow(Plugin):
             Default is "english".
             """
         ),
-        PluginArgument(
-            "mux-subtitles",
-            argument_name="funimation-mux-subtitles",
-            action="store_true",
-            help="""
-            Enable automatically including available subtitles in to the output
-            stream.
-            """
-        )
+        PluginArgument("mux-subtitles", is_global=True)
     )
 
     url_re = re.compile(r"""
diff --git a/src/streamlink/plugins/huya.py b/src/streamlink/plugins/huya.py
index f2ed8100..52a07aa5 100644
--- a/src/streamlink/plugins/huya.py
+++ b/src/streamlink/plugins/huya.py
@@ -1,37 +1,76 @@
+import base64
+import logging
 import re
+from html import unescape as html_unescape
 
 from streamlink.plugin import Plugin
-from streamlink.plugin.api import useragents, validate
-from streamlink.stream import HLSStream
-from streamlink.utils import update_scheme
+from streamlink.plugin.api import validate
+from streamlink.stream import HTTPStream
+from streamlink.utils import parse_json
 
-HUYA_URL = "http://m.huya.com/%s"
+log = logging.getLogger(__name__)
 
-_url_re = re.compile(r'https?://(www\.)?huya.com/(?P<channel>[^/]+)')
-_hls_re = re.compile(r'liveLineUrl\s*=\s*"(?P<url>[^"]+)"')
 
-_hls_schema = validate.Schema(
-    validate.transform(_hls_re.search),
-    validate.any(None, validate.get("url")),
-    validate.transform(lambda v: update_scheme("http://", v)),
-    validate.url()
-)
+class Huya(Plugin):
+    _re_url = re.compile(r'https?://(?:www\.)?huya\.com/(?P<channel>[^/]+)')
+    _re_stream = re.compile(r'"stream"\s?:\s?"([^"]+)"')
+    _schema_data = validate.Schema(
+        {
+            # 'status': int,
+            # 'msg': validate.any(None, str),
+            'data': [{
+                'gameStreamInfoList': [{
+                    'sCdnType': str,
+                    'sStreamName': str,
+                    'sFlvUrl': str,
+                    'sFlvUrlSuffix': str,
+                    'sFlvAntiCode': validate.all(str, validate.transform(lambda v: html_unescape(v))),
+                    # 'sHlsUrl': str,
+                    # 'sHlsUrlSuffix': str,
+                    # 'sHlsAntiCode': validate.all(str, validate.transform(lambda v: html_unescape(v))),
+                    validate.optional('iIsMultiStream'): int,
+                    'iPCPriorityRate': int,
+                }]
+            }],
+            # 'vMultiStreamInfo': [{
+            #    'sDisplayName': str,
+            #    'iBitRate': int,
+            # }],
+        },
+        validate.get('data'),
+        validate.get(0),
+        validate.get('gameStreamInfoList'),
+    )
+    QUALITY_WEIGHTS = {}
 
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._re_url.match(url) is not None
 
-class Huya(Plugin):
     @classmethod
-    def can_handle_url(self, url):
-        return _url_re.match(url)
+    def stream_weight(cls, key):
+        weight = cls.QUALITY_WEIGHTS.get(key)
+        if weight:
+            return weight, 'huya'
+
+        return Plugin.stream_weight(key)
 
     def _get_streams(self):
-        match = _url_re.match(self.url)
-        channel = match.group("channel")
+        res = self.session.http.get(self.url)
+        data = self._re_stream.search(res.text)
+
+        if not data:
+            return
 
-        self.session.http.headers.update({"User-Agent": useragents.IPAD})
-        # Some problem with SSL on huya.com now, do not use https
+        data = parse_json(base64.b64decode(data.group(1)), schema=self._schema_data)
+        for info in data:
+            log.trace(f'{info!r}')
+            flv_url = f'{info["sFlvUrl"]}/{info["sStreamName"]}.{info["sFlvUrlSuffix"]}?{info["sFlvAntiCode"]}'
+            name = f'source_{info["sCdnType"].lower()}'
+            self.QUALITY_WEIGHTS[name] = info['iPCPriorityRate']
+            yield name, HTTPStream(self.session, flv_url)
 
-        hls_url = self.session.http.get(HUYA_URL % channel, schema=_hls_schema)
-        yield "live", HLSStream(self.session, hls_url)
+        log.debug(f'QUALITY_WEIGHTS: {self.QUALITY_WEIGHTS!r}')
 
 
 __plugin__ = Huya
diff --git a/src/streamlink/plugins/nrk.py b/src/streamlink/plugins/nrk.py
index a0454215..09c39020 100644
--- a/src/streamlink/plugins/nrk.py
+++ b/src/streamlink/plugins/nrk.py
@@ -1,64 +1,106 @@
+import logging
 import re
 from urllib.parse import urljoin
 
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import validate
-from streamlink.stream import HLSStream
-
-COOKIE_PARAMS = (
-    "devicetype=desktop&"
-    "preferred-player-odm=hlslink&"
-    "preferred-player-live=hlslink"
-)
-
-_id_re = re.compile(r"/(?:program|direkte|serie/[^/]+)/([^/]+)")
-_url_re = re.compile(r"https?://(tv|radio).nrk.no/")
-_api_baseurl_re = re.compile(r'''apiBaseUrl:\s*["'](?P<baseurl>[^"']+)["']''')
-
-_schema = validate.Schema(
-    validate.transform(_api_baseurl_re.search),
-    validate.any(
-        None,
-        validate.all(
-            validate.get("baseurl"),
-            validate.url(
-                scheme="http"
-            )
-        )
-    )
-)
-
-_mediaelement_schema = validate.Schema({
-    "mediaUrl": validate.url(
-        scheme="http",
-        path=validate.endswith(".m3u8")
-    )
-})
+from streamlink.stream import HLSStream, HTTPStream
+
+log = logging.getLogger(__name__)
 
 
 class NRK(Plugin):
+    _psapi_url = 'https://psapi.nrk.no'
+    # Program type to manifest type mapping
+    _program_type_map = {
+        'direkte': 'channel',
+        'serie': 'program',
+        'program': 'program',
+        'podkast': 'podcast',
+    }
+
+    _program_id_re = re.compile(r'<meta property="nrk:program-id" content="([^"]+)"')
+    _url_re = re.compile(r"https?://(?:tv|radio)\.nrk\.no/(program|direkte|serie|podkast)(?:/.+)?/([^/]+)")
+
+    _playable_schema = validate.Schema(validate.any(
+        {
+            "playable": {
+                "assets": [{
+                    "url": validate.url(),
+                    "format": str,
+                }],
+            },
+            "statistics": {
+                validate.optional("luna"): validate.any(None, {
+                    "data": {
+                        "title": str,
+                        "category": validate.any(None, str),
+                    },
+                }),
+            },
+        },
+        {
+            "nonPlayable": {
+                "reason": str,
+            },
+        },
+    ))
+
+    category = None
+    title = None
+
     @classmethod
-    def can_handle_url(self, url):
-        return _url_re.match(url)
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    def get_category(self):
+        return self.category
+
+    def get_title(self):
+        return self.title
 
     def _get_streams(self):
-        # Get the stream type from the url (tv/radio).
-        stream_type = _url_re.match(self.url).group(1).upper()
-        cookie = {
-            "NRK_PLAYER_SETTINGS_{0}".format(stream_type): COOKIE_PARAMS
-        }
+        # Construct manifest URL for this program.
+        program_type, program_id = self._url_re.match(self.url).groups()
+        manifest_type = self._program_type_map.get(program_type)
+        if manifest_type is None:
+            log.error(f"Unknown program type '{program_type}'")
+            return None
 
-        # Construct API URL for this program.
-        baseurl = self.session.http.get(self.url, cookies=cookie, schema=_schema)
-        program_id = _id_re.search(self.url).group(1)
+        # Fetch program_id.
+        res = self.session.http.get(self.url)
+        m = self._program_id_re.search(res.text)
+        if m is not None:
+            program_id = m.group(1)
+        elif program_id is None:
+            log.error("Could not extract program ID from URL")
+            return None
+
+        manifest_url = urljoin(self._psapi_url, f"playback/manifest/{manifest_type}/{program_id}")
 
         # Extract media URL.
-        json_url = urljoin(baseurl, "mediaelement/{0}".format(program_id))
-        res = self.session.http.get(json_url, cookies=cookie)
-        media_element = self.session.http.json(res, schema=_mediaelement_schema)
-        media_url = media_element["mediaUrl"]
+        res = self.session.http.get(manifest_url)
+        manifest = self.session.http.json(res, schema=self._playable_schema)
+        if 'nonPlayable' in manifest:
+            reason = manifest["nonPlayable"]["reason"]
+            log.error(f"Not playable ({reason})")
+            return None
+        self._set_metadata(manifest)
+        asset = manifest['playable']['assets'][0]
+
+        # Some streams such as podcasts are not HLS but plain files.
+        if asset['format'] == 'HLS':
+            return HLSStream.parse_variant_playlist(self.session, asset['url'])
+        else:
+            return [("live", HTTPStream(self.session, asset['url']))]
 
-        return HLSStream.parse_variant_playlist(self.session, media_url)
+    def _set_metadata(self, manifest):
+        luna = manifest.get("statistics").get("luna")
+        if not luna:
+            return
+        data = luna["data"]
+        self.category = data.get("category")
+        self.title = data.get("title")
 
 
 __plugin__ = NRK
diff --git a/src/streamlink/plugins/pluzz.py b/src/streamlink/plugins/pluzz.py
index 19e38022..f1d96712 100644
--- a/src/streamlink/plugins/pluzz.py
+++ b/src/streamlink/plugins/pluzz.py
@@ -85,13 +85,7 @@ class Pluzz(Plugin):
     _player_schema = validate.Schema({'result': validate.url()})
 
     arguments = PluginArguments(
-        PluginArgument(
-            "mux-subtitles",
-            action="store_true",
-            help="""
-        Automatically mux available subtitles in to the output stream.
-        """
-        )
+        PluginArgument("mux-subtitles", is_global=True)
     )
 
     @classmethod
diff --git a/src/streamlink/plugins/rtve.py b/src/streamlink/plugins/rtve.py
index 28d573ae..357d5511 100644
--- a/src/streamlink/plugins/rtve.py
+++ b/src/streamlink/plugins/rtve.py
@@ -94,14 +94,9 @@ class Rtve(Plugin):
         validate.get("page"),
         validate.get("items"),
         validate.get(0))
+
     arguments = PluginArguments(
-        PluginArgument(
-            "mux-subtitles",
-            action="store_true",
-            help="""
-        Automatically mux available subtitles in to the output stream.
-        """
-        )
+        PluginArgument("mux-subtitles", is_global=True)
     )
 
     @classmethod
diff --git a/src/streamlink/plugins/svtplay.py b/src/streamlink/plugins/svtplay.py
index 049c42e5..a063f61c 100644
--- a/src/streamlink/plugins/svtplay.py
+++ b/src/streamlink/plugins/svtplay.py
@@ -47,11 +47,7 @@ class SVTPlay(Plugin):
     })
 
     arguments = PluginArguments(
-        PluginArgument(
-            'mux-subtitles',
-            action='store_true',
-            help="Automatically mux available subtitles in to the output stream.",
-        ),
+        PluginArgument("mux-subtitles", is_global=True)
     )
 
     @classmethod
diff --git a/src/streamlink/plugins/vimeo.py b/src/streamlink/plugins/vimeo.py
index 95180bee..f4d99d26 100644
--- a/src/streamlink/plugins/vimeo.py
+++ b/src/streamlink/plugins/vimeo.py
@@ -51,11 +51,7 @@ class Vimeo(Plugin):
     )
 
     arguments = PluginArguments(
-        PluginArgument(
-            "mux-subtitles",
-            action="store_true",
-            help="Automatically mux available subtitles in to the output stream.",
-        )
+        PluginArgument("mux-subtitles", is_global=True)
     )
 
     @classmethod
diff --git a/src/streamlink/session.py b/src/streamlink/session.py
index c8a9fd30..dc1add5f 100644
--- a/src/streamlink/session.py
+++ b/src/streamlink/session.py
@@ -81,6 +81,7 @@ class Streamlink(object):
             "ffmpeg-ffmpeg": None,
             "ffmpeg-video-transcode": "copy",
             "ffmpeg-audio-transcode": "copy",
+            "mux-subtitles": False,
             "locale": None,
             "user-input-requester": None
         })
@@ -210,6 +211,9 @@ class Streamlink(object):
                                  audio when muxing with ffmpeg
                                  e.g. ``aac``
 
+        mux-subtitles            (bool) Mux available subtitles into the
+                                 output stream.
+
         stream-segment-attempts  (int) How many attempts should be done
                                  to download each segment, default: ``3``.
                                  General option used by streams not
diff --git a/src/streamlink_cli/argparser.py b/src/streamlink_cli/argparser.py
index d71968f4..51fc18cd 100644
--- a/src/streamlink_cli/argparser.py
+++ b/src/streamlink_cli/argparser.py
@@ -1097,6 +1097,15 @@ def build_parser():
         Example: "aac"
         """
     )
+    transport.add_argument(
+        "--mux-subtitles",
+        action="store_true",
+        help="""
+        Automatically mux available subtitles into the output stream.
+
+        Needs to be supported by the used plugin.
+        """
+    )
 
     http = parser.add_argument_group("HTTP options")
     http.add_argument(
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index 7341825d..0af630ef 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -846,6 +846,9 @@ def setup_options():
     if args.ffmpeg_audio_transcode:
         streamlink.set_option("ffmpeg-audio-transcode", args.ffmpeg_audio_transcode)
 
+    if args.mux_subtitles:
+        streamlink.set_option("mux-subtitles", args.mux_subtitles)
+
     streamlink.set_option("subprocess-errorlog", args.subprocess_errorlog)
     streamlink.set_option("subprocess-errorlog-path", args.subprocess_errorlog_path)
     streamlink.set_option("locale", args.locale)
@@ -857,9 +860,23 @@ def setup_plugin_args(session, parser):
     plugin_args = parser.add_argument_group("Plugin options")
     for pname, plugin in session.plugins.items():
         defaults = {}
+
         for parg in plugin.arguments:
-            plugin_args.add_argument(parg.argument_name(pname), **parg.options)
-            defaults[parg.dest] = parg.default
+            if not parg.is_global:
+                plugin_args.add_argument(parg.argument_name(pname), **parg.options)
+                defaults[parg.dest] = parg.default
+            else:
+                pargdest = parg.dest
+                for action in parser._actions:
+                    # find matching global argument
+                    if pargdest != action.dest:
+                        continue
+                    defaults[pargdest] = action.default
+
+                    # add plugin to global argument
+                    plugins = getattr(action, "plugins", [])
+                    plugins.append(pname)
+                    setattr(action, "plugins", plugins)
 
         plugin.options = PluginOptions(defaults)
 
@@ -868,21 +885,26 @@ def setup_plugin_options(session, plugin):
     """Sets Streamlink plugin options."""
     pname = plugin.module
     required = OrderedDict({})
+
     for parg in plugin.arguments:
-        if parg.options.get("help") != argparse.SUPPRESS:
+        if parg.options.get("help") == argparse.SUPPRESS:
+            continue
+
+        value = getattr(args, parg.dest if parg.is_global else parg.namespace_dest(pname))
+        session.set_plugin_option(pname, parg.dest, value)
+
+        if not parg.is_global:
             if parg.required:
                 required[parg.name] = parg
-            value = getattr(args, parg.namespace_dest(pname))
-            session.set_plugin_option(pname, parg.dest, value)
             # if the value is set, check to see if any of the required arguments are not set
             if parg.required or value:
                 try:
                     for rparg in plugin.arguments.requires(parg.name):
                         required[rparg.name] = rparg
                 except RuntimeError:
-                    log.error("{0} plugin has a configuration error and the arguments "
-                              "cannot be parsed".format(pname))
+                    log.error(f"{pname} plugin has a configuration error and the arguments cannot be parsed")
                     break
+
     if required:
         for req in required.values():
             if not session.get_plugin_option(pname, req.dest):


Test patch:
diff --git a/tests/plugins/test_funimationnow.py b/tests/plugins/test_funimationnow.py
index a7d0a46c..ed20d9d0 100644
--- a/tests/plugins/test_funimationnow.py
+++ b/tests/plugins/test_funimationnow.py
@@ -34,10 +34,11 @@ class TestPluginFunimationNow(unittest.TestCase):
         }
 
         setup_plugin_args(session, parser)
-        self.assertSequenceEqual(plugin_parser.add_argument.mock_calls,
-                                 [call('--funimation-email', help=ANY),
-                                  call('--funimation-password', help=ANY),
-                                  call('--funimation-language',
-                                       choices=["en", "ja", "english", "japanese"],
-                                       default="english", help=ANY),
-                                  call('--funimation-mux-subtitles', action="store_true", help=ANY)])
+        self.assertSequenceEqual(
+            plugin_parser.add_argument.mock_calls,
+            [
+                call('--funimation-email', help=ANY),
+                call('--funimation-password', help=ANY),
+                call('--funimation-language', choices=["en", "ja", "english", "japanese"], default="english", help=ANY)
+            ]
+        )
diff --git a/tests/plugins/test_huya.py b/tests/plugins/test_huya.py
index c2f9deca..7deb0c34 100644
--- a/tests/plugins/test_huya.py
+++ b/tests/plugins/test_huya.py
@@ -11,11 +11,11 @@ class TestPluginHuya(unittest.TestCase):
             'https://www.huya.com/123123123',
         ]
         for url in should_match:
-            self.assertTrue(Huya.can_handle_url(url))
+            self.assertTrue(Huya.can_handle_url(url), url)
 
     def test_can_handle_url_negative(self):
         should_not_match = [
             'http://www.huya.com',
         ]
         for url in should_not_match:
-            self.assertFalse(Huya.can_handle_url(url))
+            self.assertFalse(Huya.can_handle_url(url), url)
diff --git a/tests/plugins/test_nrk.py b/tests/plugins/test_nrk.py
index f053baff..54f7c3cc 100644
--- a/tests/plugins/test_nrk.py
+++ b/tests/plugins/test_nrk.py
@@ -10,14 +10,24 @@ class TestPluginNRK(unittest.TestCase):
             'https://tv.nrk.no/direkte/nrk2',
             'https://tv.nrk.no/direkte/nrk3',
             'https://tv.nrk.no/direkte/nrksuper',
+
+            'https://tv.nrk.no/serie/nytt-paa-nytt/2020/MUHH43003020',
+            'https://tv.nrk.no/serie/kongelige-fotografer/sesong/1/episode/2/avspiller',
+
+            'https://tv.nrk.no/program/NNFA51102617',
+
             'https://radio.nrk.no/direkte/p1',
             'https://radio.nrk.no/direkte/p2',
+
+            'https://radio.nrk.no/podkast/oppdatert/l_5005d62a-7f4f-4581-85d6-2a7f4f2581f2',
         ]
         for url in should_match:
             self.assertTrue(NRK.can_handle_url(url))
 
     def test_can_handle_url_negative(self):
         should_not_match = [
+            'https://tv.nrk.no/',
+            'https://radio.nrk.no/',
             'https://nrk.no/',
         ]
         for url in should_not_match:
diff --git a/tests/test_options.py b/tests/test_options.py
index 8fc36a26..b4a8734b 100644
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -1,8 +1,9 @@
+import argparse
 import unittest
-from unittest.mock import Mock
+from unittest.mock import Mock, patch
 
 from streamlink.options import Argument, Arguments, Options
-from streamlink_cli.main import setup_plugin_args
+from streamlink_cli.main import setup_plugin_args, setup_plugin_options
 
 
 class TestOptions(unittest.TestCase):
@@ -112,13 +113,16 @@ class TestArguments(unittest.TestCase):
 
 
 class TestSetupOptions(unittest.TestCase):
-    def test_set_defaults(self):
+    def test_setup_plugin_args(self):
         session = Mock()
         plugin = Mock()
-        parser = Mock()
+        parser = argparse.ArgumentParser()
+        parser.add_argument("--global-arg1", default=123)
+        parser.add_argument("--global-arg2", default=456)
 
         session.plugins = {"mock": plugin}
         plugin.arguments = Arguments(
+            Argument("global-arg1", is_global=True),
             Argument("test1", default="default1"),
             Argument("test2", default="default2"),
             Argument("test3")
@@ -126,6 +130,45 @@ class TestSetupOptions(unittest.TestCase):
 
         setup_plugin_args(session, parser)
 
+        group = next((group for group in parser._action_groups if group.title == "Plugin options"), None)
+        self.assertIsNotNone(group, "Adds the 'Plugin options' arguments group")
+        self.assertEqual(
+            [item for action in group._group_actions for item in action.option_strings],
+            ["--mock-test1", "--mock-test2", "--mock-test3"],
+            "Only adds plugin arguments and ignores global argument references"
+        )
+
+        self.assertEqual(plugin.options.get("global-arg1"), 123)
+        self.assertEqual(plugin.options.get("global-arg2"), None)
         self.assertEqual(plugin.options.get("test1"), "default1")
         self.assertEqual(plugin.options.get("test2"), "default2")
         self.assertEqual(plugin.options.get("test3"), None)
+
+    def test_setup_plugin_options(self):
+        session = Mock()
+        plugin = Mock(module="plugin")
+        parser = argparse.ArgumentParser()
+        parser.add_argument("--foo-foo", default=123)
+
+        session.plugins = {"plugin": plugin}
+        session.set_plugin_option = lambda name, key, value: session.plugins[name].options.update({key: value})
+        plugin.arguments = Arguments(
+            Argument("foo-foo", is_global=True),
+            Argument("bar-bar", default=456),
+            Argument("baz-baz", default=789, help=argparse.SUPPRESS)
+        )
+
+        with patch("streamlink_cli.main.args") as args:
+            args.foo_foo = 321
+            args.plugin_bar_bar = 654
+            args.plugin_baz_baz = 987  # this wouldn't be set by the parser if the argument is suppressed
+
+            setup_plugin_args(session, parser)
+            self.assertEqual(plugin.options.get("foo_foo"), 123, "Sets the global-argument's default value")
+            self.assertEqual(plugin.options.get("bar_bar"), 456, "Sets the plugin-argument's default value")
+            self.assertEqual(plugin.options.get("baz_baz"), 789, "Sets the suppressed plugin-argument's default value")
+
+            setup_plugin_options(session, plugin)
+            self.assertEqual(plugin.options.get("foo_foo"), 321, "Sets the provided global-argument value")
+            self.assertEqual(plugin.options.get("bar_bar"), 654, "Sets the provided plugin-argument value")
+            self.assertEqual(plugin.options.get("baz_baz"), 789, "Doesn't set values of suppressed plugin-arguments")
diff --git a/tests/test_plugins_meta.py b/tests/test_plugins_meta.py
index dcd4c0d3..02791f00 100644
--- a/tests/test_plugins_meta.py
+++ b/tests/test_plugins_meta.py
@@ -58,3 +58,13 @@ class TestPluginMeta(unittest.TestCase):
             if pname not in self.built_in_plugins:
                 self.assertIn(pname, self.session.plugins.keys(),
                               "{0} is not a plugin but has tests".format(pname))
+
+    def test_plugin_has_valid_global_args(self):
+        from streamlink_cli.argparser import build_parser
+        parser = build_parser()
+        global_arg_dests = [action.dest for action in parser._actions]
+        for pname, plugin in self.session.plugins.items():
+            for parg in plugin.arguments:
+                if parg.is_global:
+                    self.assertIn(parg.dest, global_arg_dests,
+                                  f"{parg.name} from plugins.{pname} is not a valid global argument")