Repository: streamlink/streamlink

Issue:
Schoolism plugin not working with videos which have subtitles
<!--
Thanks for reporting a bug!
USE THE TEMPLATE. Otherwise your bug report may be rejected.

First, see the contribution guidelines:
https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink

Also check the list of open and closed bug reports:
https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22bug%22

Please see the text preview to avoid unnecessary formatting errors.
-->


## Bug Report

<!-- Replace [ ] with [x] in order to check the box -->
- [x] This is a bug report and I have read the contribution guidelines.


### Description

<!-- Explain the bug as thoroughly as you can. Don't leave out information which is necessary for us to reproduce and debug this issue. -->

Schoolism plugin works fine with other courses except the two courses below. it'll get error when opening video with subtitles.
- Digital Painting with Craig Mullins 
- Drawing Fundamentals with Thomas Fluharty

I'm using streamlink 1.1.1 in python 2.7. I think I have detected the reason why this bug happens but unfortunately I'm not familiar with python and streamlink itself so I may need some help:/
The javascript variable "allVideos" (which stores video URL link and other information) in HTML code is different when video contains subtitles.
  - Videos without subtitles:
```
var allVideos=[
{sources:[{type:"application/x-mpegurl",src:"https://d8u31iyce9xic.cloudfront.net/14/1/part1.m3u8?Policy=CiAgICAgICAgICAgIHsgCiAgICAgICAgICAgICAgICJTdGF0ZW1lbnQiOiBbCiAgICAgICAgICAgICAgICAgIHsgCiAgICAgICAgICAgICAgICAgICAgICJSZXNvdXJjZSI6Imh0dHBzOi8vZDh1MzFpeWNlOXhpYy5jbG91ZGZyb250Lm5ldC8xNC8xLyoiLAogICAgICAgICAgICAgICAgICAgICAiQ29uZGl0aW9uIjp7CiAgICAgICAgICAgICAgICAgICAgICAgICJEYXRlR3JlYXRlclRoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTU1Njk2Mjc3MH0sCiAgICAgICAgICAgICAgICAgICAgICAgICJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTU1NzA0OTE3MH0KICAgICAgICAgICAgICAgICAgICAgfSAKICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICBdIAogICAgICAgICAgICB9CiAgICAgICAg&Signature=HE0~u~zThmbVd0QMbmSuHe3Pxxcg4-ZZXf6N8Fyczbae5Ov1BSfuJgWOeGPk1Y7zuVVE2vO5TlEWNR2aVIvQIAkVQNuaohOf9xuOdnn96SFq6J-LxTEPZthTvhbb3SJeDLWzdEj9-f93pfptKsTd8gABIAltfM1A-~Wjv~jiMVd19T0GJA9PUMNIeNkY7snBE7bMT85ARGrrHynWR71g31U1O-JvfAcnHLEcql-kc~5ZbV0HiJlvqicYhyJUsLzO3lR7sBihjmdq2FDH3-ckhzX3LJhJ9mpZYqZJ6r5DF1LApM870Ct-0KOe84EqdkCxUO4eaCd2ggJx8XLGbibaGQ__&Key-Pair-Id=APKAJHWRPDT6BGTTH7LA",title:"Gesture Drawing - Lesson 1 - Part 1",playlistTitle:"Part 1",}],},
{sources:[{type:"application/x-mpegurl",src:"https://d8u31iyce9xic.cloudfront.net/14/1/part2.m3u8?Policy=CiAgICAgICAgICAgIHsgCiAgICAgICAgICAgICAgICJTdGF0ZW1lbnQiOiBbCiAgICAgICAgICAgICAgICAgIHsgCiAgICAgICAgICAgICAgICAgICAgICJSZXNvdXJjZSI6Imh0dHBzOi8vZDh1MzFpeWNlOXhpYy5jbG91ZGZyb250Lm5ldC8xNC8xLyoiLAogICAgICAgICAgICAgICAgICAgICAiQ29uZGl0aW9uIjp7CiAgICAgICAgICAgICAgICAgICAgICAgICJEYXRlR3JlYXRlclRoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTU1Njk2Mjc3MH0sCiAgICAgICAgICAgICAgICAgICAgICAgICJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTU1NzA0OTE3MH0KICAgICAgICAgICAgICAgICAgICAgfSAKICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICBdIAogICAgICAgICAgICB9CiAgICAgICAg&Signature=HE0~u~zThmbVd0QMbmSuHe3Pxxcg4-ZZXf6N8Fyczbae5Ov1BSfuJgWOeGPk1Y7zuVVE2vO5TlEWNR2aVIvQIAkVQNuaohOf9xuOdnn96SFq6J-LxTEPZthTvhbb3SJeDLWzdEj9-f93pfptKsTd8gABIAltfM1A-~Wjv~jiMVd19T0GJA9PUMNIeNkY7snBE7bMT85ARGrrHynWR71g31U1O-JvfAcnHLEcql-kc~5ZbV0HiJlvqicYhyJUsLzO3lR7sBihjmdq2FDH3-ckhzX3LJhJ9mpZYqZJ6r5DF1LApM870Ct-0KOe84EqdkCxUO4eaCd2ggJx8XLGbibaGQ__&Key-Pair-Id=APKAJHWRPDT6BGTTH7LA",title:"Gesture Drawing - Lesson 1 - Part 2",playlistTitle:"Part 2",}]}
];

```
  - Videos with subtitles:
```
var allVideos=[
{sources:[{type:"application/x-mpegurl",src:"https://d8u31iyce9xic.cloudfront.net/44/2/part1.m3u8?Policy=CiAgICAgICAgICAgIHsgCiAgICAgICAgICAgICAgICJTdGF0ZW1lbnQiOiBbCiAgICAgICAgICAgICAgICAgIHsgCiAgICAgICAgICAgICAgICAgICAgICJSZXNvdXJjZSI6Imh0dHBzOi8vZDh1MzFpeWNlOXhpYy5jbG91ZGZyb250Lm5ldC80NC8yLyoiLAogICAgICAgICAgICAgICAgICAgICAiQ29uZGl0aW9uIjp7CiAgICAgICAgICAgICAgICAgICAgICAgICJEYXRlR3JlYXRlclRoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTU1Njk2MjkyMH0sCiAgICAgICAgICAgICAgICAgICAgICAgICJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTU1NzA0OTMyMH0KICAgICAgICAgICAgICAgICAgICAgfSAKICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICBdIAogICAgICAgICAgICB9CiAgICAgICAg&Signature=WysPwDFhH0K75hoVq~XPrOjp-Lv5SbNTaM~x0NGpLUJTmzHw83Zz~HAqbpZPaeJiKRaEieC2y7uYJzzLlOdZvPWLyCoy76-5nav1bcihyD2DeJ7lIdCmsNlJarlODdxot3HaO0E7V2rv7WikNol5DmuL-dRsld8Y8JUWm4gJT5SSLVOSBWlCHI~TPrdDyjujMX1k2cLjLc7mHeOuBJOrJGWwwtb3DB48h8rjavZ6R5SXP3YU8uUDow8W74H27nKFd1mhd6ic0CBMVVEucL91ZkyGklNpoRTDqK2nHm6gZk7VWr6it7~XGtYpmHqOJf7BT0p1qaKHLNZnms~S3rKq0A__&Key-Pair-Id=APKAJHWRPDT6BGTTH7LA",title:"Digital Painting - Lesson 2 - Part 1",playlistTitle:"Part 1",}],        subtitles: [{
            "default": true,
            kind: "subtitles", srclang: "en", label: "English",
            src:  "https://s3.amazonaws.com/schoolism-encoded/44/subtitles/2/2-1.vtt",
        }],
        },
{sources:[{type:"application/x-mpegurl",src:"https://d8u31iyce9xic.cloudfront.net/44/2/part2.m3u8?Policy=CiAgICAgICAgICAgIHsgCiAgICAgICAgICAgICAgICJTdGF0ZW1lbnQiOiBbCiAgICAgICAgICAgICAgICAgIHsgCiAgICAgICAgICAgICAgICAgICAgICJSZXNvdXJjZSI6Imh0dHBzOi8vZDh1MzFpeWNlOXhpYy5jbG91ZGZyb250Lm5ldC80NC8yLyoiLAogICAgICAgICAgICAgICAgICAgICAiQ29uZGl0aW9uIjp7CiAgICAgICAgICAgICAgICAgICAgICAgICJEYXRlR3JlYXRlclRoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTU1Njk2MjkyMH0sCiAgICAgICAgICAgICAgICAgICAgICAgICJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTU1NzA0OTMyMH0KICAgICAgICAgICAgICAgICAgICAgfSAKICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICBdIAogICAgICAgICAgICB9CiAgICAgICAg&Signature=WysPwDFhH0K75hoVq~XPrOjp-Lv5SbNTaM~x0NGpLUJTmzHw83Zz~HAqbpZPaeJiKRaEieC2y7uYJzzLlOdZvPWLyCoy76-5nav1bcihyD2DeJ7lIdCmsNlJarlODdxot3HaO0E7V2rv7WikNol5DmuL-dRsld8Y8JUWm4gJT5SSLVOSBWlCHI~TPrdDyjujMX1k2cLjLc7mHeOuBJOrJGWwwtb3DB48h8rjavZ6R5SXP3YU8uUDow8W74H27nKFd1mhd6ic0CBMVVEucL91ZkyGklNpoRTDqK2nHm6gZk7VWr6it7~XGtYpmHqOJf7BT0p1qaKHLNZnms~S3rKq0A__&Key-Pair-Id=APKAJHWRPDT6BGTTH7LA",title:"Digital Painting - Lesson 2 - Part 2",playlistTitle:"Part 2",}],        subtitles: [{
            "default": true,
            kind: "subtitles", srclang: "en", label: "English",
            src:  "https://s3.amazonaws.com/schoolism-encoded/44/subtitles/2/2-2.vtt",
        }]
];
```


### Expected / Actual behavior

<!-- What do you expect to happen, and what is actually happening? -->

The plugin works whether videos have subtitles or not. / It gets error with videos with subtitles.

### Reproduction steps / Explicit stream URLs to test

<!-- How can we reproduce this? Please note the exact steps below using the list format supplied. If you need more steps please add them. -->

1. Login to Schoolism and switch course to "Digital Painting with Craig Mullins"
2. Open lesson 1 part 1 with streamlink, for example. The course page URL is "https://www.schoolism.com/watchLesson.php?type=st&id=506

### Log output

<!--
TEXT LOG OUTPUT IS REQUIRED for a bug report!
Use the `--loglevel debug` parameter and avoid using parameters which suppress log output.
https://streamlink.github.io/cli.html#cmdoption-l

Make sure to **remove usernames and passwords**
You can copy the output to https://gist.github.com/ or paste it below.
-->

```
[cli][debug] OS:         Linux-4.4.0-17134-Microsoft-x86_64-with-Ubuntu-18.04-bionic
[cli][debug] Python:     2.7.15rc1
[cli][debug] Streamlink: 1.1.1
[cli][debug] Requests(2.21.0), Socks(1.6.7), Websocket(0.56.0)
[cli][info] Found matching plugin schoolism for URL https://www.schoolism.com/watchLesson.php?type=st&id=506
[cli][debug] Plugin specific arguments:
[cli][debug]  --schoolism-email=(email omitted)
[cli][debug]  --schoolism-password=(password omitted)
[cli][debug]  --schoolism-part=1 (part)
[plugin.schoolism][debug] Logged in to Schoolism as (email omitted)
error: Unable to parse JSON: Expecting property name enclosed in double quotes: line 5 column 9 (char 1337) (u'[{"sources":[{"type":"application ...)
```


### Additional comments, screenshots, etc.
I have a schoolism account valid till August so I can help testing.
Or if someone can give me some hint fixing this issue, I can make a merge request myself. Thank you!

[Love Streamlink? Please consider supporting our collective. Thanks!](https://opencollective.com/streamlink/donate)


Comments:

Patch:
diff --git a/src/streamlink/plugins/cdnbg.py b/src/streamlink/plugins/cdnbg.py
index 79b0172b..ee7a8a82 100644
--- a/src/streamlink/plugins/cdnbg.py
+++ b/src/streamlink/plugins/cdnbg.py
@@ -23,6 +23,7 @@ class CDNBG(Plugin):
             mmtvmusic\.com/live|
             mu-vi\.tv/LiveStreams/pages/Live\.aspx|
             videochanel\.bstv\.bg|
+            live\.bstv\.bg|
             bloombergtv.bg/video
         )/?
     """, re.VERBOSE)
diff --git a/src/streamlink/plugins/pixiv.py b/src/streamlink/plugins/pixiv.py
index c1f83685..7cdee837 100644
--- a/src/streamlink/plugins/pixiv.py
+++ b/src/streamlink/plugins/pixiv.py
@@ -73,6 +73,25 @@ class Pixiv(Plugin):
         A pixiv.net account password to use with --pixiv-username
         """
         ),
+        PluginArgument(
+            "sessionid",
+            requires=["devicetoken"],
+            sensitive=True,
+            metavar="SESSIONID",
+            help="""
+        The pixiv.net sessionid thats used in pixivs PHPSESSID cookie.
+        can be used instead of the username/password login process.
+        """
+        ),
+        PluginArgument(
+            "devicetoken",
+            sensitive=True,
+            metavar="DEVICETOKEN",
+            help="""
+        The pixiv.net device token thats used in  pixivs device_token cookie.
+        can be used instead of the username/password login process.
+        """
+        ),
         PluginArgument(
             "purge-credentials",
             action="store_true",
@@ -125,6 +144,17 @@ class Pixiv(Plugin):
         else:
             log.error("Failed to log in.")
 
+
+    def _login_using_session_id_and_device_token(self, session_id, device_token):
+        res = self.session.http.get(self.login_url_get)
+
+        self.session.http.cookies.set('PHPSESSID', session_id, domain='.pixiv.net', path='/')
+        self.session.http.cookies.set('device_token', device_token, domain='.pixiv.net', path='/')
+
+        self.save_cookies()
+        log.info("Successfully set sessionId and deviceToken")
+
+
     def hls_stream(self, hls_url):
         log.debug("URL={0}".format(hls_url))
         for s in HLSStream.parse_variant_playlist(self.session, hls_url).items():
@@ -146,6 +176,9 @@ class Pixiv(Plugin):
         login_username = self.get_option("username")
         login_password = self.get_option("password")
 
+        login_session_id = self.get_option("sessionid")
+        login_device_token = self.get_option("devicetoken")
+
         if self.options.get("purge_credentials"):
             self.clear_cookies()
             self._authed = False
@@ -155,6 +188,8 @@ class Pixiv(Plugin):
             log.debug("Attempting to authenticate using cached cookies")
         elif not self._authed and login_username and login_password:
             self._login(login_username, login_password)
+        elif not self._authed and login_session_id and login_device_token:
+            self._login_using_session_id_and_device_token(login_session_id, login_device_token)
 
         streamer_data = self.get_streamer_data()
         performers = streamer_data.get("performers")
diff --git a/src/streamlink/plugins/schoolism.py b/src/streamlink/plugins/schoolism.py
index 5285c2a7..88d33312 100644
--- a/src/streamlink/plugins/schoolism.py
+++ b/src/streamlink/plugins/schoolism.py
@@ -17,8 +17,9 @@ class Schoolism(Plugin):
     url_re = re.compile(r"https?://(?:www\.)?schoolism\.com/(viewAssignment|watchLesson).php")
     login_url = "https://www.schoolism.com/index.php"
     key_time_url = "https://www.schoolism.com/video-html/key-time.php"
-    playlist_re = re.compile(r"var allVideos\s*=\s*(\[\{.*\}]);", re.DOTALL)
+    playlist_re = re.compile(r"var allVideos\s*=\s*(\[.*\]);", re.DOTALL)
     js_to_json = partial(re.compile(r'(?!<")(\w+):(?!/)').sub, r'"\1":')
+    fix_brackets = partial(re.compile(r',\s*\}').sub, r'}')
     playlist_schema = validate.Schema(
         validate.transform(playlist_re.search),
         validate.any(
@@ -26,7 +27,7 @@ class Schoolism(Plugin):
             validate.all(
                 validate.get(1),
                 validate.transform(js_to_json),
-                validate.transform(lambda x: x.replace(",}", "}")),  # remove invalid ,
+                validate.transform(fix_brackets),  # remove invalid ,
                 validate.transform(parse_json),
                 [{
                     "sources": validate.all([{
diff --git a/src/streamlink/plugins/ustvnow.py b/src/streamlink/plugins/ustvnow.py
index f02446df..4d6c213d 100644
--- a/src/streamlink/plugins/ustvnow.py
+++ b/src/streamlink/plugins/ustvnow.py
@@ -1,24 +1,33 @@
 from __future__ import unicode_literals
 
+import argparse
+import base64
+import json
 import logging
 import re
-from collections import OrderedDict
+from uuid import uuid4
 
+from Crypto.Cipher import AES
+from Crypto.Hash import SHA256
+from Crypto.Util.Padding import pad, unpad
+
+from streamlink import PluginError
+from streamlink.compat import urljoin, urlparse
 from streamlink.plugin import Plugin, PluginArguments, PluginArgument
-from streamlink.plugin.api import useragents
-from streamlink.plugin.api.utils import itertags
 from streamlink.stream import HLSStream
 
 log = logging.getLogger(__name__)
 
 
 class USTVNow(Plugin):
-    _url_re = re.compile(r"https?://(?:watch\.)?ustvnow\.com(?:/(?:watch|guide)/(?P<scode>\w+))?")
-    _token_re = re.compile(r'''var\s+token\s*=\s*"(.*?)";''')
-    _login_url = "https://watch.ustvnow.com/account/login"
-    _signin_url = "https://watch.ustvnow.com/account/signin"
-    _guide_url = "http://m.ustvnow.com/gtv/1/live/channelguidehtml"
-    _stream_url = "http://m.ustvnow.com/stream/1/live/view"
+    _url_re = re.compile(r"https?://(?:www\.)?ustvnow\.com/live/(?P<scode>\w+)/-(?P<id>\d+)")
+    _main_js_re = re.compile(r"""src=['"](main\..*\.js)['"]""")
+    _enc_key_re = re.compile(r'(?P<key>AES_(?:Key|IV))\s*:\s*"(?P<value>[^"]+)"')
+
+    TENANT_CODE = "ustvnow"
+    _api_url = "https://teleupapi.revlet.net/service/api/v1/"
+    _token_url = _api_url + "get/token"
+    _signin_url = "https://www.ustvnow.com/signin"
 
     arguments = PluginArguments(
         PluginArgument(
@@ -38,65 +47,135 @@ class USTVNow(Plugin):
         PluginArgument(
             "station-code",
             metavar="CODE",
-            help="USTV Now station code"
+            help=argparse.SUPPRESS
         ),
     )
 
+    def __init__(self, url):
+        super(USTVNow, self).__init__(url)
+        self._encryption_config = {}
+        self._token = None
+
     @classmethod
     def can_handle_url(cls, url):
         return cls._url_re.match(url) is not None
 
-    def login(self, username, password):
-        r = self.session.http.get(self._signin_url)
-        csrf = None
+    @classmethod
+    def encrypt_data(cls, data, key, iv):
+        rkey = "".join(reversed(key)).encode('utf8')
+        riv = "".join(reversed(iv)).encode('utf8')
+
+        fkey = SHA256.new(rkey).hexdigest()[:32].encode("utf8")
+
+        cipher = AES.new(fkey, AES.MODE_CBC, riv)
+        encrypted = cipher.encrypt(pad(data, 16, 'pkcs7'))
+        return base64.b64encode(encrypted)
 
-        for input in itertags(r.text, "input"):
-            if input.attributes['name'] == "csrf_ustvnow":
-                csrf = input.attributes['value']
+    @classmethod
+    def decrypt_data(cls, data, key, iv):
+        rkey = "".join(reversed(key)).encode('utf8')
+        riv = "".join(reversed(iv)).encode('utf8')
 
-        log.debug("CSRF: {0}", csrf)
+        fkey = SHA256.new(rkey).hexdigest()[:32].encode("utf8")
 
-        r = self.session.http.post(self._login_url, data={'csrf_ustvnow': csrf,
-                                             'signin_email': username,
-                                             'signin_password': password,
-                                             'signin_remember': '1'})
-        m = self._token_re.search(r.text)
-        return m and m.group(1)
+        cipher = AES.new(fkey, AES.MODE_CBC, riv)
+        decrypted = cipher.decrypt(base64.b64decode(data))
+        if decrypted:
+            return unpad(decrypted, 16, 'pkcs7')
+        else:
+            return decrypted
+
+    def _get_encryption_config(self, url):
+        # find the path to the main.js
+        # load the main.js and extract the config
+        if not self._encryption_config:
+            res = self.session.http.get(url)
+            m = self._main_js_re.search(res.text)
+            main_js_path = m and m.group(1)
+            if main_js_path:
+                res = self.session.http.get(urljoin(url, main_js_path))
+                self._encryption_config = dict(self._enc_key_re.findall(res.text))
+
+        return self._encryption_config.get("AES_Key"), self._encryption_config.get("AES_IV")
+
+    @property
+    def box_id(self):
+        if not self.cache.get("box_id"):
+            self.cache.set("box_id", str(uuid4()))
+        return self.cache.get("box_id")
+
+    def get_token(self):
+        """
+        Get the token for USTVNow
+        :return: a valid token
+        """
+
+        if not self._token:
+            log.debug("Getting new session token")
+            res = self.session.http.get(self._token_url, params={
+                "tenant_code": self.TENANT_CODE,
+                "box_id": self.box_id,
+                "product": self.TENANT_CODE,
+                "device_id": 5,
+                "display_lang_code": "ENG",
+                "device_sub_type": "",
+                "timezone": "UTC"
+            })
+
+            data = res.json()
+            if data['status']:
+                self._token = data['response']['sessionId']
+                log.debug("New token: {}".format(self._token))
+            else:
+                log.error("Token acquisition failed: {details} ({detail})".format(**data['error']))
+                raise PluginError("could not obtain token")
+
+        return self._token
+
+    def api_request(self, path, data, metadata=None):
+        key, iv = self._get_encryption_config(self._signin_url)
+        post_data = {
+            "data": self.encrypt_data(json.dumps(data).encode('utf8'), key, iv).decode("utf8"),
+            "metadata": self.encrypt_data(json.dumps(metadata).encode('utf8'), key, iv).decode("utf8")
+        }
+        headers = {"box-id": self.box_id,
+                   "session-id": self.get_token(),
+                   "tenant-code": self.TENANT_CODE,
+                   "content-type": "application/json"}
+        res = self.session.http.post(self._api_url + path, data=json.dumps(post_data), headers=headers).json()
+        data = dict((k, v and json.loads(self.decrypt_data(v, key, iv)))for k, v in res.items())
+        return data
+
+    def login(self, username, password):
+        log.debug("Trying to login...")
+        resp = self.api_request("send",
+                         {
+                             "login_id": username,
+                             "login_key": password,
+                             "login_mode": "1",
+                             "manufacturer": "123"
+                         },
+                         {"request": "signin"})
+
+        return resp['data']['status']
 
     def _get_streams(self):
         """
-        Finds the streams from tvcatchup.com.
+        Finds the streams from ustvnow.com.
         """
-        token = self.login(self.get_option("username"), self.get_option("password"))
-        m = self._url_re.match(self.url)
-        scode = m and m.group("scode") or self.get_option("station_code")
-
-        res = self.session.http.get(self._guide_url, params=dict(token=token))
-
-        channels = OrderedDict()
-        for t in itertags(res.text, "a"):
-            if t.attributes.get('cs'):
-                channels[t.attributes.get('cs').lower()] = t.attributes.get('title').replace("Watch ", "").strip()
-
-        if not scode:
-            log.error("Station code not provided, use --ustvnow-station-code.")
-            log.info("Available stations are: \n{0} ".format('\n'.join('    {0} ({1})'.format(c, n) for c, n in channels.items())))
-            return
-
-        if scode in channels:
-            log.debug("Finding streams for: {0}", channels.get(scode))
-
-            r = self.session.http.get(self._stream_url, params={"scode": scode,
-                                                   "token": token,
-                                                   "br_n": "Firefox",
-                                                   "br_v": "52",
-                                                   "br_d": "desktop"},
-                         headers={"User-Agent": useragents.FIREFOX})
-
-            data = self.session.http.json(r)
-            return HLSStream.parse_variant_playlist(self.session, data["stream"])
+        if self.login(self.get_option("username"), self.get_option("password")):
+            path = urlparse(self.url).path.strip("/")
+            resp = self.api_request("send", {"path": path}, {"request": "page/stream"})
+            if resp['data']['status']:
+                for stream in resp['data']['response']['streams']:
+                    if stream['keys']['licenseKey']:
+                        log.warning("Stream possibly protected by DRM")
+                    for q, s in HLSStream.parse_variant_playlist(self.session, stream['url']).items():
+                        yield (q, s)
+            else:
+                log.error("Could not find any streams: {code}: {message}".format(**resp['data']['error']))
         else:
-            log.error("Invalid station-code: {0}", scode)
+            log.error("Failed to login, check username and password")
 
 
 __plugin__ = USTVNow


Test patch:
diff --git a/tests/plugins/test_cdnbg.py b/tests/plugins/test_cdnbg.py
index b98b16ee..898cb598 100644
--- a/tests/plugins/test_cdnbg.py
+++ b/tests/plugins/test_cdnbg.py
@@ -23,6 +23,7 @@ class TestPluginCDNBG(unittest.TestCase):
         self.assertTrue(CDNBG.can_handle_url("https://mmtvmusic.com/live/"))
         self.assertTrue(CDNBG.can_handle_url("http://mu-vi.tv/LiveStreams/pages/Live.aspx"))
         self.assertTrue(CDNBG.can_handle_url("http://videochanel.bstv.bg/"))
+        self.assertTrue(CDNBG.can_handle_url("http://live.bstv.bg/"))
         self.assertTrue(CDNBG.can_handle_url("https://www.bloombergtv.bg/video"))
 
         # shouldn't match
diff --git a/tests/plugins/test_schoolism.py b/tests/plugins/test_schoolism.py
index 8890ee11..f37e85e8 100644
--- a/tests/plugins/test_schoolism.py
+++ b/tests/plugins/test_schoolism.py
@@ -17,3 +17,37 @@ class TestPluginSchoolism(unittest.TestCase):
         ]
         for url in should_not_match:
             self.assertFalse(Schoolism.can_handle_url(url))
+
+    def test_playlist_parse_subs(self):
+        with_subs = """var allVideos=[
+            {sources:[{type:"application/x-mpegurl",src:"https://d8u31iyce9xic.cloudfront.net/44/2/part1.m3u8?Policy=TOKEN&Signature=TOKEN&Key-Pair-Id=TOKEN",title:"Digital Painting - Lesson 2 - Part 1",playlistTitle:"Part 1",}],        subtitles: [{
+                    "default": true,
+                    kind: "subtitles", srclang: "en", label: "English",
+                    src:  "https://s3.amazonaws.com/schoolism-encoded/44/subtitles/2/2-1.vtt",
+                }],
+                },
+            {sources:[{type:"application/x-mpegurl",src:"https://d8u31iyce9xic.cloudfront.net/44/2/part2.m3u8?Policy=TOKEN&Signature=TOKEN&Key-Pair-Id=TOKEN",title:"Digital Painting - Lesson 2 - Part 2",playlistTitle:"Part 2",}],        subtitles: [{
+                    "default": true,
+                    kind: "subtitles", srclang: "en", label: "English",
+                    src:  "https://s3.amazonaws.com/schoolism-encoded/44/subtitles/2/2-2.vtt",
+                }]
+            }];
+            """
+
+        data = Schoolism.playlist_schema.validate(with_subs)
+
+        self.assertIsNotNone(data)
+        self.assertEqual(2, len(data))
+
+
+    def test_playlist_parse(self):
+        without_subs = """var allVideos=[
+            {sources:[{type:"application/x-mpegurl",src:"https://d8u31iyce9xic.cloudfront.net/14/1/part1.m3u8?Policy=TOKEN&Signature=TOKEN&Key-Pair-Id=TOKEN",title:"Gesture Drawing - Lesson 1 - Part 1",playlistTitle:"Part 1",}],},
+            {sources:[{type:"application/x-mpegurl",src:"https://d8u31iyce9xic.cloudfront.net/14/1/part2.m3u8?Policy=TOKEN&Signature=TOKEN&Key-Pair-Id=TOKEN",title:"Gesture Drawing - Lesson 1 - Part 2",playlistTitle:"Part 2",}]}
+            ];
+        """
+
+        data = Schoolism.playlist_schema.validate(without_subs)
+
+        self.assertIsNotNone(data)
+        self.assertEqual(2, len(data))
diff --git a/tests/plugins/test_ustvnow.py b/tests/plugins/test_ustvnow.py
index c7195265..4e7160d9 100644
--- a/tests/plugins/test_ustvnow.py
+++ b/tests/plugins/test_ustvnow.py
@@ -5,12 +5,27 @@ from streamlink.plugins.ustvnow import USTVNow
 
 class TestPluginUSTVNow(unittest.TestCase):
     def test_can_handle_url(self):
-        self.assertTrue(USTVNow.can_handle_url("http://watch.ustvnow.com"))
-        self.assertTrue(USTVNow.can_handle_url("https://watch.ustvnow.com/"))
-        self.assertTrue(USTVNow.can_handle_url("http://watch.ustvnow.com/watch"))
-        self.assertTrue(USTVNow.can_handle_url("https://watch.ustvnow.com/watch"))
-        self.assertTrue(USTVNow.can_handle_url("https://watch.ustvnow.com/watch/syfy"))
-        self.assertTrue(USTVNow.can_handle_url("https://watch.ustvnow.com/guide/foxnews"))
+        self.assertTrue(USTVNow.can_handle_url("http://www.ustvnow.com/live/foo/-65"))
 
     def test_can_not_handle_url(self):
         self.assertFalse(USTVNow.can_handle_url("http://www.tvplayer.com"))
+
+    def test_encrypt_data(self):
+
+        key = "80035ad42d7d-bb08-7a14-f726-78403b29"
+        iv = "3157b5680927cc4a"
+
+        self.assertEqual(
+            b"uawIc5n+TnmsmR+aP2iEDKG/eMKji6EKzjI4mE+zMhlyCbHm7K4hz7IDJDWwM3aE+Ro4ydSsgJf4ZInnoW6gqvXvG0qB/J2WJeypTSt4W124zkJpvfoJJmGAvBg2t0HT",
+            USTVNow.encrypt_data(b'{"login_id":"test@test.com","login_key":"testtest1234","login_mode":"1","manufacturer":"123"}', key, iv)
+        )
+
+    def test_decrypt_data(self):
+
+        key = "80035ad42d7d-bb08-7a14-f726-78403b29"
+        iv = "3157b5680927cc4a"
+
+        self.assertEqual(
+            b'{"status":false,"error":{"code":-2,"type":"","message":"Invalid credentials.","details":{}}}',
+            USTVNow.decrypt_data(b"KcRLETVAmHlosM0OyUd5hdTQ6WhBRTe/YRAHiLJWrzf94OLkSueXTtQ9QZ1fjOLCbpX2qteEPUWVnzvvSgVDkQmRUttN/royoxW2aL0gYQSoH1NWoDV8sIgvS5vDiQ85", key, iv)
+        )