Repository: streamlink/streamlink

Issue:
Plugin request: USTVnow
### Checklist

- [ ] This is a bug report.
- [ ] This is a feature request.
- [x] This is a plugin (improvement) request.
- [x] I have read the contribution guidelines.

### Description

Hi, i would like to request a plugin for [USTVnow](https://www.ustvnow.com/). They offer [free](https://www.ustvnow.com/register.php?sub=7) access to [7 US TV Channels](https://support.ustvnow.com/hc/en-us/articles/214145143-What-plans-channels-are-available-) and paid access to 30 channels, [some in HD](https://support.ustvnow.com/hc/en-us/articles/214145203-What-USTVnow-channels-are-available-in-HD-on-the-paid-plans-).

Access-URL:
http://m.ustvnow.com/iphone (iStuff)
http://www.ustvnow.com/gtv/ (SmartTVs)
http://watch.ustvnow.com/guide (Webbrowsers)

The direct links are javascript-obfuscated and i can't see deep-links to the programs

They use various protocols, including HLS and RTMP, **but HLS access only works for 30 days on the free account**. HLS seems to offer a bit more quality (even on the SD-Only free account)

There are already several projects in python that can produce an m3u playlist (the urls are only valid for about 30mins):

https://github.com/mathsgrinds/ustvnow-iptv-m3u-playlist (HLS playlist, worked a week ago)
https://github.com/guilleiguaran/ustvnow-m3u-server (RTMP playlist, stopped working about a year ago).

A choice of the protocol(s) would be great (as mentioned, HLS only works for 30 days for free), but i would be happy with anything that "just works" :)

Thanks :D

Comments:

Patch:
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 0de102ab..d9b24173 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -247,6 +247,7 @@ tvrplus                 tvrplus.ro           Yes   No    Streams may be geo-rest
 twitch                  twitch.tv            Yes   Yes   Possible to authenticate for access to
                                                          subscription streams.
 ustreamtv               ustream.tv           Yes   Yes
+ustvnow                 ustvnow.com          Yes   --    All streams require an account, some streams require a subscription.
 vaughnlive              - vaughnlive.tv      Yes   --
                         - breakers.tv
                         - instagib.tv
diff --git a/src/streamlink/plugins/ustvnow.py b/src/streamlink/plugins/ustvnow.py
new file mode 100644
index 00000000..12a395e1
--- /dev/null
+++ b/src/streamlink/plugins/ustvnow.py
@@ -0,0 +1,96 @@
+import re
+
+from streamlink.plugin import Plugin, PluginArguments, PluginArgument
+from streamlink.plugin.api import http, useragents
+from streamlink.plugin.api.utils import itertags
+from streamlink.stream import HLSStream
+
+
+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"
+
+    arguments = PluginArguments(
+        PluginArgument(
+            "username",
+            metavar="USERNAME",
+            required=True,
+            help="Your USTV Now account username"
+        ),
+        PluginArgument(
+            "password",
+            sensitive=True,
+            metavar="PASSWORD",
+            required=True,
+            help="Your USTV Now account password",
+            prompt="Enter USTV Now account password"
+        ),
+        PluginArgument(
+            "station-code",
+            metavar="CODE",
+            help="USTV Now station code"
+        ),
+    )
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    def login(self, username, password):
+        r = http.get(self._signin_url)
+        csrf = None
+
+        for input in itertags(r.text, "input"):
+            if input.attributes['name'] == "csrf_ustvnow":
+                csrf = input.attributes['value']
+
+        self.logger.debug("CSRF: {0}", csrf)
+
+        r = 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)
+
+    def _get_streams(self):
+        """
+        Finds the streams from tvcatchup.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 = http.get(self._guide_url)
+
+        channels = {}
+        for t in itertags(res.text, "a"):
+            if t.attributes.get('cs'):
+                channels[t.attributes.get('cs').lower()] = t.attributes.get('title').replace("Watch ", "")
+
+        if not scode:
+            self.logger.error("Station code not provided, use --ustvnow-station-code.")
+            self.logger.error("Available stations are: {0}", ", ".join(channels.keys()))
+            return
+
+        if scode in channels:
+            self.logger.debug("Finding streams for: {0}", channels.get(scode))
+
+            r = 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 = http.json(r)
+            return HLSStream.parse_variant_playlist(self.session, data["stream"])
+        else:
+            self.logger.error("Invalid station-code: {0}", scode)
+
+
+__plugin__ = USTVNow


Test patch:
diff --git a/tests/test_plugin_ustvnow.py b/tests/test_plugin_ustvnow.py
new file mode 100644
index 00000000..c7195265
--- /dev/null
+++ b/tests/test_plugin_ustvnow.py
@@ -0,0 +1,16 @@
+import unittest
+
+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"))
+
+    def test_can_not_handle_url(self):
+        self.assertFalse(USTVNow.can_handle_url("http://www.tvplayer.com"))