Repository: streamlink/streamlink
Pull request #3318 (2020-11-04 21:39:54)

plugins.nrk: fix/rewrite plugin
The TV and radio sites of nrk.no has been changed some time ago which
has left the NRK plugin broken. This change makes use of [PSAPI] to
bring both TV and radio back to working state, for both live and
on-demand shows.

[PSAPI]: https://psapi-we.nrk.no/documentation/


Patch:
diff --git a/docs/install.rst b/docs/install.rst
index 59382d5b..a9dc093d 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -177,6 +177,7 @@ Windows port. version                RosadinTV <RosadinTV at outlook.com> |br|
                                      beardypig <beardypig at protonmail.com>
 ==================================== ===========================================
 
+.. _PyPI package and source code:
 
 PyPI package and source code
 ----------------------------
@@ -285,6 +286,8 @@ using `virtualenv`_, which creates a user owned Python environment instead.
 
 .. _virtualenv: https://virtualenv.readthedocs.io/en/latest/
 
+.. _Dependencies:
+
 Dependencies
 ^^^^^^^^^^^^
 
@@ -357,6 +360,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<PyPI package and source code>`
+    in a :ref:`compatible<Dependencies>` Python environment.
+
 .. _Stable release:
 .. _GitHub releases page: https://github.com/streamlink/streamlink/releases/latest
 .. _Development build:
diff --git a/script/makeinstaller.sh b/script/makeinstaller.sh
index d17afd37..38558e8b 100755
--- a/script/makeinstaller.sh
+++ b/script/makeinstaller.sh
@@ -33,7 +33,7 @@ cd "${ROOT}"
 STREAMLINK_VERSION=$(python setup.py --version)
 STREAMLINK_VERSION_PLAIN="${STREAMLINK_VERSION%%+*}"
 STREAMLINK_INSTALLER="${1:-"streamlink-${STREAMLINK_VERSION/\+/_}"}"
-STREAMLINK_PYTHON_VERSION=3.6.6
+STREAMLINK_PYTHON_VERSION=3.8.6
 STREAMLINK_ASSETS_REPO="${STREAMLINK_ASSETS_REPO:-streamlink/streamlink-assets}"
 STREAMLINK_ASSETS_RELEASE="${STREAMLINK_ASSETS_RELEASE:-latest}"
 
@@ -118,7 +118,7 @@ packages=pkg_resources
          socks
          sockshandler
          isodate
-pypi_wheels=pycryptodome==3.6.4
+pypi_wheels=pycryptodome==3.9.9
 
 files=${ROOT}/win32/THIRD-PARTY.txt > \$INSTDIR
       ${ROOT}/build/lib/streamlink > \$INSTDIR\pkgs
diff --git a/src/streamlink/plugins/abweb.py b/src/streamlink/plugins/abweb.py
index 8e25da24..5cb52b78 100644
--- a/src/streamlink/plugins/abweb.py
+++ b/src/streamlink/plugins/abweb.py
@@ -1,39 +1,27 @@
 import logging
 import re
-import time
 
-from streamlink.cache import Cache
 from streamlink.exceptions import PluginError
 from streamlink.plugin import Plugin, PluginArgument, PluginArguments
-from streamlink.plugin.api import useragents
+from streamlink.plugin.api.utils import itertags
 from streamlink.stream import HLSStream
 from streamlink.utils import update_scheme
+from streamlink.utils.url import url_concat
 
 log = logging.getLogger(__name__)
 
 
 class ABweb(Plugin):
-    '''BIS Livestreams of french AB Groupe
-       http://www.abweb.com/BIS-TV-Online/
-    '''
-
-    login_url = 'http://www.abweb.com/BIS-TV-Online/Default.aspx'
+    url_l = 'https://www.abweb.com/BIS-TV-Online/identification.aspx?ReturnUrl=%2fBIS-TV-Online%2fbistvo-tele-universal.aspx'
 
     _url_re = re.compile(r'https?://(?:www\.)?abweb\.com/BIS-TV-Online/bistvo-tele-universal.aspx', re.IGNORECASE)
     _hls_re = re.compile(r'''["']file["']:\s?["'](?P<url>[^"']+\.m3u8[^"']+)["']''')
-    _iframe_re = re.compile(r'''<iframe[^>]+src=["'](?P<url>[^"']+)["']''')
-
-    _input_re = re.compile(r'''(<input[^>]+>)''')
-    _name_re = re.compile(r'''name=["']([^"']*)["']''')
-    _value_re = re.compile(r'''value=["']([^"']*)["']''')
-
-    expires_time = 3600 * 24
 
     arguments = PluginArguments(
         PluginArgument(
             "username",
-            required=True,
             requires=["password"],
+            sensitive=True,
             metavar="USERNAME",
             help="""
             The username associated with your ABweb account, required to access any
@@ -59,78 +47,38 @@ class ABweb(Plugin):
 
     def __init__(self, url):
         super().__init__(url)
-        self._session_attributes = Cache(filename='plugin-cache.json', key_prefix='abweb:attributes')
-        self._authed = self._session_attributes.get('ASP.NET_SessionId') and self._session_attributes.get('.abportail1')
-        self._expires = self._session_attributes.get('expires', time.time() + self.expires_time)
+        self._authed = (self.session.http.cookies.get('ASP.NET_SessionId', domain='.abweb.com')
+                        and self.session.http.cookies.get('.abportail1', domain='.abweb.com'))
 
     @classmethod
     def can_handle_url(cls, url):
         return cls._url_re.match(url) is not None
 
-    def set_expires_time_cache(self):
-        expires = time.time() + self.expires_time
-        self._session_attributes.set('expires', expires, expires=self.expires_time)
-
-    def get_iframe_url(self):
-        log.debug('search for an iframe')
-        res = self.session.http.get(self.url)
-        m = self._iframe_re.search(res.text)
-        if not m:
-            raise PluginError('No iframe found.')
-
-        iframe_url = m.group('url')
-        iframe_url = update_scheme('http://', iframe_url)
-        log.debug('IFRAME URL={0}'.format(iframe_url))
-        return iframe_url
+    def _login(self, username, password):
+        log.debug('Attempting to login.')
 
-    def get_hls_url(self, iframe_url):
-        log.debug('search for hls url')
-        res = self.session.http.get(iframe_url)
-        m = self._hls_re.search(res.text)
-        if not m:
-            raise PluginError('No playlist found.')
+        data = {}
+        for i in itertags(self.session.http.get(self.url_l).text, 'input'):
+            data[i.attributes.get('name')] = i.attributes.get('value', '')
 
-        return m and m.group('url')
+        if not data:
+            raise PluginError('Missing input data on login website.')
 
-    def _login(self, username, password):
-        '''login and update cached cookies'''
-        log.debug('login ...')
+        data.update({
+            'ctl00$ContentPlaceHolder1$Login1$UserName': username,
+            'ctl00$ContentPlaceHolder1$Login1$Password': password,
+            'ctl00$ContentPlaceHolder1$Login1$LoginButton.x': '0',
+            'ctl00$ContentPlaceHolder1$Login1$LoginButton.y': '0',
+            'ctl00$ContentPlaceHolder1$Login1$RememberMe': 'on',
+        })
 
-        res = self.session.http.get(self.login_url)
-        input_list = self._input_re.findall(res.text)
-        if not input_list:
-            raise PluginError('Missing input data on login website.')
+        self.session.http.post(self.url_l, data=data)
+        if (self.session.http.cookies.get('ASP.NET_SessionId') and self.session.http.cookies.get('.abportail1')):
+            for cookie in self.session.http.cookies:
+                # remove www from cookie domain
+                cookie.domain = '.abweb.com'
 
-        data = {}
-        for _input_data in input_list:
-            try:
-                _input_name = self._name_re.search(_input_data).group(1)
-            except AttributeError:
-                continue
-
-            try:
-                _input_value = self._value_re.search(_input_data).group(1)
-            except AttributeError:
-                _input_value = ''
-
-            data[_input_name] = _input_value
-
-        login_data = {
-            'ctl00$Login1$UserName': username,
-            'ctl00$Login1$Password': password,
-            'ctl00$Login1$LoginButton.x': '0',
-            'ctl00$Login1$LoginButton.y': '0'
-        }
-        data.update(login_data)
-
-        res = self.session.http.post(self.login_url, data=data)
-
-        for cookie in self.session.http.cookies:
-            self._session_attributes.set(cookie.name, cookie.value, expires=3600 * 24)
-
-        if self._session_attributes.get('ASP.NET_SessionId') and self._session_attributes.get('.abportail1'):
-            log.debug('New session data')
-            self.set_expires_time_cache()
+            self.save_cookies(default_expires=3600 * 24)
             return True
         else:
             log.error('Failed to login, check your username/password')
@@ -138,7 +86,6 @@ class ABweb(Plugin):
 
     def _get_streams(self):
         self.session.http.headers.update({
-            'User-Agent': useragents.CHROME,
             'Referer': 'http://www.abweb.com/BIS-TV-Online/bistvo-tele-universal.aspx'
         })
 
@@ -146,39 +93,41 @@ class ABweb(Plugin):
         login_password = self.get_option('password')
 
         if self.options.get('purge_credentials'):
-            self._session_attributes.set('ASP.NET_SessionId', None, expires=0)
-            self._session_attributes.set('.abportail1', None, expires=0)
+            self.clear_cookies()
             self._authed = False
             log.info('All credentials were successfully removed.')
 
-        if not self._authed and not (login_username and login_password):
+        if self._authed:
+            log.info('Attempting to authenticate using cached cookies')
+        elif not self._authed and not (login_username and login_password):
             log.error('A login for ABweb is required, use --abweb-username USERNAME --abweb-password PASSWORD')
             return
+        elif not self._authed and not self._login(login_username, login_password):
+            return
 
-        if self._authed:
-            if self._expires < time.time():
-                log.debug('get new cached cookies')
-                # login after 24h
-                self.set_expires_time_cache()
-                self._authed = False
+        log.debug('get iframe_url')
+        res = self.session.http.get(self.url)
+        for iframe in itertags(res.text, 'iframe'):
+            iframe_url = iframe.attributes.get('src')
+            if iframe_url.startswith('/'):
+                iframe_url = url_concat('https://www.abweb.com', iframe_url)
             else:
-                log.info('Attempting to authenticate using cached cookies')
-                self.session.http.cookies.set('ASP.NET_SessionId', self._session_attributes.get('ASP.NET_SessionId'))
-                self.session.http.cookies.set('.abportail1', self._session_attributes.get('.abportail1'))
-
-        if not self._authed and not self._login(login_username, login_password):
-            return
+                iframe_url = update_scheme('https://', iframe_url)
+            log.debug(f'iframe_url={iframe_url}')
+            break
+        else:
+            raise PluginError('No iframe_url found.')
 
-        iframe_url = self.get_iframe_url()
         self.session.http.headers.update({'Referer': iframe_url})
+        res = self.session.http.get(iframe_url)
+        m = self._hls_re.search(res.text)
+        if not m:
+            raise PluginError('No hls_url found.')
 
-        hls_url = self.get_hls_url(iframe_url)
-        hls_url = update_scheme(self.url, hls_url)
-
-        log.debug('URL={0}'.format(hls_url))
-        variant = HLSStream.parse_variant_playlist(self.session, hls_url)
-        if variant:
-            yield from variant.items()
+        hls_url = update_scheme('https://', m.group('url'))
+        streams = HLSStream.parse_variant_playlist(self.session, hls_url)
+        if streams:
+            yield from streams.items()
         else:
             yield 'live', HLSStream(self.session, hls_url)
 
diff --git a/src/streamlink/plugins/bfmtv.py b/src/streamlink/plugins/bfmtv.py
index a62b2ac8..877ea57e 100644
--- a/src/streamlink/plugins/bfmtv.py
+++ b/src/streamlink/plugins/bfmtv.py
@@ -1,45 +1,46 @@
+import logging
 import re
 
 from streamlink.plugin import Plugin
 from streamlink.plugins.brightcove import BrightcovePlayer
-from streamlink.stream import HLSStream
+
+log = logging.getLogger(__name__)
 
 
 class BFMTV(Plugin):
     _url_re = re.compile(r'https://.+\.(?:bfmtv|01net)\.com')
+    _dailymotion_url = 'https://www.dailymotion.com/embed/video/{}'
     _brightcove_video_re = re.compile(
-        r'data-holder="video(?P<video_id>[0-9]+)" data-account="(?P<account_id>[0-9]+)"'
-    )
-    _brightcove_video_alt_re = re.compile(
-        r'data-account="(?P<account_id>[0-9]+).*?data-video-id="(?P<video_id>[0-9]+)"',
+        r'accountid="(?P<account_id>[0-9]+).*?videoid="(?P<video_id>[0-9]+)"',
         re.DOTALL
     )
-    _embed_video_url_re = re.compile(
-        r"\$YOPLAYER\('liveStitching', {.+?file: '(?P<video_url>[^\"]+?)'.+?}\);",
+    _embed_video_id_re = re.compile(
+        r'<iframe.*?src=".*?/(?P<video_id>\w+)"',
         re.DOTALL
     )
 
     @classmethod
     def can_handle_url(cls, url):
-        return BFMTV._url_re.match(url)
+        return cls._url_re.match(url) is not None
 
     def _get_streams(self):
         # Retrieve URL page and search for Brightcove video data
         res = self.session.http.get(self.url)
-        match = self._brightcove_video_re.search(res.text) or self._brightcove_video_alt_re.search(res.text)
+        match = self._brightcove_video_re.search(res.text)
         if match is not None:
             account_id = match.group('account_id')
+            log.debug(f'Account ID: {account_id}')
             video_id = match.group('video_id')
+            log.debug(f'Video ID: {video_id}')
             player = BrightcovePlayer(self.session, account_id)
-            for stream in player.get_streams(video_id):
-                yield stream
+            yield from player.get_streams(video_id)
         else:
-            # Try to get the stream URL in the page
-            match = self._embed_video_url_re.search(res.text)
+            # Try to find the Dailymotion video ID
+            match = self._embed_video_id_re.search(res.text)
             if match is not None:
-                video_url = match.group('video_url')
-                if '.m3u8' in video_url:
-                    yield from HLSStream.parse_variant_playlist(self.session, video_url).items()
+                video_id = match.group('video_id')
+                log.debug(f'Video ID: {video_id}')
+                yield from self.session.streams(self._dailymotion_url.format(video_id)).items()
 
 
 __plugin__ = BFMTV
diff --git a/src/streamlink/plugins/cdnbg.py b/src/streamlink/plugins/cdnbg.py
index 38f77cd3..58351a5d 100644
--- a/src/streamlink/plugins/cdnbg.py
+++ b/src/streamlink/plugins/cdnbg.py
@@ -1,5 +1,6 @@
 import logging
 import re
+from html import unescape as html_unescape
 from urllib.parse import urlparse
 
 from streamlink.plugin import Plugin
@@ -43,29 +44,24 @@ class CDNBG(Plugin):
     def find_iframe(self, url):
         self.session.http.headers.update({"User-Agent": useragents.CHROME})
         res = self.session.http.get(self.url)
-        p = urlparse(url)
         for iframe_url in self.iframe_re.findall(res.text):
             if "googletagmanager" not in iframe_url:
-                log.debug(f"Found iframe: {iframe_url}")
-                iframe_url = iframe_url.replace("&#58;", ":")
-                if iframe_url.startswith("//"):
-                    return update_scheme(p.scheme, iframe_url)
-                else:
-                    return iframe_url
+                iframe_url = html_unescape(iframe_url)
+                return update_scheme(self.url, iframe_url)
 
     def _get_streams(self):
-        if "i.cdn.bg/live/" in self.url:
+        if "cdn.bg" in urlparse(self.url).netloc:
             iframe_url = self.url
         else:
             iframe_url = self.find_iframe(self.url)
 
-        if iframe_url:
-            res = self.session.http.get(iframe_url, headers={"Referer": self.url})
-            stream_url = update_scheme(self.url, self.stream_schema.validate(res.text))
-            log.warning("SSL Verification disabled.")
-            return HLSStream.parse_variant_playlist(self.session,
-                                                    stream_url,
-                                                    verify=False)
+        log.debug(f"Found iframe: {iframe_url}")
+        res = self.session.http.get(iframe_url, headers={"Referer": self.url})
+        stream_url = update_scheme(self.url, self.stream_schema.validate(res.text))
+        log.warning("SSL Verification disabled.")
+        return HLSStream.parse_variant_playlist(self.session,
+                                                stream_url,
+                                                verify=False)
 
 
 __plugin__ = CDNBG
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/vlive.py b/src/streamlink/plugins/vlive.py
index 77cd3f9a..ea64311a 100644
--- a/src/streamlink/plugins/vlive.py
+++ b/src/streamlink/plugins/vlive.py
@@ -1,58 +1,106 @@
-import json
 import logging
 import re
 
-from streamlink.plugin import Plugin, PluginError
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import validate
 from streamlink.stream import HLSStream
+from streamlink.utils import parse_json
 
 log = logging.getLogger(__name__)
 
 
 class Vlive(Plugin):
-    _url_re = re.compile(r"https?://(?:www.)vlive\.tv/video/(\d+)")
-    _video_status = re.compile(r'oVideoStatus = (.+)<', re.DOTALL)
-    _video_init_url = "https://www.vlive.tv/video/init/view"
+    _url_re = re.compile(r"https://www\.vlive\.tv/(?P<format>video|post)/(?P<id>[0-9\-]+)")
+    _page_info = re.compile(r'window\.__PRELOADED_STATE__\s*=\s*({.*})\s*<', re.DOTALL)
+    _playinfo_url = "https://www.vlive.tv/globalv-web/vam-web/old/v3/live/{0}/playInfo"
+
+    _schema_video = validate.Schema(
+        validate.transform(_page_info.search),
+        validate.any(None, validate.all(
+            validate.get(1),
+            validate.transform(parse_json),
+            validate.any(validate.all(
+                {"postDetail": {"post": {"officialVideo": {
+                    "type": str,
+                    "videoSeq": int,
+                    validate.optional("status"): str,
+                }}}},
+                validate.get("postDetail"),
+                validate.get("post"),
+                validate.get("officialVideo")),
+                validate.all(
+                    {"postDetail": {"error": {
+                        "errorCode": str,
+                    }}},
+                    validate.get("postDetail"),
+                    validate.get("error")))
+        ))
+    )
+
+    _schema_stream = validate.Schema(
+        validate.transform(parse_json),
+        validate.all(
+            {"result": {"streamList": [{
+                "streamName": str,
+                "serviceUrl": str,
+            }]}},
+            validate.get("result"),
+            validate.get("streamList")
+        )
+    )
 
     @classmethod
     def can_handle_url(cls, url):
         return cls._url_re.match(url) is not None
 
-    @property
-    def video_id(self):
-        return self._url_re.match(self.url).group(1)
-
     def _get_streams(self):
-        vinit_req = self.session.http.get(self._video_init_url,
-                                          params=dict(videoSeq=self.video_id),
-                                          headers=dict(referer=self.url))
-        if vinit_req.status_code != 200:
-            raise PluginError('Could not get video init page (HTTP Status {})'
-                              .format(vinit_req.status_code))
-
-        video_status_js = self._video_status.search(vinit_req.text)
-        if not video_status_js:
-            raise PluginError('Could not find video status information!')
-
-        video_status = json.loads(video_status_js.group(1))
-
-        if video_status['viewType'] == 'vod':
-            raise PluginError('VODs are not supported')
+        video_json = self.session.http.get(self.url, headers={"Referer": self.url},
+                                           schema=self._schema_video)
+        if video_json is None:
+            log.error('Could not parse video page')
+            return
+
+        err = video_json.get('errorCode')
+        if err == 'common_700':
+            log.error('Available only to members of the channel')
+            return
+        elif err == 'common_702':
+            log.error('Vlive+ VODs are not supported')
+            return
+        elif err == 'common_404':
+            log.error('Could not find video page')
+            return
+        elif err is not None:
+            log.error('Unknown error code: {0}'.format(err))
+            return
+
+        if video_json['type'] == 'VOD':
+            log.error('VODs are not supported')
+            return
+
+        url_format, video_id = self._url_re.match(self.url).groups()
+        if url_format == 'post':
+            video_id = str(video_json['videoSeq'])
+
+        video_status = video_json.get('status')
+        if video_status == 'ENDED':
+            log.error('Stream has ended')
+            return
+        elif video_status != 'ON_AIR':
+            log.error('Unknown video status: {0}'.format(video_status))
+            return
+
+        stream_info = self.session.http.get(self._playinfo_url.format(video_id),
+                                            headers={"Referer": self.url},
+                                            schema=self._schema_stream)
 
-        if 'liveStreamInfo' not in video_status:
-            raise PluginError('Stream is offline')
-
-        stream_info = json.loads(video_status['liveStreamInfo'])
-
-        streams = dict()
         # All "resolutions" have a variant playlist with only one entry, so just combine them
-        for i in stream_info['resolutions']:
-            res_streams = HLSStream.parse_variant_playlist(self.session, i['cdnUrl'])
+        for i in stream_info:
+            res_streams = HLSStream.parse_variant_playlist(self.session, i['serviceUrl'])
             if len(res_streams.values()) > 1:
                 log.warning('More than one stream in variant playlist, using first entry!')
 
-            streams[i['name']] = res_streams.popitem()[1]
-
-        return streams
+            yield i['streamName'], res_streams.popitem()[1]
 
 
 __plugin__ = Vlive


Test patch:
diff --git a/tests/plugins/test_abweb.py b/tests/plugins/test_abweb.py
index 3bab6f57..8688600d 100644
--- a/tests/plugins/test_abweb.py
+++ b/tests/plugins/test_abweb.py
@@ -9,12 +9,14 @@ class TestPluginABweb(unittest.TestCase):
             'http://www.abweb.com/bis-tv-online/bistvo-tele-universal.aspx?chn=ab1',
             'http://www.abweb.com/BIS-TV-Online/bistvo-tele-universal.aspx?chn=ab1',
             'http://www.abweb.com/BIS-TV-Online/bistvo-tele-universal.aspx?chn=luckyjack',
+            'https://www.abweb.com/BIS-TV-Online/bistvo-tele-universal.aspx?chn=action',
         ]
         for url in should_match:
-            self.assertTrue(ABweb.can_handle_url(url))
+            self.assertTrue(ABweb.can_handle_url(url), url)
 
+    def test_can_handle_url_negative(self):
         should_not_match = [
             'http://www.abweb.com',
         ]
         for url in should_not_match:
-            self.assertFalse(ABweb.can_handle_url(url))
+            self.assertFalse(ABweb.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/plugins/test_vlive.py b/tests/plugins/test_vlive.py
index 37b32be1..5e98e309 100644
--- a/tests/plugins/test_vlive.py
+++ b/tests/plugins/test_vlive.py
@@ -8,6 +8,7 @@ from streamlink.plugins.vlive import Vlive
 class TestPluginVlive:
     valid_urls = [
         ("https://www.vlive.tv/video/156824",),
+        ("https://www.vlive.tv/post/0-19740901",)
     ]
     invalid_urls = [
         ("https://www.vlive.tv/events/2019vheartbeat?lang=en",),