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


Discussion:
Hmm this one might be a bit questionable, if they're obfuscating links or doing any sort of encryption it might be difficult to support. Looking at that script that was working a week ago, it looks like they were specifically pulling it as a mobile stream, so that's probably fixed whatever mechanic was allowing that bypass. Thanks for the request, we'll see if someone wants to investigate it.
Hiya, i'm not sure obfuscating was the right term, i meant they are not simply using www.ustvnow.com/abc etc links in the browser but some javascript-links, but i'm pretty sure they don't use encryption, at least when i was testing it a year ago, the rtmp links where standard rtmp (not rtmps etc)... Also, the hls playlists worked fine last week (before my trial expired) with tvheadend, which does NOT support encrypted hls-stream...

But then again, you can probably see more from the two python-examples than i can ever tell you ;). Just one thing i forgot, they also offer access via Roku, so there has to be a (relativly) stable api... In any case, thanks for your time :)
Sounds cool, I'll take a look :)
Edit: thanks for the detailed request!
Just FYI I am using https://github.com/mathsgrinds/ustvnow-iptv-m3u-playlist in termux on android and generating a playlist that works beautfully. 

What I have been unable to detgermine is how long the links are valid  it seems somewhat random

You probably should try the mathgrinds USTVNow code 

Resolving patch:
diff --git a/src/streamlink/plugins/ustvnow.py b/src/streamlink/plugins/ustvnow.py
new file mode 100644
--- /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
--- /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"))