Repository: streamlink/streamlink
Pull request #896 (2017-05-08 02:41:23)

Add a few more examples to the player option
This adds a few more examples to the player docs to hopefully address confusion like in https://github.com/streamlink/streamlink/issues/895


Patch:
diff --git a/.travis.yml b/.travis.yml
index 874d4514..d12eec06 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -56,7 +56,7 @@ deploy:
     skip_cleanup: true
     on:
       tags: true
-      condition: $BUILD_INSTALLER = yes
+      condition: $BUILD_INSTALLER == yes
   - provider: bintray
     file: build/bintray-nightly.json
     user: "${BINTRAY_USER}"
@@ -64,7 +64,7 @@ deploy:
     skip_cleanup: true
     on:
       branch: master
-      condition: $BUILD_INSTALLER = yes && $TRAVIS_EVENT_TYPE == cron
+      condition: $BUILD_INSTALLER == yes && $TRAVIS_EVENT_TYPE == cron
   - provider: bintray
     file: build/bintray-latest.json
     user: "${BINTRAY_USER}"
@@ -72,4 +72,7 @@ deploy:
     skip_cleanup: true
     on:
       branch: master
-      condition: $BUILD_INSTALLER = yes && $TRAVIS_EVENT_TYPE == cron
+      condition: $BUILD_INSTALLER == yes && $TRAVIS_EVENT_TYPE == cron
+
+after_deploy:
+  - if [[ "$BUILD_INSTALLER" == 'yes' && "$TRAVIS_EVENT_TYPE" == 'cron' ]]; then ./script/bintrayupdate.sh; fi
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 855769e2..08cddb4c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,3 +1,116 @@
+streamlink 0.6.0 (2017-05-11)
+-----------------------------
+Another release of Streamlink!
+
+We've updated more plugins, improved documentation, and moved out nightly builds to Bintray (S3 was costing *wayyyy* too much).
+
+Again, many thanks for those who've contributed!
+
+If you think that this application is helpful, please consider supporting the maintainers by [donating](https://streamlink.github.io/donate.html).
+
+Thank you very much!
+
+::
+
+    Daniel Draper <Germandrummer92@users.noreply.github.com> (1):
+          Will exit with exit code 1 if stream cannot be opened. (#785)
+    
+    Forrest Alvarez <gravyboat@users.noreply.github.com> (3):
+          Update readme so users are aware using Streamlink bypasses ads
+          Forgot a )
+          Make notice more agnostic
+    
+    Mohamed El Morabity <melmorabity@fedoraproject.org> (18):
+          Disable HDS streams which are no more available
+          Add support for pc-yourfreetv.com
+          Add support for BFMTV
+          Add support for Cam4
+          Disable HDS streams for live videos
+          Add support for Bloomberg
+          Add support for Bloomberg Radio live stream
+          Add support for cnews.fr
+          Fix unit tests for canalplus plugin
+          Add authentication token to http queries
+          Add rte.ie/player support
+          Add support for HLS streams
+          Update for new page layout
+          Update for new new page layout
+          Fix for new layout
+          Pluzz platform replaced by new france.tv website
+          Update documentation
+          Always use token generator for streams from france.tv
+    
+    Mohamed El Morabity <melmorabity@users.noreply.github.com> (1):
+          plugins.brightcove: support for HLS stream URLs with query strings + RTMPE stream URLs (#790)
+    
+    RosadinTV <rosadintv@outlook.com> (5):
+          Update plugin_matrix.rst
+          Add telefe.py
+          Add test_plugin_telefe.py
+          Update telefe.py
+          Add support for ElTreceTV (VOD & Live) (#816)
+    
+    Sebastian Meyer <mail@bastimeyer.de> (1):
+          Improve contribution guidelines (#772)
+    
+    back-to <back-to@users.noreply.github.com> (9):
+          [chaturbate] New API for HLS url
+          [chaturbate] Fixed python 3.5 bug and added regex tests
+          [VRTbe] new plugin for vrt.be/vrtnu
+          [oldlivestream] New regex for cdn subdomains and embeded streams
+          [tv1channel.org] New Plugin for embeded streams on tv1channel.org
+          [cyro] New plugin for embeded streams from cyro.se
+          [Facebook] Added unittests
+          [ArteTV] new regex, removed rtmp and better result for available streams
+          [NRK.NO] fixed regex for _api_baseurl_re
+    
+    beardypig <beardypig@protonmail.com> (15):
+          travis: use pytest to run the tests for coverage
+          Revert "stream.hds: ensure the live edge does not go past the latest fragment"
+          plugins.azubutv: plugin removed
+          plugins.ustreamtv: log timeout errors and adjust retries for polling
+          appveyor: update config to fix builds on Python 3.3
+          plugin.tvplayer: update to support new site layout
+          plugin.tvplayer: update tests to match new plugin
+          plugins.tvplayer: allow https stream URLs
+          plugins.tvnbg: add support for live streams on tvn.bg
+          plugins.apac: add ustream apac wrapper
+          Deploy nightly builds to Bintray instead of S3
+          plugins.streann: support for ott.streann.com
+          utils.crypto: fix openssl_decrypt for py27
+          build: update the bintray release notes for nightlies
+          plugins.streamable: support for videos on streamable.com
+    
+    beardypig <beardypig@users.noreply.github.com> (20):
+          plugins.ustreamtv: support for the new ustream.tv API
+          plugins.ustreamtv: add suppot for redirectLocked embedded streams
+          plugins.livecodingtv: renamed to livedu, and updated for new site
+          plugins.ustreamtv: continue to poll the ustream API when streaming
+          plugins.ustreamtv: rename the plugin class back to UStreamTV
+          docs: remove references to python-librtmp
+          plugins.ustream: add some comments
+          plugins.ustreamtv: support for password protected streams
+          plugins.nbc: support vod from nbc.com
+          plugins.nbcsports: add support for nbcsports.com via theplatform
+          stream.hds: ensure the live edge does not go past the latest fragment
+          Dailymotion feature video and backup stream fallback (#773)
+          plugin.gardenersworld: support for VOD on gardenersworld.com
+          plugins.twitch: support for pop-out player URLS and fixed clips
+          tests: cmdline tests can fail if there are some config options set
+          plugins.ustreamtv: fix moduleInfo retry loop
+          cli: add --url option that can be used in config files to set a URL
+          cli: clarification of the --url option
+          cli: add wildcard to --stream-types option
+          plugins.rtve: stop IOError bubbling up on 404 errors
+    
+    wlerin <wlerin@gmail.com> (2):
+          Send Referer and UserAgent headers
+          Fix method decorator
+    
+    zp@users.noreply.github.com <zp@users.noreply.github.com> (1):
+          New plugin for Facebook 360p streams https://gist.github.com/zp/c461761565dba764c90548758ee5ae9f
+    
+
 streamlink 0.5.0 (2017-04-04)
 -----------------------------
 Streamlink 0.5.0!
diff --git a/docs-requirements.txt b/docs-requirements.txt
index 6966869c..060735e4 100644
--- a/docs-requirements.txt
+++ b/docs-requirements.txt
@@ -1,1 +1,2 @@
-sphinx
+sphinx>=1.6,<1.7
+
diff --git a/docs/conf.py b/docs/conf.py
index a1c0ff12..5591eb25 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -135,10 +135,6 @@ html_static_path = ['_static']
 # using the given strftime format.
 #html_last_updated_fmt = '%b %d, %Y'
 
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
-#html_use_smartypants = True
-
 # Custom sidebar templates, maps document names to template names.
 #html_sidebars = {}
 
diff --git a/docs/docutils.conf b/docs/docutils.conf
new file mode 100644
index 00000000..42e9d6f6
--- /dev/null
+++ b/docs/docutils.conf
@@ -0,0 +1,3 @@
+[restructuredtext parser]
+smart_quotes=no
+
diff --git a/docs/install.rst b/docs/install.rst
index b864ae00..3e2d1513 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -64,15 +64,26 @@ Platform                             Installing
 Mac OS X                             .. code-block:: console
 
                                         # easy_install -U streamlink
+`Homebrew`_                          .. code-block:: console
+
+                                        # brew install streamlink
+
+                                     `Installing Homebrew packages`_
 Microsoft Windows                    See `Windows binaries`_ and `Windows portable version`_.
 
 `Chocolatey`_                        .. code-block:: console
 
                                         C:\> choco install streamlink
+
+                                     `Installing Chocolatey packages`_
 ==================================== ===========================================
 
+.. _Homebrew: https://github.com/Homebrew/homebrew-core/blob/master/Formula/streamlink.rb
 .. _Chocolatey: https://chocolatey.org/packages/streamlink
 
+.. _Installing Homebrew packages: https://brew.sh
+.. _Installing Chocolatey packages: https://chocolatey.org
+
 Package maintainers
 -------------------
 ==================================== ===========================================
@@ -237,11 +248,13 @@ Windows binaries
     Windows XP is not supported.
     Windows Vista requires at least SP2 to be installed. 
 
-You can download the latest stable Windows installer from the `GitHub Releases Page <https://github.com/streamlink/streamlink/releases/latest>`__.
+A Windows installer of the latest **stable release** can be found on the `GitHub releases page <https://github.com/streamlink/streamlink/releases/latest>`__.
 
-Alternatively, you can download the latest `nightly Windows installer <https://dl.bintray.com/streamlink/streamlink-nightly/streamlink-latest.exe>`__.
+Alternatively, a Windows installer of the `latest development build <https://dl.bintray.com/streamlink/streamlink-nightly/streamlink-latest.exe>`__ for testing purposes is available,
+with a summary of the changes in the `release notes <https://bintray.com/streamlink/streamlink-nightly/streamlink/latest#release>`__. This development build is updated once per day,
+and a list of `previous builds <https://dl.bintray.com/streamlink/streamlink-nightly/>`__ is provided.
 
-This is a installer which contains:
+This is an installer which contains:
 
 - A compiled version of Streamlink that does not require an existing Python
   installation
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 4fdc7e46..a524287f 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -112,7 +112,8 @@ goodgame            goodgame.ru          Yes   No    Only HLS streams are availa
 googledrive         - docs.google.com    --    Yes
                     - drive.google.com
 gulli               replay.gulli.fr      Yes   Yes   Streams may be geo-restricted to France.
-hitbox              hitbox.tv            Yes   Yes
+hitbox              - hitbox.tv          Yes   Yes
+                    - smashcast.tv
 huajiao             huajiao.com          Yes   No    
 huomao              huomao.com           Yes   No
 huya                huya.com             Yes   No    Temporarily only HLS streams available.
@@ -149,7 +150,7 @@ pcyourfreetv        pc-yourfreetv.com    Yes   --    Requires a login.
 periscope           periscope.tv         Yes   Yes   Replay/VOD is supported.
 picarto             picarto.tv           Yes   --
 playtv              playtv.fr            Yes   --    Streams may be geo-restricted to France.
-pluzz               - pluzz.francetv.fr  Yes   Yes   Streams may be geo-restricted to France, Andorra and Monaco.
+pluzz               - france.tv          Yes   Yes   Streams may be geo-restricted to France, Andorra and Monaco.
                     - ludo.fr
                     - zouzous.fr
 powerapp            powerapp.com.tr      Yes   No
@@ -172,6 +173,7 @@ srgssr              - srf.ch             Yes   No    Streams are geo-restricted
                     - rsi.ch
                     - rtr.ch
 ssh101              ssh101.com           Yes   No
+streamable          streamable.com       -     Yes
 streamboat          streamboat.tv        Yes   No
 streamingvi... [1]_ streamingvid... [2]_ Yes   --    RTMP streams requires rtmpdump with
                                                      K-S-V patches.
diff --git a/script/bintrayconfig.sh b/script/bintrayconfig.sh
index f5c2f7ef..f4e64ab4 100755
--- a/script/bintrayconfig.sh
+++ b/script/bintrayconfig.sh
@@ -82,3 +82,12 @@ cat > "${build_dir}/bintray-nightly.json" <<EOF
 EOF
 
 echo "Wrote Bintray config to: ${build_dir}/bintray-nightly.json"
+
+# update repo to full clone and get the tags
+git fetch --unshallow
+git fetch --tags
+latest_tag=$(git describe --tags --abbrev=0)
+tag_changes_json=$(git log ${latest_tag}..HEAD --no-merges --pretty=format:" * [\`%h\`](https://github.com/streamlink/streamlink/commit/%H) %s" | sed ':a;N;$!ba;s/\n/\\n/g' | sed 's/"/\\"/g')
+echo "{\"bintray\": {\"content\": \"**This build includes the following changes since [v${latest_tag}](https://github.com/streamlink/streamlink/releases/tag/${latest_tag})**\n\n${tag_changes_json}\"}}" > "${build_dir}/bintray-changelog.json"
+
+echo "Wrote changelog to ${build_dir}/bintray-changelog.json"
diff --git a/script/bintrayupdate.sh b/script/bintrayupdate.sh
new file mode 100755
index 00000000..74e16e7d
--- /dev/null
+++ b/script/bintrayupdate.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+# Update the Bintray release
+
+build_dir="$(pwd)/build"
+current_api_url="https://api.bintray.com/packages/streamlink/streamlink-nightly/streamlink/versions/$(date +'%Y.%m.%d')/release_notes"
+latest_api_url="https://api.bintray.com/packages/streamlink/streamlink-nightly/streamlink/versions/latest/release_notes"
+curl -u "${BINTRAY_USER}:${BINTRAY_KEY}" -H "Content-Type: application/json" -d @"${build_dir}/bintray-changelog.json" ${current_api_url}
+curl -u "${BINTRAY_USER}:${BINTRAY_KEY}" -H "Content-Type: application/json" -d @"${build_dir}/bintray-changelog.json" ${latest_api_url}
diff --git a/setup.py b/setup.py
index 5840c171..a5f9bc86 100644
--- a/setup.py
+++ b/setup.py
@@ -53,7 +53,7 @@ srcdir = join(dirname(abspath(__file__)), "src/")
 sys_path.insert(0, srcdir)
 
 setup(name="streamlink",
-      version="0.5.0",
+      version="0.6.0",
       description="Streamlink is command-line utility that extracts streams "
                   "from various services and pipes them into a video player of "
                   "choice.",
diff --git a/src/streamlink/__init__.py b/src/streamlink/__init__.py
index 1a45a45a..28dbb515 100644
--- a/src/streamlink/__init__.py
+++ b/src/streamlink/__init__.py
@@ -22,7 +22,7 @@ if version_info[:2] == (2, 6):
     )
 
 __title__ = "streamlink"
-__version__ = "0.5.0"
+__version__ = "0.6.0"
 __license__ = "Simplified BSD"
 __author__ = "Streamlink"
 __copyright__ = "Copyright 2016 Streamlink"
diff --git a/src/streamlink/plugins/bbciplayer.py b/src/streamlink/plugins/bbciplayer.py
index d4c37ad9..a5c326e4 100644
--- a/src/streamlink/plugins/bbciplayer.py
+++ b/src/streamlink/plugins/bbciplayer.py
@@ -5,7 +5,7 @@ import re
 from functools import partial
 from hashlib import sha1
 
-from streamlink.plugin import Plugin
+from streamlink.plugin import Plugin, PluginOptions
 from streamlink.plugin.api import http
 from streamlink.plugin.api import validate
 from streamlink.stream import HDSStream
@@ -20,14 +20,28 @@ class BBCiPlayer(Plugin):
             live/(?P<channel_name>\w+)
         )
     """, re.VERBOSE)
-    vpid_re = re.compile(r'"vpid"\s*:\s*"(\w+)"')
+    vpid_re = re.compile(r'"ident_id"\s*:\s*"(\w+)"')
     tvip_re = re.compile(r'event_master_brand=(\w+?)&')
+    account_locals_re = re.compile(r'window.bbcAccount.locals\s*=\s*(\{.*?});')
     swf_url = "http://emp.bbci.co.uk/emp/SMPf/1.18.3/StandardMediaPlayerChromelessFlash.swf"
     hash = base64.b64decode(b"N2RmZjc2NzFkMGM2OTdmZWRiMWQ5MDVkOWExMjE3MTk5MzhiOTJiZg==")
     api_url = ("http://open.live.bbc.co.uk/mediaselector/5/select/"
                "version/2.0/mediaset/{platform}/vpid/{vpid}/atk/{vpid_hash}/asn/1/")
     platforms = ("pc", "iptv-all")
+    config_url = "http://www.bbc.co.uk/idcta/config"
+    auth_url = "https://account.bbc.com/signin"
 
+    config_schema = validate.Schema(
+        validate.transform(parse_json),
+        {
+            "signin_url": validate.url(),
+            "identity": {
+                "cookieAgeDays": int,
+                "accessTokenCookieName": validate.text,
+                "idSignedInCookieName": validate.text
+            }
+         }
+    )
     mediaselector_schema = validate.Schema(
         validate.transform(partial(parse_xml, ignore_ns=True)),
         validate.union({
@@ -39,6 +53,10 @@ class BBCiPlayer(Plugin):
             validate.transform(lambda x: list(set(x)))  # unique
         )}
     )
+    options = PluginOptions({
+        "password": None,
+        "username": None
+    })
 
     @classmethod
     def can_handle_url(cls, url):
@@ -48,9 +66,10 @@ class BBCiPlayer(Plugin):
     def _hash_vpid(cls, vpid):
         return sha1(cls.hash + str(vpid).encode("utf8")).hexdigest()
 
-    def find_vpid(self, url):
+    def find_vpid(self, url, res=None):
         self.logger.debug("Looking for vpid on {0}", url)
-        res = http.get(url)
+        # Use pre-fetched page if available
+        res = res or http.get(url)
         m = self.vpid_re.search(res.text)
         return m and m.group(1)
 
@@ -71,14 +90,46 @@ class BBCiPlayer(Plugin):
                 for s in HDSStream.parse_manifest(self.session, surl).items():
                     yield s
 
+    def login(self, ptrt_url, context="tvandiplayer"):
+        # get the site config, to find the signin url
+        config = http.get(self.config_url, params=dict(ptrt=ptrt_url), schema=self.config_schema)
+
+        res = http.get(config["signin_url"],
+                       params=dict(userOrigin=context, context=context),
+                       headers={"Referer": self.url})
+        m = self.account_locals_re.search(res.text)
+        if m:
+            auth_data = parse_json(m.group(1))
+            res = http.post(self.auth_url,
+                            params=dict(context=auth_data["userOrigin"],
+                                        ptrt=auth_data["ptrt"]["value"],
+                                        userOrigin=auth_data["userOrigin"],
+                                        nonce=auth_data["nonce"]),
+                            data=dict(jsEnabled="false", attempts=0, username=self.get_option("username"),
+                                      password=self.get_option("password")))
+            # redirects to ptrt_url on successful login
+            if res.url == ptrt_url:
+                return res
+        else:
+            self.logger.error("Could not authenticate, could not find the authentication nonce")
+
     def _get_streams(self):
+        self.logger.info("A TV License is required to watch BBC iPlayer streams, see the BBC website for more "
+                         "information: https://www.bbc.co.uk/iplayer/help/tvlicence")
+        page_res = None
+        if self.get_option("username"):
+            page_res = self.login(self.url)
+            if not page_res:
+                self.logger.error("Could not authenticate, check your username and password")
+                return
+
         m = self.url_re.match(self.url)
         episode_id = m.group("episode_id")
         channel_name = m.group("channel_name")
 
         if episode_id:
             self.logger.debug("Loading streams for episode: {0}", episode_id)
-            vpid = self.find_vpid(self.url)
+            vpid = self.find_vpid(self.url, res=page_res)
             if vpid:
                 self.logger.debug("Found VPID: {0}", vpid)
                 for s in self.mediaselector(vpid):
@@ -93,5 +144,4 @@ class BBCiPlayer(Plugin):
                 for s in self.mediaselector(tvip):
                     yield s
 
-
 __plugin__ = BBCiPlayer
diff --git a/src/streamlink/plugins/euronews.py b/src/streamlink/plugins/euronews.py
index bacb7ae3..45d88ed2 100644
--- a/src/streamlink/plugins/euronews.py
+++ b/src/streamlink/plugins/euronews.py
@@ -9,24 +9,14 @@ from streamlink.stream import HLSStream, HTTPStream
 class Euronews(Plugin):
     _url_re = re.compile(r"http(?:s)?://(\w+)\.?euronews.com/(live|.*)")
     _re_vod = re.compile(r'<meta\s+property="og:video"\s+content="(http.*?)"\s*/>')
-    _live_api_url = "http://fr.euronews.com/api/watchlive.json"
+    _live_api_url = "http://{0}.euronews.com/api/watchlive.json"
     _live_schema = validate.Schema({
         u"url": validate.url()
     })
     _stream_api_schema = validate.Schema({
         u'status': u'ok',
-        u'primary': {
-            validate.text: {
-                validate.optional(u'hls'): validate.url(),
-                validate.optional(u'rtsp'): validate.url(scheme="rtsp")
-            }
-        },
-        validate.optional(u'backup'): {
-            validate.text: {
-                validate.optional(u'hls'): validate.url(),
-                validate.optional(u'rtsp'): validate.url(scheme="rtsp")
-            }
-        }
+        u'primary': validate.url(),
+        validate.optional(u'backup'): validate.url()
     })
 
     @classmethod
@@ -43,20 +33,17 @@ class Euronews(Plugin):
         if len(video_urls):
             return dict(vod=HTTPStream(self.session, video_urls[0]))
 
-    def _get_live_streams(self, language):
+    def _get_live_streams(self, subdomain):
         """
         Get the live stream in a particular language
-        :param language:
+        :param subdomain:
         :return:
         """
-        res = http.get(self._live_api_url)
+        res = http.get(self._live_api_url.format(subdomain))
         live_res = http.json(res, schema=self._live_schema)
         api_res = http.get(live_res[u"url"])
         stream_data = http.json(api_res, schema=self._stream_api_schema)
-        # find the stream in the requested language
-        if language in stream_data[u'primary']:
-            playlist_url = stream_data[u'primary'][language][u"hls"]
-            return HLSStream.parse_variant_playlist(self.session, playlist_url)
+        return HLSStream.parse_variant_playlist(self.session, stream_data[u'primary'])
 
     def _get_streams(self):
         """
@@ -64,13 +51,10 @@ class Euronews(Plugin):
         :return:
         """
         match = self._url_re.match(self.url)
-        language, path = match.groups()
-
-        # remap domain to language (default to english)
-        language = {"www": "en", "": "en", "arabic": "ar"}.get(language, language)
+        subdomain, path = match.groups()
 
         if path == "live":
-            return self._get_live_streams(language)
+            return self._get_live_streams(subdomain)
         else:
             return self._get_vod_stream()
 
diff --git a/src/streamlink/plugins/hitbox.py b/src/streamlink/plugins/hitbox.py
index 3502aae0..4d7a3e9b 100644
--- a/src/streamlink/plugins/hitbox.py
+++ b/src/streamlink/plugins/hitbox.py
@@ -8,25 +8,27 @@ from streamlink.plugin.api import StreamMapper, http, validate
 from streamlink.stream import HLSStream, HTTPStream, RTMPStream
 from streamlink.utils import absolute_url
 
-HLS_PLAYLIST_BASE = "http://www.hitbox.tv{0}"
-LIVE_API = "http://www.hitbox.tv/api/media/live/{0}?showHidden=true"
-PLAYER_API = "http://www.hitbox.tv/api/player/config/{0}/{1}?embed=false&showHidden=true"
+HLS_PLAYLIST_BASE = "http://www.smashcast.tv{0}"
+LIVE_API = "http://www.smashcast.tv/api/media/live/{0}?showHidden=true&liveonly=false"
+PLAYER_API = "http://www.smashcast.tv/api/player/config/{0}/{1}?embed=false&showHidden=true"
 SWF_BASE = "http://edge.vie.hitbox.tv/static/player/flowplayer/"
 SWF_URL = SWF_BASE + "flowplayer.commercial-3.2.16.swf"
-VOD_BASE_URL = "http://www.hitbox.tv/"
+VOD_BASE_URL = "http://www.smashcast.tv/"
 
 _quality_re = re.compile(r"(\d+p)$")
 _url_re = re.compile(r"""
-    http(s)?://(www\.)?hitbox.tv
+    http(s)?://(www\.)?(hitbox|smashcast).tv
     /(?P<channel>[^/]+)
     (?:
-        /(?P<media_id>[^/]+)
+        (?:/videos)?/(?P<media_id>[^/]+)
     )?
 """, re.VERBOSE)
 
 _live_schema = validate.Schema(
     {
         "livestream": [{
+            "media_user_name": validate.text,
+            validate.optional("media_hosted_media"): object,
             "media_is_live": validate.all(
                 validate.text,
                 validate.transform(int),
@@ -76,7 +78,7 @@ _player_schema = validate.Schema(
 
 class Hitbox(Plugin):
     @classmethod
-    def can_handle_url(self, url):
+    def can_handle_url(cls, url):
         return _url_re.match(url)
 
     def _get_quality(self, label):
@@ -172,9 +174,15 @@ class Hitbox(Plugin):
             return
 
         channel, media_id = match.group("channel", "media_id")
-        if channel != "video":
+        self.logger.debug("Matched URL: channel={0}, media_id={1}".format(channel, media_id))
+        if not media_id:
             res = http.get(LIVE_API.format(channel))
             livestream = http.json(res, schema=_live_schema)
+            if livestream["media_hosted_media"]:
+                hosted = _live_schema.validate(livestream["media_hosted_media"])
+                self.logger.info("{0} is hosting {1}", livestream["media_user_name"], hosted["media_user_name"])
+                livestream = hosted
+
             if not livestream["media_is_live"]:
                 return
 
diff --git a/src/streamlink/plugins/nrk.py b/src/streamlink/plugins/nrk.py
index ed97fa49..031c4cb9 100644
--- a/src/streamlink/plugins/nrk.py
+++ b/src/streamlink/plugins/nrk.py
@@ -13,7 +13,7 @@ COOKIE_PARAMS = (
 
 _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>[^"]+)"')
+_api_baseurl_re = re.compile(r'''apiBaseUrl:\s*["'](?P<baseurl>[^"']+)["']''')
 
 _schema = validate.Schema(
     validate.transform(_api_baseurl_re.search),
diff --git a/src/streamlink/plugins/pcyourfreetv.py b/src/streamlink/plugins/pcyourfreetv.py
index c116823b..cf253c2c 100644
--- a/src/streamlink/plugins/pcyourfreetv.py
+++ b/src/streamlink/plugins/pcyourfreetv.py
@@ -11,7 +11,7 @@ class PCYourFreeTV(Plugin):
     _url_re = re.compile(r'http://pc-yourfreetv\.com/index_player\.php\?channel=.+?&page_id=\d+')
     _iframe_re = re.compile(r"<iframe .*?\bsrc='(?P<iframe>.+?)'.*?</iframe>")
     _player_re = re.compile(r"<script language=JavaScript>m='(?P<player>.+?)'", re.DOTALL)
-    _video_url_re = re.compile(r'new YourFreeTV.Player\({source: "(?P<video_url>[^"]+?)".+?}.*\);', re.DOTALL)
+    _video_url_re = re.compile(r'new [a-zA-Z0-9]+.Player\({source: "(?P<video_url>[^"]+?)".+?}.*\);', re.DOTALL)
 
     options = PluginOptions({
         'username': None,
@@ -53,13 +53,13 @@ class PCYourFreeTV(Plugin):
             return
 
         res = http.get(match.group('iframe'))
-        match = self._player_re.search(res.text)
-        if match is None:
+        players = self._player_re.findall(res.text)
+        if len(players) == 0 is None:
             return
 
-        while match is not None:
-            player = unquote(match.group('player'))
-            match = self._player_re.search(player)
+        while len(players) > 0:
+            player = unquote(players[-1])
+            players = self._player_re.findall(player)
 
         match = self._video_url_re.search(player)
         if match is None:
diff --git a/src/streamlink/plugins/pluzz.py b/src/streamlink/plugins/pluzz.py
index db49cdc1..0e9377c0 100644
--- a/src/streamlink/plugins/pluzz.py
+++ b/src/streamlink/plugins/pluzz.py
@@ -1,4 +1,3 @@
-import os.path
 import re
 import sys
 import time
@@ -12,11 +11,12 @@ from streamlink.utils import update_scheme
 
 class Pluzz(Plugin):
     GEO_URL = 'http://geo.francetv.fr/ws/edgescape.json'
-    API_URL = 'http://sivideo.webservices.francetelevisions.fr/tools/getInfosOeuvre/v2/?idDiffusion={0}&catalogue={1}'
+    API_URL = 'http://sivideo.webservices.francetelevisions.fr/tools/getInfosOeuvre/v2/?idDiffusion={0}'
+    PLAYER_GENERATOR_URL = 'https://sivideo.webservices.francetelevisions.fr/assets/staticmd5/getUrl?id=jquery.player.7.js'
     TOKEN_URL = 'http://hdfauthftv-a.akamaihd.net/esi/TA?url={0}'
 
-    _url_re = re.compile(r'http://(pluzz\.francetv\.fr/(videos/.+\.html|[\w-]+)|www\.(ludo|zouzous)\.fr/heros/[\w-]+)')
-    _pluzz_video_id_re = re.compile(r'id="current_video" href="http://.+?\.(?:francetv|francetelevisions)\.fr/(?:video/|\?id-video=)(?P<video_id>.+?)"')
+    _url_re = re.compile(r'https?://((?:www)\.france\.tv/.+\.html|www\.(ludo|zouzous)\.fr/heros/[\w-]+)')
+    _pluzz_video_id_re = re.compile(r'data-main-video="(?P<video_id>.+?)"')
     _other_video_id_re = re.compile(r'playlist: \[{.*?,"identity":"(?P<video_id>.+?)@(?P<catalogue>Ludo|Zouzous)"')
     _player_re = re.compile(r'src="(?P<player>//staticftv-a\.akamaihd\.net/player/jquery\.player.+?-[0-9a-f]+?\.js)"></script>')
     _swf_re = re.compile(r'//staticftv-a\.akamaihd\.net/player/bower_components/player_flash/dist/FranceTVNVPVFlashPlayer\.akamai-[0-9a-f]+\.swf')
@@ -71,6 +71,8 @@ class Pluzz(Plugin):
         )
     })
 
+    _player_schema = validate.Schema({'result': validate.url()})
+
     options = PluginOptions({
         "mux_subtitles": False
     })
@@ -87,29 +89,21 @@ class Pluzz(Plugin):
 
         # Retrieve URL page and search for video ID
         res = http.get(self.url)
-        if 'pluzz.francetv.fr' in self.url:
-            video_re = self._pluzz_video_id_re
-        else:
-            video_re = self._other_video_id_re
-        match = video_re.search(res.text)
+        match = self._pluzz_video_id_re.search(res.text) or self._other_video_id_re.search(res.text)
         if match is None:
             return
-        catalogue = 'Pluzz'
         video_id = match.group('video_id')
-        if 'catalogue' in match.groupdict():
-            catalogue = match.group('catalogue')
 
         # Retrieve SWF player URL
-        match = self._player_re.search(res.text)
         swf_url = None
+        res = http.get(self.PLAYER_GENERATOR_URL)
+        player_url = update_scheme(self.url, http.json(res, schema=self._player_schema)['result'])
+        res = http.get(player_url)
+        match = self._swf_re.search(res.text)
         if match is not None:
-            player_url = update_scheme(self.url, match.group('player'))
-            res = http.get(player_url)
-            match = self._swf_re.search(res.text)
-            if match is not None:
-                swf_url = update_scheme(self.url, match.group(0))
+            swf_url = update_scheme(self.url, match.group(0))
 
-        res = http.get(self.API_URL.format(video_id, catalogue))
+        res = http.get(self.API_URL.format(video_id))
         videos = http.json(res, schema=self._api_schema)
         now = time.time()
 
@@ -151,12 +145,15 @@ class Pluzz(Plugin):
             if '.mpd' in video_url:
                 continue
 
-            if catalogue == 'Pluzz' or '.f4m' in video_url:
+            if '.f4m' in video_url or 'france.tv' in self.url:
                 res = http.get(self.TOKEN_URL.format(video_url))
                 video_url = res.text
 
             if '.f4m' in video_url and swf_url is not None:
-                for bitrate, stream in HDSStream.parse_manifest(self.session, video_url, pvswf=swf_url).items():
+                for bitrate, stream in HDSStream.parse_manifest(self.session,
+                                                                video_url,
+                                                                is_akamai=True,
+                                                                pvswf=swf_url).items():
                     # HDS videos with data in their manifest fragment token
                     # doesn't seem to be supported by HDSStream. Ignore such
                     # stream (but HDS stream having only the hdntl parameter in
diff --git a/src/streamlink/plugins/streamable.py b/src/streamlink/plugins/streamable.py
new file mode 100644
index 00000000..e6f45af4
--- /dev/null
+++ b/src/streamlink/plugins/streamable.py
@@ -0,0 +1,40 @@
+import re
+
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http, validate
+from streamlink.stream import HTTPStream
+from streamlink.utils import parse_json, update_scheme
+
+
+class Streamable(Plugin):
+    url_re = re.compile(r"https?://(?:www\.)?streamable\.com/(.+)")
+    meta_re = re.compile(r'''var\s*videoObject\s*=\s*({.*});''')
+    config_schema = validate.Schema(
+        validate.transform(meta_re.search),
+        validate.any(None,
+                     validate.all(
+                         validate.get(1),
+                         validate.transform(parse_json),
+                         {
+                             "files": {validate.text: {"url": validate.url(),
+                                                       "width": int,
+                                                       "height": int,
+                                                       "bitrate": int}}
+                         })
+                     )
+    )
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls.url_re.match(url) is not None
+
+    def _get_streams(self):
+        data = http.get(self.url, schema=self.config_schema)
+
+        for info in data["files"].values():
+            stream_url = update_scheme(self.url,  info["url"])
+            # pick the smaller of the two dimensions, for landscape v. portrait videos
+            res = min(info["width"], info["height"])
+            yield "{0}p".format(res), HTTPStream(self.session, stream_url)
+
+__plugin__ = Streamable
diff --git a/src/streamlink/plugins/tvplayer.py b/src/streamlink/plugins/tvplayer.py
index 9d8e637d..4e4d8523 100644
--- a/src/streamlink/plugins/tvplayer.py
+++ b/src/streamlink/plugins/tvplayer.py
@@ -30,6 +30,7 @@ class TVPlayer(Plugin):
         validate.get("response"))
     context_schema = validate.Schema({
         "validate": validate.text,
+        validate.optional("token"): validate.text,
         "platform": {
             "key": validate.text
         }
@@ -59,6 +60,7 @@ class TVPlayer(Plugin):
 
     def _get_stream_data(self, resource, token, service=1):
         # Get the context info (validation token and platform)
+        self.logger.debug("Getting stream information for resource={0}".format(resource))
         context_res = http.get(self.context_url, params={"resource": resource,
                                                          "nonce": token})
         context_data = http.json(context_res, schema=self.context_schema)
@@ -68,13 +70,15 @@ class TVPlayer(Plugin):
             service=service,
             id=resource,
             validate=context_data["validate"],
+            token=context_data["token"],
             platform=context_data["platform"]["key"]))
 
         return http.json(res, schema=self.stream_schema)
 
     def _get_streams(self):
         if self.get_option("email") and self.get_option("password"):
-            self.authenticate(self.get_option("email"), self.get_option("password"))
+            if not self.authenticate(self.get_option("email"), self.get_option("password")):
+                self.logger.warning("Failed to login as {0}".format(self.get_option("email")))
 
         # find the list of channels from the html in the page
         self.url = self.url.replace("https", "http")  # https redirects to http
diff --git a/src/streamlink_cli/argparser.py b/src/streamlink_cli/argparser.py
index b9ffd778..ef0d8815 100644
--- a/src/streamlink_cli/argparser.py
+++ b/src/streamlink_cli/argparser.py
@@ -306,18 +306,33 @@ player.add_argument(
     metavar="COMMAND",
     default=find_default_player(),
     help="""
-    Player to feed stream data to. This is a shell-like syntax to
-    support passing options to the player. For example:
+    Player to feed stream data to. By default, VLC will be used
+    if it can be found in its default location.
 
-      "vlc --file-caching=5000"
+    This is a shell-like syntax to support using a specific player:
+
+      "streamlink --player=vlc <url> <quality>"
+
+    Absolute or relative paths can also be passed via this option
+    in the event the player's executable can not be resolved:
+
+      streamlink --player=/path/to/vlc <url> <quality>
+
+      streamlink --player=./vlc-player/vlc <url> <quality>
 
     To use a player that is located in a path with spaces you must
-    quote the path:
+    quote the parameter or its value:
 
-      "'/path/with spaces/vlc' --file-caching=5000"
+      streamlink "--player=/path/with spaces/vlc" <url> <quality>
 
-    By default VLC will be used if it can be found in its default
-    location.
+      streamlink --player "C:\path\with spaces\mpc-hc64.exe" <url> <quality
+    
+    Options may also be passed to the player. For example:
+
+      streamlink --player "vlc --file-caching=5000" <url> <quality>
+
+    As an alternative to this, see the --player-args parameter,
+    which does not log the customer player arguments.
     """
 )
 player.add_argument(
@@ -1231,6 +1246,20 @@ plugin.add_argument(
     A pc-yourfreetv.com account password to use with --pcyourfreetv-username.
     """
 )
+plugin.add_argument(
+    "--bbciplayer-username",
+    metavar="USERNAME",
+    help="""
+    The username used to register with bbc.co.uk.
+    """
+)
+plugin.add_argument(
+    "--bbciplayer-password",
+    metavar="PASSWORD",
+    help="""
+    A bbc.co.uk account password to use with --bbciplayer-username.
+    """
+)
 
 # Deprecated options
 stream.add_argument(
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index 057d1e50..e111b530 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -930,6 +930,17 @@ def setup_plugin_options():
     if pcyourfreetv_password:
         streamlink.set_plugin_option("pcyourfreetv", "password", pcyourfreetv_password)
 
+    if args.bbciplayer_username:
+        streamlink.set_plugin_option("bbciplayer", "username", args.bbciplayer_username)
+
+    if args.bbciplayer_username and not args.bbciplayer_password:
+        bbciplayer_password = console.askpass("Enter bbc.co.uk account password: ")
+    else:
+        bbciplayer_password = args.bbciplayer_password
+
+    if bbciplayer_password:
+        streamlink.set_plugin_option("bbciplayer", "password", bbciplayer_password)
+
     # Deprecated options
     if args.jtv_legacy_names:
         console.logger.warning("The option --jtv/twitch-legacy-names is "
diff --git a/src/streamlink_cli/utils/http_server.py b/src/streamlink_cli/utils/http_server.py
index 42dedc94..14aaf291 100644
--- a/src/streamlink_cli/utils/http_server.py
+++ b/src/streamlink_cli/utils/http_server.py
@@ -114,6 +114,6 @@ class HTTPServer(object):
         if not client_only:
             try:
                 self.socket.shutdown(2)
-            except OSError:
+            except (OSError, socket.error):
                 pass
             self.socket.close()


Test patch:
diff --git a/tests/test_plugin_euronews.py b/tests/test_plugin_euronews.py
new file mode 100644
index 00000000..54fa2257
--- /dev/null
+++ b/tests/test_plugin_euronews.py
@@ -0,0 +1,27 @@
+import unittest
+
+from streamlink.plugins.euronews import Euronews
+
+
+class TestPluginEuronews(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(Euronews.can_handle_url("http://www.euronews.com/live"))
+        self.assertTrue(Euronews.can_handle_url("http://fr.euronews.com/live"))
+        self.assertTrue(Euronews.can_handle_url("http://de.euronews.com/live"))
+        self.assertTrue(Euronews.can_handle_url("http://it.euronews.com/live"))
+        self.assertTrue(Euronews.can_handle_url("http://es.euronews.com/live"))
+        self.assertTrue(Euronews.can_handle_url("http://pt.euronews.com/live"))
+        self.assertTrue(Euronews.can_handle_url("http://ru.euronews.com/live"))
+        self.assertTrue(Euronews.can_handle_url("http://ua.euronews.com/live"))
+        self.assertTrue(Euronews.can_handle_url("http://tr.euronews.com/live"))
+        self.assertTrue(Euronews.can_handle_url("http://gr.euronews.com/live"))
+        self.assertTrue(Euronews.can_handle_url("http://hu.euronews.com/live"))
+        self.assertTrue(Euronews.can_handle_url("http://fa.euronews.com/live"))
+        self.assertTrue(Euronews.can_handle_url("http://arabic.euronews.com/live"))
+        self.assertTrue(Euronews.can_handle_url("http://www.euronews.com/2017/05/10/peugeot-expects-more-opel-losses-this-year"))
+        self.assertTrue(Euronews.can_handle_url("http://fr.euronews.com/2017/05/10/l-ag-de-psa-approuve-le-rachat-d-opel"))
+
+        # shouldn't match
+        self.assertFalse(Euronews.can_handle_url("http://www.tvcatchup.com/"))
+        self.assertFalse(Euronews.can_handle_url("http://www.youtube.com/"))
diff --git a/tests/test_plugin_pluzz.py b/tests/test_plugin_pluzz.py
index 70680220..cac35e71 100644
--- a/tests/test_plugin_pluzz.py
+++ b/tests/test_plugin_pluzz.py
@@ -6,21 +6,22 @@ from streamlink.plugins.pluzz import Pluzz
 class TestPluginPluzz(unittest.TestCase):
     def test_can_handle_url(self):
         # should match
-        self.assertTrue(Pluzz.can_handle_url("http://pluzz.francetv.fr/france2"))
-        self.assertTrue(Pluzz.can_handle_url("http://pluzz.francetv.fr/france3"))
-        self.assertTrue(Pluzz.can_handle_url("http://pluzz.francetv.fr/france3-franche-comte"))
-        self.assertTrue(Pluzz.can_handle_url("http://pluzz.francetv.fr/france4"))
-        self.assertTrue(Pluzz.can_handle_url("http://pluzz.francetv.fr/france5"))
-        self.assertTrue(Pluzz.can_handle_url("http://pluzz.francetv.fr/franceo"))
-        self.assertTrue(Pluzz.can_handle_url("http://pluzz.francetv.fr/franceinfo"))
-        self.assertTrue(Pluzz.can_handle_url("http://pluzz.francetv.fr/videos/jt20h.html"))
-        self.assertTrue(Pluzz.can_handle_url("http://pluzz.francetv.fr/videos/jt_1213_franche_comte.html"))
+        self.assertTrue(Pluzz.can_handle_url("https://www.france.tv/france-2/direct.html"))
+        self.assertTrue(Pluzz.can_handle_url("https://www.france.tv/france-3/direct.html"))
+        self.assertTrue(Pluzz.can_handle_url("https://www.france.tv/france-3-franche-comte/direct.html"))
+        self.assertTrue(Pluzz.can_handle_url("https://www.france.tv/france-4/direct.html"))
+        self.assertTrue(Pluzz.can_handle_url("https://www.france.tv/france-5/direct.html"))
+        self.assertTrue(Pluzz.can_handle_url("https://www.france.tv/france-o/direct.html"))
+        self.assertTrue(Pluzz.can_handle_url("https://www.france.tv/franceinfo/direct.html"))
+        self.assertTrue(Pluzz.can_handle_url("https://www.france.tv/france-2/journal-20h00/141003-edition-du-lundi-8-mai-2017.html"))
+        self.assertTrue(Pluzz.can_handle_url("https://www.france.tv/france-o/underground/saison-1/132187-underground.html"))
         self.assertTrue(Pluzz.can_handle_url("http://www.ludo.fr/heros/the-batman"))
         self.assertTrue(Pluzz.can_handle_url("http://www.ludo.fr/heros/il-etait-une-fois-la-vie"))
         self.assertTrue(Pluzz.can_handle_url("http://www.zouzous.fr/heros/oui-oui"))
         self.assertTrue(Pluzz.can_handle_url("http://www.zouzous.fr/heros/marsupilami-1"))
 
         # shouldn't match
+        self.assertFalse(Pluzz.can_handle_url("http://www.france.tv/"))
         self.assertFalse(Pluzz.can_handle_url("http://pluzz.francetv.fr/"))
         self.assertFalse(Pluzz.can_handle_url("http://www.ludo.fr/"))
         self.assertFalse(Pluzz.can_handle_url("http://www.ludo.fr/jeux"))