Repository: streamlink/streamlink

Issue:
Plugin Request for UFC Fightpass
### 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.

Not sure if this is possible but would be cool to have a plugin for ufc fightpass. Happy to contribute to developing the plugin but python isn't my primary language so would need some help.

### Description
I would like to be able to stream ufc fight pass. This is a "paid" service but they do have some free videos. Some that require a login and some that don't. It appears that there are existing "paid" services implemented already (crunchyroll)

Here is an example video that requires no login. 
https://www.ufc.tv/video/fight-night-oklahoma-city-michael-chiesa-vs-kevin-lee-preview

Here is an example of video that requires a paid account (30 day free trial available)
https://www.ufc.tv/video/ufc-fortaleza-2017

### Details
There's a similar project that exists for Kodi. I was actually looking at this to attempt to build a similar project before I found this repo. 

https://github.com/portse/plugin.video.ufcfightpass/blob/master/default.py

If you parse out the xbmc parts of the code you can very easily get stream urls. I was having issues pairing them with cookies in browser though. 

ufc.tv is using neulion and swfobject player to handle all the auth management. The code passed down from the server isn't minified but it's a nightmare to comprehend.

### Source URL 
From the above code I was able to parse out this source url. 
```
http://nlds277.cdnak.neulion.com/nlds_vod/ufc/vod/2017/06/17/28527/2_28527_cam1_cam0_2017_cam0_whole_1_ced.mp4.m3u8?hdnea=expires%3D1497944837%7Eaccess%3D%2Fnlds_vod%2Fufc%2Fvod%2F2017%2F06%2F17%2F28527%2F*%7Emd5%3D80b467247583fc329c2ba2767f552276&nltid=ufc&nltdt=8&nltnt=1&uid=2906116|User-Agent=Mozilla/5.0%20(Linux;%20Android%206.0.1;%20D6603%20Build/23.5.A.0.570;%20wv)%20AppleWebKit/537.36%20(KHTML,%20like%20Gecko)%20Version/4.0%20Chrome/56.0.2924.87%20Mobile%20Safari/537.36%20android%20mobile%20ufc%207.0310
```
The userAgent doesn't seem to be completely necessary as the `.m0_whole_1_ced.mp4.m3u8` downloads regardless. 

Contents to that file points to a bunch of smaller `mp4.m3u8` files.
### Stream file contents
```
Contents are this.
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=800000
2_28527_cam1_cam0_2017_cam0_whole_1_800_ced.mp4.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3000000
2_28527_cam1_cam0_2017_cam0_whole_1_3000_ced.mp4.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2400000
2_28527_cam1_cam0_2017_cam0_whole_1_2400_ced.mp4.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1600000
2_28527_cam1_cam0_2017_cam0_whole_1_1600_ced.mp4.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1200000
2_28527_cam1_cam0_2017_cam0_whole_1_1200_ced.mp4.m3u8
```
Appending one of thse files names to the above url produces a larger m3u8 file filled with smaller `.ts` files. I'm assuming those .ts files are encrypted as they don't play in any player.
```
http://nlds277.cdnak.neulion.com/nlds_vod/ufc/vod/2017/06/17/28527/2_28527_cam1_cam0_2017_cam0_whole_1_1200_ced.mp4.m3u8?hdnea=expires%3D1497944837%7Eaccess%3D%2Fnlds_vod%2Fufc%2Fvod%2F2017%2F06%2F17%2F28527%2F*%7Emd5%3D80b467247583fc329c2ba2767f552276&nltid=ufc&nltdt=8&nltnt=1&uid=2906116
```
Notice the bitrate before ced in the above uri. Alternatively, you can replace ced with android but doesn't seem to change much.

Without cookies and pairing with the aes key it doesn't do any good. 

Using Charles while playing on ufc.tv I was able to detect the stream.xml file which had these contents. 
### More Stream Details
```
<?xml version="1.0" encoding="utf-8" ?>
<channel version="3.5" currentTime="1488136644000" currentTimeDescription="2017-02-26 19:17:24" defaultStreamIndex="1">
    <streamDatas>
        <streamData url="/nlds/skysportsfanpass/skysports1/as/live/skysports1_hd_400" isLive="true" blockDuration="2000" bitrate="409600" liveStartupTime="1458138474000" liveDVRDuration="86400000" livePlayerDelay="25000" liveBlockDelay="16000">
            <encryption method="aes-128" keyUrl="/nlds/skysportsfanpass/skysports1/as/live/skysports1_hd_400" keyDuration="3600000" keySecure="false" keyTransfer="secure" token="NLAuth=40a77e36af54091f6442ea8b6e8530b3-0-32;serverKey_internal_ASV3=1b6f3532e8ee36cbcd1e1ba2d3536e32-d47043cd5ef615c8105f12567731d186-1488149269-32" />
            <video width="400" height="224" fps="25.000000" ccChannels="CC1,CC2,CC3,CC4" codec="avc1" />
            <audio channelCount="2" samplesRate="44100" sampleBitSize="16" bitrate="65536" />
            <httpservers>
                <httpserver name="nlds635.cdnaknl.oc.neulion.com" port="80" />
            </httpservers>
            <ranges>
                <range begin="2017-02-25 19:17:22" />
            </ranges>
        </streamData>
        ....
    </streamDatas>
    <ccStreamDatas>
        <streamData url="CC1" language="CC1" displayName="CC1" />
        <streamData url="CC2" language="CC2" displayName="CC2" />
        <streamData url="CC3" language="CC3" displayName="CC3" />
        <streamData url="CC4" language="CC4" displayName="CC4" />
    </ccStreamDatas>
</channel>
```

> `...` - Truncated, view full file [here](http://stream-recorder.com/forum/neul10n-livestreamer-capture-livestreamer-t22707.html?s=fec248f50809e33f379c3cb46b2ed2c7&amp;p=89946&mode=threaded)

**Notice**

> `<encryption method="aes-128" keyUrl="/nlds/skysportsfanpass/skysports1/as/live/skysports1_hd_4500" keyDuration="3600000" keySecure="false" keyTransfer="secure" token="NLAuth=40a77e36af54091f6442ea8b6e8530b3-0-32;serverKey_internal_ASV3=1b6f3532e8ee36cbcd1e1ba2d3536e32-d47043cd5ef615c8105f12567731d186-1488149269-32" />`

Neulion is used for hockey and NBA streams so there's some knowledge sprinkled through various forums related to the service. Though it's hard to tell what is old/new and some of it seems to be under wraps as they are attempting to bypass auth (not my intention).

### Grab-bag of links that I have collected. 
- [Python code to fetch stream url and cookie](https://github.com/portse/plugin.video.ufcfightpass/blob/master/default.py)
- [Kodi forum post detailing above python code](https://forum.kodi.tv/showthread.php?tid=213642&page=6)
- [Example of stream.xml with keys](http://stream-recorder.com/forum/neul10n-livestreamer-capture-livestreamer-t22707.html?s=fec248f50809e33f379c3cb46b2ed2c7&amp;p=89946&mode=threaded)
- [details on parsing neulion streams, possibly old, unrelated ]( https://www.reddit.com/r/NHLStreams/comments/2jz9kv/how_to_record_a_vlc_stream/)




Discussion:
Thanks for the detailed issue @SeanDunford! We'll see if someone is interested in picking this one up. We've had some issues with encryption in the past so I'm not sure how that will impact this one.
@gravyboat - Cool! 

Yeah, the encryption seems to be the tricky part. Does that count as DRM? 

I actually spent a few hours today trying to capture the contents sent to chromecast but it doesn't do anything different than the swfobject. There's also an "isIos" flag that seems to make the video display differently but I haven't dug into it, yet. 






Resolving patch:
diff --git a/src/streamlink/plugins/ufctv.py b/src/streamlink/plugins/ufctv.py
new file mode 100644
--- /dev/null
+++ b/src/streamlink/plugins/ufctv.py
@@ -0,0 +1,91 @@
+from __future__ import print_function
+
+import re
+import string
+from functools import partial
+
+from streamlink.plugin import Plugin, PluginOptions
+from streamlink.plugin.api import http
+from streamlink.plugin.api import useragents
+from streamlink.plugin.api import validate
+from streamlink.stream import HLSStream
+from streamlink.utils import parse_json
+
+
+def js_to_json(data):
+    js_re = re.compile(r'(?!<")(\w+):(?!/)')
+    trimmed = [y.replace("\r", "").strip() for y in data.split(",")]
+    jsons = ','.join([js_re.sub(r'"\1":', x, count=1) for x in trimmed])
+    return parse_json(jsons)
+
+
+class UFCTV(Plugin):
+    url_re = re.compile(r"https?://(?:www\.)?ufc\.tv/(channel|video)/.+")
+    video_info_re = re.compile(r"""program\s*=\s*(\{.*?});""", re.DOTALL)
+    channel_info_re = re.compile(r"""g_channel\s*=\s(\{.*?});""", re.DOTALL)
+
+    stream_api_url = "https://www.ufc.tv/service/publishpoint"
+    auth_url = "https://www.ufc.tv/secure/authenticate"
+    auth_schema = validate.Schema(validate.xml_findtext("code"))
+
+    options = PluginOptions({
+        "username": None,
+        "password": None
+    })
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls.url_re.match(url) is not None
+
+    def _get_stream_url(self, video_id, vtype="video"):
+        res = http.post(self.stream_api_url, data={
+            "id": video_id,
+            "type": vtype,
+            "format": "json"
+        }, headers={
+            "User-Agent": useragents.IPHONE_6
+        })
+        data = http.json(res)
+        return data.get("path")
+
+    def _get_info(self, url):
+        res = http.get(url)
+        # try to find video info first
+        m = self.video_info_re.search(res.text)
+        if not m:
+            # and channel info if that fails
+            m = self.channel_info_re.search(res.text)
+        return m and js_to_json(m.group(1))
+
+    def _login(self, username, password):
+        res = http.post(self.auth_url, data={
+            "username": username,
+            "password": password,
+            "cookielink": False
+        })
+        login_status = http.xml(res, schema=self.auth_schema)
+        self.logger.debug("Login status for {0}: {1}", username, login_status)
+        if login_status == "loginlocked":
+            self.logger.error("The account {0} has been locked, the password needs to be reset")
+        return login_status == "loginsuccess"
+
+    def _get_streams(self):
+        if self.get_option("username") and self.get_option("password"):
+            self.logger.debug("Attempting login as {0}", self.get_option("username"))
+            if self._login(self.get_option("username"), self.get_option("password")):
+                self.logger.info("Successfully logged in as {0}", self.get_option("username"))
+            else:
+                self.logger.info("Failed to login as {0}", self.get_option("username"))
+
+        video = self._get_info(self.url)
+        if video:
+            self.logger.debug("Found {type}: {name}", **video)
+            surl = self._get_stream_url(video['id'], video.get('type', "video"))
+            if surl:
+                return HLSStream.parse_variant_playlist(self.session, surl)
+            else:
+                self.logger.error("Could not get stream URL for video: {name} ({id})", **video)
+        else:
+            self.logger.error("Could not find any video info on the page")
+
+__plugin__ = UFCTV
diff --git a/src/streamlink_cli/argparser.py b/src/streamlink_cli/argparser.py
--- a/src/streamlink_cli/argparser.py
+++ b/src/streamlink_cli/argparser.py
@@ -1271,6 +1271,20 @@ def boolean(value):
     A bbc.co.uk account password to use with --bbciplayer-username.
     """
 )
+plugin.add_argument(
+    "--ufctv-username",
+    metavar="USERNAME",
+    help="""
+    The username used to register with ufc.tv.
+    """
+)
+plugin.add_argument(
+    "--ufctv-password",
+    metavar="PASSWORD",
+    help="""
+    A ufc.tv account password to use with --ufctv-username.
+    """
+)
 
 # Deprecated options
 stream.add_argument(
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -930,6 +930,17 @@ def setup_plugin_options():
     if bbciplayer_password:
         streamlink.set_plugin_option("bbciplayer", "password", bbciplayer_password)
 
+    if args.ufctv_username:
+        streamlink.set_plugin_option("ufctv", "username", args.ufctv_username)
+
+    if args.ufctv_username and not args.ufctv_password:
+        ufctv_password = console.askpass("Enter ufc.tv account password: ")
+    else:
+        ufctv_password = args.ufctv_password
+
+    if ufctv_password:
+        streamlink.set_plugin_option("ufctv", "password", ufctv_password)
+
     # Deprecated options
     if args.jtv_legacy_names:
         console.logger.warning("The option --jtv/twitch-legacy-names is "


Test patch: