A new user interface for you! Read more...

File livestreamer-git-ab80dbd.patch of Package livestreamer

diff --git a/.travis.yml b/.travis.yml
index 0026fac..810cf19 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,19 +1,25 @@
 language: python
 sudo: false
 python:
-- 2.6
-- 2.7
-- 3.3
+  - 2.6
+  - 2.7
+  - 3.3
 script: python setup.py test
+
+addons:
+  apt:
+    packages:
+      - nsis
+
 install:
-- travis_retry pip install requests argparse --use-mirrors
-- travis_retry gem install --version 0.8.9 faraday
-- travis_retry gem install travis-artifacts
+  - travis_retry pip install requests argparse --use-mirrors
+  - curl -sL https://raw.githubusercontent.com/travis-ci/artifacts/master/install | bash
+
 after_success:
-- sh .travis/build-win32.sh
+  - bash .travis/build-win32.sh
+
 env:
   global:
-  - ARTIFACTS_AWS_REGION=us-east-1
-  - ARTIFACTS_S3_BUCKET=livestreamer-builds
-  - secure: hCNU8Oaw944R+pbHiPFputtgXgC6B02m2UM+7flIatoTnUYTZbwGSkVCxF12s5vSuI4Zg01/54qSuCe4sNxloPmqQ50oxKaxFkq8LlidOjbGw6hq7u+qDvD5+E2PfRwcfDNnso9NbuRSP8TMk9/WRXeBD/SrbgnbZ/1o/LQEKBQ=
-  - secure: YUltQXSDTBIP0iDbU9NiLv8FP3Xr/TT/JYuuyw8JfRzOcjM0PP+qKXyR9bc/gDpSj2uoLDaKfTQvPGFCYdrd+UuTZmlvV56VYQ2/tG0XXifvINJOyESWBLvQfhySuvC/IDpMhz0yEKI8btKHurlvIdvv/9HKlxVL/EIU3C2apiQ=
+    - ARTIFACTS_S3_REGION=eu-west-1
+    - ARTIFACTS_S3_BUCKET=livestreamer-builds
+
diff --git a/.travis/build-win32.sh b/.travis/build-win32.sh
index fb0077c..56688cb 100755
--- a/.travis/build-win32.sh
+++ b/.travis/build-win32.sh
@@ -1,5 +1,9 @@
 #!/bin/sh
 
+if [ $TRAVIS_SECURE_ENV_VARS != "true" ]; then
+	exit 0
+fi
+
 if [ $TRAVIS_PYTHON_VERSION != "2.7" ]; then
 	exit 0
 fi
@@ -10,8 +14,12 @@ git fetch --unshallow
 
 sh win32/build-with-bootstrap.sh
 cd dist/
-cp *zip livestreamer-latest-win32.zip
 
-for zip in *zip; do
-	travis-artifacts upload --path "$zip" --target-path ""
+if [ $TRAVIS_BRANCH = "develop" ]; then
+	cp *zip livestreamer-latest-win32.zip
+	cp *exe livestreamer-latest-win32-setup.exe
+fi
+
+for file in ./{*.exe,*.zip}; do
+	~/bin/artifacts upload --target-paths "/" "$file"
 done
diff --git a/docs/issues.rst b/docs/issues.rst
index 4530df7..317160d 100644
--- a/docs/issues.rst
+++ b/docs/issues.rst
@@ -11,9 +11,9 @@ Streams are buffering/lagging
 Enable caching in your player
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-By default most players do not cache the data they receieve from Livestreamer.
+By default most players do not cache the data they receive from Livestreamer.
 Caching can reduce the amount of buffering you run into because the player will 
-have some breathing room between receving the data and playing it.
+have some breathing room between receiving the data and playing it.
 
 ============= ======================== ======================================
 Player        Parameter                Note
diff --git a/docs/players.rst b/docs/players.rst
index 381bea1..e747500 100644
--- a/docs/players.rst
+++ b/docs/players.rst
@@ -48,7 +48,7 @@ Name                                                  Stdin Pipe Named Pipe HTTP
 .. [3] Some versions of VLC might be unable to use the stdin pipe and
        prints the error message::
 
-            VLC is unable to open the MRL 'fd://0'
+       VLC is unable to open the MRL 'fd://0'
 
        Use one of the other transport methods instead to work around this.
 
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
old mode 100644
new mode 100755
index 61d72f8..a186d2e
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -16,12 +16,15 @@ afreeca             afreecatv.com        Yes   No
 afreecatv           afreeca.tv           Yes   No
 aftonbladet         aftonbladet.se       Yes   Yes
 alieztv             aliez.tv             Yes   Yes
+antenna             antenna.gr           --    Yes
 ard_live            live.daserste.de     Yes   --    Streams may be geo-restricted to Germany.
 ard_mediathek       ardmediathek.de      Yes   Yes   Streams may be geo-restricted to Germany.
 artetv              arte.tv              Yes   Yes
 azubutv             azubu.tv             Yes   No
+beam                beam.pro             Yes   No
 beattv              be-at.tv             Yes   Yes   Playlist not implemented yet.
 bambuser            bambuser.com         Yes   Yes
+bliptv              blip.tv              --    Yes
 chaturbate          chaturbate.com       Yes   No
 connectcast         connectcast.tv       Yes   Yes
 crunchyroll         crunchyroll.com      --    Yes
@@ -34,6 +37,7 @@ douyutv             douyutv.com          Yes   --
 dmcloud             api.dmcloud.net      Yes   --
 drdk                dr.dk                Yes   Yes   Streams may be geo-restricted to Denmark.
 euronews            euronews.com         Yes   No
+expressen           expressen.se         Yes   Yes
 filmon              filmon.com           Yes   Yes   Only SD quality streams.
 filmon_us           filmon.us            Yes   Yes
 furstream           furstre.am           Yes   No
@@ -45,8 +49,10 @@ itvplayer           itv.com/itvplayer    Yes   Yes   Streams may be geo-restrict
 letontv             leton.tv             Yes   --
 livestation         livestation.com      Yes   --
 livestream          new.livestream.com   Yes   --
-media_ccc_de        - media.ccc.de       Yes   Yes   Only mp4 and HLS ar are supported.
+livecoding          livecoding.tv        Yes   --
+media_ccc_de        - media.ccc.de       Yes   Yes   Only mp4 and HLS are supported.
                     - streaming... [4]_
+meerkat             meerkatapp.co        Yes   --
 mips                mips.tv              Yes   --    Requires rtmpdump with K-S-V patches.
 mlgtv               mlg.tv               Yes   --
 nhkworld            nhk.or.jp/nhkworld   Yes   No
@@ -55,9 +61,10 @@ npo                 npo.nl               Yes   Yes   Streams may be geo-restrict
 nrk                 - tv.nrk.no          Yes   Yes   Streams may be geo-restricted to Norway.
                     - radio.nrk.no
 oldlivestream       original.liv... [3]_ Yes   No    Only mobile streams are supported.
-periscope           periscope.tv         Yes   --
+periscope           periscope.tv         Yes   Yes   Replay/VOD is supported.
 picarto             picarto.tv           Yes   --
 rtve                rtve.es              Yes   No
+ruv                 ruv.is               Yes   Yes   Streams may be geo-restricted to Iceland.
 sbsdiscovery        - kanal5play.se      --    Yes
                     - kanal9play.se
                     - kanal11play.se
@@ -67,6 +74,7 @@ ssh101              ssh101.com           Yes   No
 streamingvi... [1]_ streamingvid... [2]_ Yes   --    RTMP streams requires rtmpdump with
                                                      K-S-V patches.
 streamlive          streamlive.to        Yes   --
+streamupcom         streamup.com         Yes   --
 svtplay             - svtplay.se         Yes   Yes   Streams may be geo-restricted to Sweden.
                     - svtflow.se
                     - oppetarkiv.se
diff --git a/src/livestreamer/plugins/afreeca.py b/src/livestreamer/plugins/afreeca.py
index ebaaf3b..ec748f0 100644
--- a/src/livestreamer/plugins/afreeca.py
+++ b/src/livestreamer/plugins/afreeca.py
@@ -10,6 +10,7 @@ STREAM_INFO_URLS = {
     "rtmp": "http://sessionmanager01.afreeca.tv:6060/broad_stream_assign.html",
     "hls": "http://resourcemanager.afreeca.tv:9090/broad_stream_assign.html"
 }
+HLS_KEY_URL = "http://api.m.afreeca.com/broad/a/watch"
 
 CHANNEL_RESULT_ERROR = 0
 CHANNEL_RESULT_OK = 1
@@ -55,18 +56,37 @@ class AfreecaTV(Plugin):
 
         return http.json(res, schema=_channel_schema)
 
+    def _get_hls_key(self, broadcast, username):
+        headers = {
+            "Referer": self.url
+        }
+        data = {
+            "bj_id": username,
+            "broad_no": broadcast
+        }
+        res = http.post(HLS_KEY_URL, data=data, headers=headers)
+
+        return http.json(res)
+
     def _get_stream_info(self, broadcast, type):
         params = {
             "return_type": "gs_cdn",
+            "use_cors": "true",
+            "cors_origin_url": "m.afreeca.com",
+            "broad_no": "{broadcast}-mobile-hd-{type}".format(**locals()),
             "broad_key": "{broadcast}-flash-hd-{type}".format(**locals())
         }
         res = http.get(STREAM_INFO_URLS[type], params=params)
         return http.json(res, schema=_stream_schema)
 
-    def _get_hls_stream(self, broadcast):
+    def _get_hls_stream(self, broadcast, username):
+        keyjson = self._get_hls_key(broadcast, username)
+        if keyjson["result"] != CHANNEL_RESULT_OK:
+            return
+        key = keyjson["data"]["hls_authentication_key"]
         info = self._get_stream_info(broadcast, "hls")
         if "view_url" in info:
-            return HLSStream(self.session, info["view_url"])
+            return HLSStream(self.session, info["view_url"], params=dict(aid=key))
 
     def _get_rtmp_stream(self, broadcast):
         info = self._get_stream_info(broadcast, "rtmp")
@@ -90,7 +110,7 @@ class AfreecaTV(Plugin):
         if flash_stream:
             yield "live", flash_stream
 
-        mobile_stream = self._get_hls_stream(broadcast)
+        mobile_stream = self._get_hls_stream(broadcast, username)
         if mobile_stream:
             yield "live", mobile_stream
 
diff --git a/src/livestreamer/plugins/afreecatv.py b/src/livestreamer/plugins/afreecatv.py
index 135d36b..b84675c 100644
--- a/src/livestreamer/plugins/afreecatv.py
+++ b/src/livestreamer/plugins/afreecatv.py
@@ -7,8 +7,10 @@ from livestreamer.stream import RTMPStream
 
 
 VIEW_LIVE_API_URL = "http://api.afreeca.tv/live/view_live.php"
+VIEW_LIVE_API_URL_TW = "http://api.afreecatv.com.tw/live/view_live.php"
 
-_url_re = re.compile("http(s)?://(\w+\.)?afreeca.tv/(?P<channel>[\w\-_]+)")
+_url_re = re.compile("http(s)?://(\w+\.)?(afreecatv.com.tw|afreeca.tv)/(?P<channel>[\w\-_]+)")
+_url_re_tw = re.compile("http(s)?://(\w+\.)?(afreecatv.com.tw)/(?P<channel>[\w\-_]+)")
 _flashvars_re = re.compile('<param name="flashvars" value="([^"]+)" />')
 
 _flashvars_schema = validate.Schema(
@@ -27,7 +29,6 @@ _view_live_schema = validate.Schema(
     {
         "channel": {
             "strm": [{
-                "brt": validate.text,
                 "bps": validate.text,
                 "purl": validate.url(scheme="rtmp")
             }]
@@ -57,7 +58,12 @@ class AfreecaTV(Plugin):
             "adok": "",
             "bno": ""
         }
-        res = http.get(VIEW_LIVE_API_URL, params=params)
+        
+        if re.search(_url_re_tw, self.url):
+            res = http.get(VIEW_LIVE_API_URL_TW, params=params)
+        else:
+            res = http.get(VIEW_LIVE_API_URL, params=params)
+            
         streams = http.json(res, schema=_view_live_schema)
 
         for stream in streams:
diff --git a/src/livestreamer/plugins/antenna.py b/src/livestreamer/plugins/antenna.py
new file mode 100644
index 0000000..bca7470
--- /dev/null
+++ b/src/livestreamer/plugins/antenna.py
@@ -0,0 +1,49 @@
+import re
+import json
+
+from livestreamer.plugin import Plugin
+from livestreamer.plugin.api import http, validate
+from livestreamer.stream import HDSStream
+
+_url_re = re.compile("(http(s)?://(\w+\.)?antenna.gr)/webtv/watch\?cid=.+")
+_playlist_re = re.compile("playlist:\s*\"(/templates/data/jplayer\?cid=[^\"]+)")
+_manifest_re = re.compile("jwplayer:source\s+file=\"([^\"]+)\"")
+_swf_re = re.compile("<jwplayer:provider>(http[^<]+)</jwplayer:provider>")
+
+class Antenna(Plugin):
+    @classmethod
+    def can_handle_url(self, url):
+        return _url_re.match(url)
+
+    def _get_streams(self):
+
+        # Discover root
+        match = _url_re.search(self.url)
+        root = match.group(1)
+
+        # Download main URL
+        res = http.get(self.url)
+
+        # Find playlist
+        match = _playlist_re.search(res.text)
+        playlist_url = root + match.group(1) + "d"
+
+        # Download playlist
+        res = http.get(playlist_url)
+
+        # Find manifest
+        match = _manifest_re.search(res.text)
+        manifest_url = match.group(1)
+
+        # Find SWF
+        match = _swf_re.search(res.text)
+        swf_url = match.group(1);
+
+        streams = {}
+        streams.update(
+            HDSStream.parse_manifest(self.session, manifest_url, pvswf=swf_url)
+        )
+        
+        return streams
+
+__plugin__ = Antenna
diff --git a/src/livestreamer/plugins/artetv.py b/src/livestreamer/plugins/artetv.py
index 684993c..b8d8a09 100644
--- a/src/livestreamer/plugins/artetv.py
+++ b/src/livestreamer/plugins/artetv.py
@@ -12,14 +12,14 @@ from livestreamer.stream import HLSStream, HTTPStream, RTMPStream
 SWF_URL = "http://www.arte.tv/player/v2/jwplayer6/mediaplayer.6.6.swf"
 
 _url_re = re.compile("http(s)?://(\w+\.)?arte.tv/")
-_json_re = re.compile("arte_vp_(?:live-)?url='([^']+)'")
+_json_re = re.compile("arte_vp_(?:live-)?url=(['\"])(.+?)\\1")
 
 _schema = validate.Schema(
     validate.transform(_json_re.search),
     validate.any(
         None,
         validate.all(
-            validate.get(1),
+            validate.get(2),
             validate.url(scheme="http")
         )
     )
diff --git a/src/livestreamer/plugins/azubutv.py b/src/livestreamer/plugins/azubutv.py
index fd52cde..572881f 100644
--- a/src/livestreamer/plugins/azubutv.py
+++ b/src/livestreamer/plugins/azubutv.py
@@ -18,7 +18,8 @@ HTTP_HEADERS = {
                    "(KHTML, like Gecko) Chrome/36.0.1944.9 Safari/537.36")
 }
 
-_url_re = re.compile("http(s)?://(\w+\.)?azubu.tv/[^/]+")
+_url_re = re.compile("http(s)?://(\w+\.)?azubu.tv/(?P<domain>\w+)")
+CHANNEL_INFO_URL = "http://api.azubu.tv/public/channel/%s/player"
 
 _viewerexp_schema = validate.Schema(
     validate.attr({
@@ -122,8 +123,10 @@ class AzubuTV(Plugin):
         return AMFPacket.deserialize(BytesIO(res.content))
 
     def _get_player_params(self, retries=5):
+        match = _url_re.match(self.url);
+        domain = match.group('domain');
         try:
-            res = http.get(self.url, headers=HTTP_HEADERS)
+            res = http.get(CHANNEL_INFO_URL % str(domain))
         except PluginError as err:
             # The server sometimes gives us 404 for no reason
             if "404" in str(err) and retries:
@@ -131,41 +134,21 @@ class AzubuTV(Plugin):
                 return self._get_player_params(retries - 1)
             else:
                 raise
+        channel_info = http.json(res)
+        channel_info = channel_info['data']
 
-        match = re.search("<param name=\"playerKey\" value=\"(.+)\" />", res.text)
-        if not match:
-            # The HTML returned sometimes doesn't contain the parameters
-            if not retries:
-                raise PluginError("Missing key 'playerKey' in player params")
-            else:
-                sleep(1)
-                return self._get_player_params(retries - 1)
+        key = channel_info['player_key'];
 
-        key = match.group(1)
-        match = re.search("AZUBU.setVar\(\"firstVideoRefId\", \"(.+)\"\);", res.text)
-        if not match:
-            # The HTML returned sometimes doesn't contain the parameters
-            if not retries:
-                raise PluginError("Unable to find video reference")
-            else:
-                sleep(1)
-                return self._get_player_params(retries - 1)
+        is_live = channel_info['is_live'];
 
-        video_player = "ref:" + match.group(1)
-        match = re.search("<param name=\"playerID\" value=\"(\d+)\" />", res.text)
-        if not match:
-            # The HTML returned sometimes doesn't contain the parameters
-            if not retries:
-                raise PluginError("Missing key 'playerID' in player params")
-            else:
-                sleep(1)
-                return self._get_player_params(retries - 1)
+        stream_video = channel_info['stream_video']
+        if stream_video:
+            video_player = "ref:" + stream_video['reference_id']
+        else:
+            is_live = False
+            video_player = None
 
-        player_id = match.group(1)
-        match = re.search("<!-- live on -->", res.text)
-        if not match:
-            match = re.search("<div id=\"channel_live\">", res.text)
-        is_live = not not match
+        player_id = channel_info['player_id']
 
         return key, video_player, player_id, is_live
 
diff --git a/src/livestreamer/plugins/beam.py b/src/livestreamer/plugins/beam.py
new file mode 100644
index 0000000..b396607
--- /dev/null
+++ b/src/livestreamer/plugins/beam.py
@@ -0,0 +1,64 @@
+import re
+
+from livestreamer.plugin import Plugin
+from livestreamer.plugin.api import http, validate
+from livestreamer.stream import RTMPStream
+
+_url_re = re.compile("http(s)?://(\w+.)?beam.pro/(?P<channel>[^/]+)")
+
+CHANNEL_INFO = "https://beam.pro/api/v1/channels/{0}"
+CHANNEL_MANIFEST = "https://beam.pro/api/v1/channels/{0}/manifest.smil"
+
+_assets_schema = validate.Schema(
+    validate.union({
+        "base": validate.all(
+            validate.xml_find("./head/meta"),
+            validate.get("base"),
+            validate.url(scheme="rtmp")
+        ),
+        "videos": validate.all(
+            validate.xml_findall(".//video"),
+            [
+                validate.union({
+                    "src": validate.all(
+                        validate.get("src"),
+                        validate.text
+                    ),
+                    "height": validate.all(
+                        validate.get("height"),
+                        validate.text,
+                        validate.transform(int)
+                    )
+                })
+            ]
+        )
+    })
+)
+
+class Beam(Plugin):
+    @classmethod
+    def can_handle_url(self, url):
+        return _url_re.match(url)
+
+    def _get_streams(self):
+        match = _url_re.match(self.url)
+        channel = match.group("channel")
+        res = http.get(CHANNEL_INFO.format(channel))
+        channel_info = http.json(res)
+
+        if not channel_info["online"]:
+            return
+
+        res = http.get(CHANNEL_MANIFEST.format(channel_info["id"]))
+        assets = http.xml(res, schema=_assets_schema)
+        streams = {}
+        for video in assets["videos"]:
+            name = "{0}p".format(video["height"])
+            stream = RTMPStream(self.session,{
+                "rtmp"     : "{0}/{1}".format(assets["base"], video["src"])
+            })
+            streams[name] = stream
+
+        return streams
+
+__plugin__ = Beam
diff --git a/src/livestreamer/plugins/bliptv.py b/src/livestreamer/plugins/bliptv.py
new file mode 100644
index 0000000..d8c3864
--- /dev/null
+++ b/src/livestreamer/plugins/bliptv.py
@@ -0,0 +1,73 @@
+import re
+
+from livestreamer.plugin import Plugin, PluginError
+from livestreamer.plugin.api import http
+from livestreamer.stream import HTTPStream
+
+_url_re = re.compile("(http(s)?://)?blip.tv/.*-(?P<videoid>\d+)")
+VIDEO_GET_URL = 'http://player.blip.tv/file/get/{0}'
+SINGLE_VIDEO_URL = re.compile('.*\.((mp4)|(mov)|(m4v)|(flv))')
+
+QUALITY_WEIGHTS = {
+    "ultra": 1080,
+    "high": 720,
+    "medium": 480,
+    "low": 240,
+}
+
+QUALITY_WEIGHTS_ULTRA = re.compile('ultra+_(?P<level>\d+)')
+
+
+def get_quality_dict(quality_list):
+    quality_list.sort()
+    quality_dict = {}
+    i = 0
+    for i, bitrate in enumerate(quality_list):
+        if i == 0:
+            quality_dict['%i' % bitrate] = 'low'
+        elif i == 1:
+            quality_dict['%i' % bitrate] = 'medium'
+        elif i == 2:
+            quality_dict['%i' % bitrate] = 'high'
+        elif i == 3:
+            quality_dict['%i' % bitrate] = 'ultra'
+        else:
+            quality_dict['%i' % bitrate] = 'ultra+_%i' % (i-3)
+    return quality_dict
+
+
+class bliptv(Plugin):
+    @classmethod
+    def can_handle_url(cls, url):
+        return _url_re.match(url)
+
+    @classmethod
+    def stream_weight(cls, key):
+        match_ultra = QUALITY_WEIGHTS_ULTRA.match(key)
+        if match_ultra:
+            ultra_level = int(match_ultra.group('level'))
+            return 1080 * (ultra_level + 1), "bliptv"
+        weight = QUALITY_WEIGHTS.get(key)
+        if weight:
+            return weight, "bliptv"
+        return Plugin.stream_weight(key)
+
+    def _get_streams(self):
+        match = _url_re.match(self.url)
+        videoid = match.group('videoid')
+        get_return = http.get(VIDEO_GET_URL.format(videoid))
+        json_decode = http.json(get_return)
+        streams = {}
+        quality_list = []
+        for stream in json_decode:
+            if SINGLE_VIDEO_URL.match(stream['direct_url']):
+                quality_list.append(int(stream['video_bitrate']))
+        if len(quality_list) == 0:
+            return
+        quality_dict = get_quality_dict(quality_list)
+        for stream in json_decode:
+            if SINGLE_VIDEO_URL.match(stream['direct_url']):
+                streams[quality_dict[stream['video_bitrate']]] = HTTPStream(self.session, stream['direct_url'])
+        return streams
+
+__plugin__ = bliptv
diff --git a/src/livestreamer/plugins/crunchyroll.py b/src/livestreamer/plugins/crunchyroll.py
index 56d021d..d9c578d 100644
--- a/src/livestreamer/plugins/crunchyroll.py
+++ b/src/livestreamer/plugins/crunchyroll.py
@@ -8,15 +8,15 @@ from livestreamer.plugin.api import http, validate
 from livestreamer.stream import HLSStream
 
 API_URL = "https://api.crunchyroll.com/{0}.0.json"
+API_DEFAULT_LOCALE = "en_US"
+API_USER_AGENT = "Mozilla/5.0 (iPhone; iPhone OS 8.3.0; {})"
 API_HEADERS = {
-    "User-Agent": "Mozilla/5.0 (iPhone; iPhone OS 8.3.0; en_US)",
     "Host": "api.crunchyroll.com",
     "Accept-Encoding": "gzip, deflate",
     "Accept": "*/*",
     "Content-Type": "application/x-www-form-urlencoded"
 }
 API_VERSION = "2313.8"
-API_LOCALE = "enUS"
 API_ACCESS_TOKEN = "QWjz212GspMHH9h"
 API_DEVICE_TYPE = "com.crunchyroll.iphone"
 STREAM_WEIGHTS = {
@@ -98,13 +98,14 @@ class CrunchyrollAPIError(Exception):
 
 
 class CrunchyrollAPI(object):
-    def __init__(self, session_id=None, auth=None):
+    def __init__(self, session_id=None, auth=None, locale=API_DEFAULT_LOCALE):
         """Abstract the API to access to Crunchyroll data.
 
         Can take saved credentials to use on it's calls to the API.
         """
         self.session_id = session_id
         self.auth = auth
+        self.locale = locale
 
     def _api_call(self, entrypoint, params, schema=None):
         """Makes a call against the api.
@@ -119,14 +120,18 @@ class CrunchyrollAPI(object):
         params = dict(params)
         params.update({
             "version": API_VERSION,
-            "locale": API_LOCALE,
+            "locale": self.locale.replace('_', ''),
         })
 
         if self.session_id:
             params["session_id"] = self.session_id
 
+        # Headers
+        headers = dict(API_HEADERS)
+        headers['User-Agent'] = API_USER_AGENT.format(self.locale)
+
         # The certificate used by Crunchyroll cannot be verified in some environments.
-        res = http.get(url, params=params, headers=API_HEADERS, verify=False)
+        res = http.get(url, params=params, headers=headers, verify=False)
         json_res = http.json(res, schema=_api_schema)
 
         if json_res["error"]:
@@ -193,6 +198,7 @@ class Crunchyroll(Plugin):
         "username": None,
         "password": None,
         "purge_credentials": None,
+        "locale": API_DEFAULT_LOCALE
     })
 
     @classmethod
@@ -250,8 +256,9 @@ class Crunchyroll(Plugin):
 
         current_time = datetime.datetime.utcnow()
         device_id = self._get_device_id()
+        locale = self.options.get("locale")
         api = CrunchyrollAPI(
-            self.cache.get("session_id"), self.cache.get("auth")
+            self.cache.get("session_id"), self.cache.get("auth"), locale
         )
 
         self.logger.debug("Creating session")
@@ -260,7 +267,7 @@ class Crunchyroll(Plugin):
         except CrunchyrollAPIError as err:
             if err.code == "bad_session":
                 self.logger.debug("Current session has expired, creating a new one")
-                api = CrunchyrollAPI()
+                api = CrunchyrollAPI(locale=locale)
                 api.session_id = api.start_session(device_id, schema=_session_schema)
             else:
                 raise err
diff --git a/src/livestreamer/plugins/dailymotion.py b/src/livestreamer/plugins/dailymotion.py
index 6eb7918..2e4c62a 100644
--- a/src/livestreamer/plugins/dailymotion.py
+++ b/src/livestreamer/plugins/dailymotion.py
@@ -5,7 +5,7 @@ from functools import reduce
 from livestreamer.compat import urlparse, range
 from livestreamer.plugin import Plugin
 from livestreamer.plugin.api import http, validate
-from livestreamer.stream import HDSStream, HTTPStream, RTMPStream
+from livestreamer.stream import HDSStream, HLSStream, HTTPStream, RTMPStream
 from livestreamer.stream.playlist import FLVPlaylist
 
 COOKIES = {
@@ -173,6 +173,12 @@ class DailyMotion(Plugin):
             # TODO: Replace with "yield from" when dropping Python 2.
             for __ in streams.items():
                 yield __
+        elif res.headers.get("Content-Type") == "application/vnd.apple.mpegurl":
+            streams = HLSStream.parse_variant_playlist(self.session, res.url)
+
+            # TODO: Replace with "yield from" when dropping Python 2.
+            for __ in streams.items():
+                yield __
         else:
             manifest = http.json(res, schema=_vod_manifest_schema)
             for params in manifest["alternates"]:
diff --git a/src/livestreamer/plugins/douyutv.py b/src/livestreamer/plugins/douyutv.py
index 5ebccec..d0b0311 100644
--- a/src/livestreamer/plugins/douyutv.py
+++ b/src/livestreamer/plugins/douyutv.py
@@ -1,10 +1,14 @@
+import hashlib
 import re
+import time
 
 from livestreamer.plugin import Plugin
 from livestreamer.plugin.api import http, validate
-from livestreamer.stream import HTTPStream
+from livestreamer.stream import (
+    HTTPStream, HLSStream
+)
 
-API_URL = "http://www.douyutv.com/api/client/room/{0}"
+API_URL = "http://www.douyutv.com/api/v1/room/{0}?aid=android&client_sys=android&time={1}&auth={2}"
 SHOW_STATUS_ONLINE = 1
 SHOW_STATUS_OFFLINE = 2
 STREAM_WEIGHTS = {
@@ -26,6 +30,7 @@ _room_schema = validate.Schema(
             ),
             "rtmp_url": validate.text,
             "rtmp_live": validate.text,
+            "hls_url": validate.text,
             "rtmp_multi_bitrate": validate.all(
                 validate.any([], {
                     validate.text: validate.text
@@ -54,7 +59,10 @@ class Douyutv(Plugin):
         match = _url_re.match(self.url)
         channel = match.group("channel")
 
-        res = http.get(API_URL.format(channel))
+        ts = int(time.time())
+        sign = hashlib.md5(("room/{0}?aid=android&client_sys=android&time={1}".format(channel, ts) + "1231").encode("ascii")).hexdigest()
+
+        res = http.get(API_URL.format(channel, ts, sign))
         room = http.json(res, schema=_room_schema)
         if not room:
             return
@@ -62,6 +70,10 @@ class Douyutv(Plugin):
         if room["show_status"] != SHOW_STATUS_ONLINE:
             return
 
+        hls_url = "{room[hls_url]}?wsiphost=local".format(room=room)
+        hls_stream = HLSStream(self.session, hls_url)
+        yield "hls", hls_stream
+
         url = "{room[rtmp_url]}/{room[rtmp_live]}".format(room=room)
         stream = HTTPStream(self.session, url)
         yield "source", stream
diff --git a/src/livestreamer/plugins/expressen.py b/src/livestreamer/plugins/expressen.py
new file mode 100644
index 0000000..29391ab
--- /dev/null
+++ b/src/livestreamer/plugins/expressen.py
@@ -0,0 +1,70 @@
+import re
+from livestreamer.compat import urlparse
+from livestreamer.plugin import Plugin
+from livestreamer.plugin.api import http
+from livestreamer.stream import HDSStream, HLSStream, RTMPStream
+
+
+STREAMS_INFO_URL = "http://www.expressen.se/Handlers/WebTvHandler.ashx?id={0}";
+
+_url_re = re.compile("http(s)?://(?:\w+.)?\.expressen\.se")
+_meta_xmlurl_id_re = re.compile('<meta.+xmlUrl=http%3a%2f%2fwww.expressen.se%2fHandlers%2fWebTvHandler.ashx%3fid%3d([0-9]*)" />')
+
+
+class Expressen(Plugin):
+    @classmethod
+    def can_handle_url(cls, url):
+        return _url_re.match(url)
+
+    def _get_streams(self):
+        res = http.get(self.url)
+        
+        match = _meta_xmlurl_id_re.search(res.text)
+        if not match:
+            return;
+        
+        xml_info_url = STREAMS_INFO_URL.format(match.group(1))
+        video_info_res = http.get(xml_info_url)
+        parsed_info = http.xml(video_info_res)
+        
+        live_el = parsed_info.find("live");
+        live = live_el is not None and live_el.text == "1"
+        
+        streams = { }
+        
+        hdsurl_el = parsed_info.find("hdsurl");
+        if hdsurl_el is not None and hdsurl_el.text is not None:
+            hdsurl = hdsurl_el.text
+            streams.update(HDSStream.parse_manifest(self.session, hdsurl))
+            
+        if live:
+            vurls_el = parsed_info.find("vurls");
+            if vurls_el is not None:
+                for i, vurl_el in enumerate(vurls_el):
+                    bitrate = vurl_el.get("bitrate")
+                    name = bitrate + "k" if bitrate is not None else "rtmp{0}".format(i)
+                    params = {
+                        "rtmp": vurl_el.text,
+                    }
+                    streams[name] = RTMPStream(self.session, params)
+                    
+        parsed_urls = set()
+        mobileurls_el = parsed_info.find("mobileurls");
+        if mobileurls_el is not None:
+            for mobileurl_el in mobileurls_el:
+                text = mobileurl_el.text
+                if not text:
+                    continue
+                
+                if text in parsed_urls:
+                    continue
+                
+                parsed_urls.add(text)
+                url = urlparse(text)
+                
+                if url[0] == "http" and url[2].endswith("m3u8"):
+                    streams.update(HLSStream.parse_variant_playlist(self.session, text))
+        
+        return streams
+
+__plugin__ = Expressen
\ No newline at end of file
diff --git a/src/livestreamer/plugins/goodgame.py b/src/livestreamer/plugins/goodgame.py
index 97bb7cc..c8482f9 100644
--- a/src/livestreamer/plugins/goodgame.py
+++ b/src/livestreamer/plugins/goodgame.py
@@ -12,9 +12,12 @@ QUALITIES = {
     "240p": "_240"
 }
 
-_url_re = re.compile("http://(?:www\.)?goodgame.ru/channel/(?P<user>\w+)/")
+_url_re = re.compile("http://(?:www\.)?goodgame.ru/channel/(?P<user>\w+)")
 _stream_re = re.compile(
-    "iframe frameborder=\"0\" width=\"100%\" height=\"100%\" src=\"http://goodgame.ru/player(\d)?\?(\d+)\""
+    "iframe frameborder=\"0\" width=\"100%\" height=\"100%\" src=\"http://goodgame.ru/player(\d)?\?(\w+)\""
+)
+_ddos_re = re.compile(
+    "document.cookie=\"(__DDOS_[^;]+)"
 )
 
 class GoodGame(Plugin):
@@ -28,7 +31,16 @@ class GoodGame(Plugin):
             return True
 
     def _get_streams(self):
-        res = http.get(self.url)
+        headers = {
+            "Referer": self.url
+        }
+        res = http.get(self.url, headers=headers)
+
+        match = _ddos_re.search(res.text)
+        if (match):
+            headers["Cookie"] = match.group(1)
+            res = http.get(self.url, headers=headers)
+
         match = _stream_re.search(res.text)
         if not match:
             return
diff --git a/src/livestreamer/plugins/livecodingtv.py b/src/livestreamer/plugins/livecodingtv.py
new file mode 100644
index 0000000..03a5d5c
--- /dev/null
+++ b/src/livestreamer/plugins/livecodingtv.py
@@ -0,0 +1,34 @@
+import re
+
+from livestreamer.plugin import Plugin
+from livestreamer.stream import RTMPStream
+from livestreamer.plugin.api import http
+
+
+_rtmp_re = re.compile('rtmp://[^"]+/(?P<channel>\w+)+[^/"]+')
+_url_re = re.compile("http(s)?://(?:\w+.)?\livecoding\.tv")
+
+
+class LivecodingTV(Plugin):
+    @classmethod
+    def can_handle_url(cls, url):
+        return _url_re.match(url)
+
+    def _get_streams(self):
+        res = http.get(self.url)
+        match = _rtmp_re.search(res.text)
+        rtmp_url = match.group(0)
+
+        if not match:
+            return
+
+        stream = RTMPStream(self.session, {
+            "rtmp": rtmp_url,
+            "pageUrl": self.url,
+            "live": True,
+        })
+
+        return dict(live=stream)
+
+
+__plugin__ = LivecodingTV
diff --git a/src/livestreamer/plugins/media_ccc_de.py b/src/livestreamer/plugins/media_ccc_de.py
index aa082fb..9f059f7 100644
--- a/src/livestreamer/plugins/media_ccc_de.py
+++ b/src/livestreamer/plugins/media_ccc_de.py
@@ -26,15 +26,15 @@ from livestreamer.plugin.api import http
 from livestreamer.stream import HTTPStream, HLSStream
 
 API_URL_MEDIA           = "https://api.media.ccc.de"
-API_URL_STREAMING_MEDIA = "http://streaming.media.ccc.de/streams/v1.json"
+API_URL_STREAMING_MEDIA = "https://streaming.media.ccc.de/streams/v1.json"
 
 # http(s)://media.ccc.de/path/to/talk.html
 _url_media_re           = re.compile("(?P<scheme>http|https)"
                                      ":\/\/"
                                      "(?P<server>media\.ccc\.de)"
-                                     "\/.*\.html")
-# http://streaming.media.ccc.de/room/
-_url_streaming_media_re = re.compile("(?P<scheme>http)"
+                                     "\/")
+# https://streaming.media.ccc.de/room/
+_url_streaming_media_re = re.compile("(?P<scheme>http|https)"
                                      ":\/\/"
                                      "(?P<server>streaming\.media\.ccc\.de)"
                                      "\/"
diff --git a/src/livestreamer/plugins/meerkat.py b/src/livestreamer/plugins/meerkat.py
new file mode 100755
index 0000000..ab551cc
--- /dev/null
+++ b/src/livestreamer/plugins/meerkat.py
@@ -0,0 +1,26 @@
+import re
+
+from livestreamer.plugin import Plugin
+from livestreamer.stream import HLSStream
+
+
+_url_re = re.compile(r"http(s)?://meerkatapp.co/(?P<user>[\w\-\=]+)/(?P<token>[\w\-]+)")
+
+
+class Meerkat(Plugin):
+    @classmethod
+    def can_handle_url(cls, url):
+        return _url_re.match(url)
+
+    def _get_streams(self):
+        match = _url_re.match(self.url)
+        if not match:
+            return
+
+        streams = {}
+        streams["live"] = HLSStream(self.session, "http://cdn.meerkatapp.co/broadcast/{0}/live.m3u8".format(match.group("token")))
+
+        return streams
+
+
+__plugin__ = Meerkat
diff --git a/src/livestreamer/plugins/nos.py b/src/livestreamer/plugins/nos.py
index 7cab27b..ea37e54 100644
--- a/src/livestreamer/plugins/nos.py
+++ b/src/livestreamer/plugins/nos.py
@@ -3,6 +3,7 @@
 Supports:
    MP$: http://nos.nl/uitzending/nieuwsuur.html
    Live: http://www.nos.nl/livestream/*
+   Tour: http://nos.nl/tour/live
 """
 
 import re
@@ -67,7 +68,7 @@ class NOS(Plugin):
     def _get_streams(self):
         urlparts = self.url.split('/')
 
-        if urlparts[-2] == 'livestream':
+        if urlparts[-2] == 'livestream' or urlparts[-3] == 'tour':
             return self._resolve_stream()
         else:
             return self._get_source_streams()
diff --git a/src/livestreamer/plugins/npo.py b/src/livestreamer/plugins/npo.py
index 94e1a87..2ad01f1 100644
--- a/src/livestreamer/plugins/npo.py
+++ b/src/livestreamer/plugins/npo.py
@@ -8,6 +8,7 @@ Supports:
 import re
 import json
 
+from livestreamer.compat import quote
 from livestreamer.plugin import Plugin
 from livestreamer.plugin.api import http
 from livestreamer.stream import HTTPStream, HLSStream
@@ -23,19 +24,47 @@ class NPO(Plugin):
         return _url_re.match(url)
 
     def get_token(self):
-        token = http.get('http://ida.omroep.nl/npoplayer/i.js', headers=HTTP_HEADERS).content
-        return re.compile('token.*?"(.*?)"', re.DOTALL + re.IGNORECASE).search(token).group(1)
+        url = 'http://ida.omroep.nl/npoplayer/i.js?s={}'.format(quote(self.url))
+        token = http.get(url, headers=HTTP_HEADERS).text
+        token = re.compile('token.*?"(.*?)"', re.DOTALL + re.IGNORECASE).search(token).group(1)
+
+        # Great the have a ['en','ok','t'].reverse() decurity option in npoplayer.js
+        secured = list(token)
+        token = list(token)
+
+        first = -1
+        second = -1
+        for i, c in enumerate(token):
+            if c.isdigit() and 4 < i < len(token):
+                if first == -1:
+                    first = i
+                else:
+                    second = i
+                    break
+
+        if first == -1:
+            first = 12
+        if second == -1:
+            second = 13
+
+        secured[first] = token[second]
+        secured[second] = token[first]
+        return ''.join(secured)
 
     def _get_meta(self):
-        html = http.get('http://www.npo.nl/live/{}'.format(self.npo_id), headers=HTTP_HEADERS).content
+        html = http.get('http://www.npo.nl/live/{}'.format(self.npo_id), headers=HTTP_HEADERS).text
         program_id = re.compile('data-prid="(.*?)"', re.DOTALL + re.IGNORECASE).search(html).group(1)
-        meta = http.get('http://e.omroep.nl/metadata/{}'.format(program_id), headers=HTTP_HEADERS).content
+        meta = http.get('http://e.omroep.nl/metadata/{}'.format(program_id), headers=HTTP_HEADERS).text
         meta = re.compile('({.*})', re.DOTALL + re.IGNORECASE).search(meta).group(1)
         return json.loads(meta)
 
     def _get_vod_streams(self):
-        url = 'http://ida.omroep.nl/odi/?prid={}&puboptions=adaptive,h264_bb,h264_std,h264_sb&adaptive=no&part=1&token={}'.format(self.npo_id, self.get_token())
-        data = http.get(url, headers=HTTP_HEADERS).json()
+        url = 'http://ida.omroep.nl/odi/?prid={}&puboptions=adaptive,h264_bb,h264_sb,h264_std&adaptive=no&part=1&token={}'\
+            .format(quote(self.npo_id), quote(self.get_token()))
+        res = http.get(url, headers=HTTP_HEADERS);
+
+        data = res.json()
+
         streams = {}
         stream = http.get(data['streams'][0].replace('jsonp', 'json'), headers=HTTP_HEADERS).json()
         streams['best'] = streams['high'] = HTTPStream(self.session, stream['url'])
@@ -43,11 +72,11 @@ class NPO(Plugin):
 
     def _get_live_streams(self):
         meta = self._get_meta()
-        stream = filter(lambda x: x['type'] == 'hls', meta['streams'])[0]['url']
+        stream = [x for x in meta['streams'] if x['type'] == 'hls'][0]['url']
 
         url = 'http://ida.omroep.nl/aapi/?type=jsonp&stream={}&token={}'.format(stream, self.get_token())
         streamdata = http.get(url, headers=HTTP_HEADERS).json()
-        deeplink = http.get(streamdata['stream'], headers=HTTP_HEADERS).content
+        deeplink = http.get(streamdata['stream'], headers=HTTP_HEADERS).text
         deeplink = re.compile('"(.*?)"', re.DOTALL + re.IGNORECASE).search(deeplink).group(1)
         playlist_url = deeplink.replace("\\/", "/")
         return HLSStream.parse_variant_playlist(self.session, playlist_url)
diff --git a/src/livestreamer/plugins/nrk.py b/src/livestreamer/plugins/nrk.py
index 6e8df5e..6a28ffa 100644
--- a/src/livestreamer/plugins/nrk.py
+++ b/src/livestreamer/plugins/nrk.py
@@ -10,7 +10,7 @@ COOKIE_PARAMS = (
     "preferred-player-live=hlslink"
 )
 
-_url_re = re.compile("http://(tv|radio).nrk.no/")
+_url_re = re.compile("https?://(tv|radio).nrk.no/")
 _media_url_re = re.compile("""
     <div[^>]*?id="playerelement"[^>]+
     data-media="(?P<url>[^"]+)"
@@ -37,6 +37,7 @@ class NRK(Plugin):
         return _url_re.match(url)
 
     def _get_streams(self):
+        # Get the stream type from the url (tv/radio).
         stream_type = _url_re.match(self.url).group(1).upper()
         cookie = {
             "NRK_PLAYER_SETTINGS_{0}".format(stream_type): COOKIE_PARAMS
diff --git a/src/livestreamer/plugins/periscope.py b/src/livestreamer/plugins/periscope.py
index f9eacb7..09a9e0d 100644
--- a/src/livestreamer/plugins/periscope.py
+++ b/src/livestreamer/plugins/periscope.py
@@ -10,10 +10,23 @@ STREAM_INFO_URL = "https://api.periscope.tv/api/v2/getAccessPublic"
 STATUS_GONE = 410
 STATUS_UNAVAILABLE = (STATUS_GONE,)
 
-_url_re = re.compile(r"http(s)?://(www\.)?periscope.tv/w/(?P<token>[\w\-\=]+)")
+_url_re = re.compile(r"http(s)?://(www\.)?periscope.tv/w/(?P<broadcast_id>[\w\-\=]+)")
 _stream_schema = validate.Schema(
-    {"hls_url": validate.url(scheme="http")},
-    validate.get("hls_url")
+    validate.any(
+        None,
+        validate.union({
+            "hls_url": validate.all(
+                {"hls_url": validate.url(scheme="http")},
+                validate.get("hls_url")
+            ),
+        }),
+        validate.union({
+            "replay_url": validate.all(
+                {"replay_url": validate.url(scheme="http")},
+                validate.get("replay_url")
+            ),
+        }),
+    ),
 )
 
 
@@ -32,7 +45,12 @@ class Periscope(Plugin):
             return
 
         playlist_url = http.json(res, schema=_stream_schema)
-        return HLSStream.parse_variant_playlist(self.session, playlist_url)
-
+        if "hls_url" in playlist_url:
+            return HLSStream.parse_variant_playlist(self.session, playlist_url["hls_url"])
+        elif "replay_url" in playlist_url:
+            self.logger.info("Live Stream ended, using replay instead")
+            return dict(replay=HLSStream(self.session, playlist_url["replay_url"]))
+        else:
+            return
 
 __plugin__ = Periscope
diff --git a/src/livestreamer/plugins/picarto.py b/src/livestreamer/plugins/picarto.py
index db80af3..b05b00f 100644
--- a/src/livestreamer/plugins/picarto.py
+++ b/src/livestreamer/plugins/picarto.py
@@ -1,14 +1,18 @@
 import re
 
 from livestreamer.plugin import Plugin
+from livestreamer.plugin.api import http
 from livestreamer.stream import RTMPStream
 
-RTMP_URL = "rtmp://live.us.picarto.tv/golive/{0}"
+API_CHANNEL_INFO = "https://picarto.tv/process/channel"
+RTMP_URL = "{}/?{}/{}"
 
-_url_re = re.compile("""
-    http(s)?://(\w+\.)?picarto.tv
-    /live/(channel|channelhd|multistream).php
-    .+watch=(?P<channel>[^&?/]+)
+_url_re = re.compile(r"""
+    https?://(\w+\.)?picarto\.tv/[^&?/]
+""", re.VERBOSE)
+
+_channel_casing_re = re.compile(r"""
+    <script>placeStreamChannel\('(?P<channel>[^']+)',[^,]+,[^,]+,'(?P<visibility>[^']+)'\);</script>
 """, re.VERBOSE)
 
 
@@ -18,12 +22,22 @@ class Picarto(Plugin):
         return _url_re.match(url)
 
     def _get_streams(self):
-        match = _url_re.match(self.url)
+        page_res = http.get(self.url)
+        match = _channel_casing_re.search(page_res.text)
+
+        if not match:
+            return {}
+
         channel = match.group("channel")
+        visibility = match.group("visibility")
+
+        channel_server_res = http.post(API_CHANNEL_INFO, data={
+            "loadbalancinginfo": channel
+        })
 
         streams = {}
         streams["live"] = RTMPStream(self.session, {
-            "rtmp": RTMP_URL.format(channel),
+            "rtmp": RTMP_URL.format(channel_server_res.text, visibility, channel),
             "pageUrl": self.url,
             "live": True
         })
diff --git a/src/livestreamer/plugins/ruv.py b/src/livestreamer/plugins/ruv.py
new file mode 100644
index 0000000..598f1a9
--- /dev/null
+++ b/src/livestreamer/plugins/ruv.py
@@ -0,0 +1,163 @@
+"""Plugin for RUV, the Icelandic national television."""
+
+import re
+
+from livestreamer.plugin import Plugin
+from livestreamer.stream import RTMPStream, HLSStream
+
+from livestreamer.plugin.api import http
+
+RTMP_LIVE_URL = "rtmp://ruv{0}livefs.fplive.net/ruv{0}live-live/stream{1}"
+RTMP_SARPURINN_URL = "rtmp://sipvodfs.fplive.net/sipvod/{0}/{1}{2}.{3}"
+
+HLS_RUV_LIVE_URL = "http://ruvruv-live.hls.adaptive.level3.net/ruv/ruv/index/stream{0}.m3u8"
+HLS_RADIO_LIVE_URL = "http://sip-live.hds.adaptive.level3.net/hls-live/ruv-{0}/_definst_/live/stream1.m3u8"
+HLS_SARPURINN_URL = "http://sip-ruv-vod.dcp.adaptive.level3.net/{0}/{1}{2}.{3}.m3u8"
+
+
+_live_url_re = re.compile(r"""^(?:https?://)?(?:www\.)?ruv\.is/
+                                (?P<channel_path>
+                                    ruv|
+                                    ras1|
+                                    ras-1|
+                                    ras2|
+                                    ras-2|
+                                    rondo
+                                )
+                                /?
+                                """, re.VERBOSE)
+
+_sarpurinn_url_re = re.compile(r"""^(?:https?://)?(?:www\.)?ruv\.is/sarpurinn/
+                                    (?:
+                                        ruv|
+                                        ruv2|
+                                        ruv-2|
+                                        ruv-aukaras|
+                                        ras1|
+                                        ras-1|
+                                        ras2|
+                                        ras-2
+                                    )
+                                    /
+                                    [a-zA-Z0-9_-]+
+                                    /
+                                    [0-9]+
+                                    /?
+                                    """, re.VERBOSE)
+
+_rtmp_url_re = re.compile(r"""rtmp://sipvodfs\.fplive.net/sipvod/
+                                (?P<status>
+                                    lokad|
+                                    opid
+                                )
+                                /
+                                (?P<date>[0-9]+/[0-9][0-9]/[0-9][0-9]/)?
+                                (?P<id>[A-Z0-9\$_]+)
+                                \.
+                                (?P<ext>
+                                    mp4|
+                                    mp3
+                                )""", re.VERBOSE)
+
+_id_map = {
+    "ruv": "ruv",
+    "ras1": "ras1",
+    "ras-1": "ras1",
+    "ras2": "ras2",
+    "ras-2": "ras2",
+    "rondo": "ras3"
+}
+
+
+class Ruv(Plugin):
+    @classmethod
+    def can_handle_url(cls, url):
+        if _live_url_re.match(url):
+            return _live_url_re.match(url)
+        else:
+            return _sarpurinn_url_re.match(url)
+
+    def __init__(self, url):
+        Plugin.__init__(self, url)
+        live_match = _live_url_re.match(url)
+
+        if live_match:
+            self.live = True
+            self.channel_path = live_match.group("channel_path")
+        else:
+            self.live = False
+
+    def _get_live_streams(self):
+        stream_id = _id_map[self.channel_path]
+
+        if stream_id == "ruv":
+            qualities_rtmp = ["720p", "480p", "360p", "240p"]
+
+            for i, quality in enumerate(qualities_rtmp):
+                yield quality, RTMPStream(
+                    self.session,
+                    {
+                        "rtmp": RTMP_LIVE_URL.format(stream_id, i+1),
+                        "pageUrl": self.url,
+                        "live": True
+                    }
+                )
+
+            qualities_hls = ["240p", "360p", "480p", "720p"]
+            for i, quality_hls in enumerate(qualities_hls):
+                yield quality_hls, HLSStream(
+                    self.session,
+                    HLS_RUV_LIVE_URL.format(i+1)
+                )
+
+        else:
+            yield "audio", RTMPStream(self.session, {
+                "rtmp": RTMP_LIVE_URL.format(stream_id, 1),
+                "pageUrl": self.url,
+                "live": True
+            })
+
+            yield "audio", HLSStream(
+                self.session,
+                HLS_RADIO_LIVE_URL.format(stream_id)
+            )
+
+    def _get_sarpurinn_streams(self):
+        res = http.get(self.url)
+        match = _rtmp_url_re.search(res.text)
+
+        if not match:
+            yield
+
+        token = match.group("id")
+        status = match.group("status")
+        extension = match.group("ext")
+        date = match.group("date")
+        if not date:
+            date = ""
+
+        if extension == "mp3":
+            key = "audio"
+        else:
+            key = "576p"
+
+            # HLS on Sarpurinn is currently only available on videos
+            yield key, HLSStream(
+                self.session,
+                HLS_SARPURINN_URL.format(status, date, token, extension)
+            )
+
+        yield key, RTMPStream(self.session, {
+            "rtmp": RTMP_SARPURINN_URL.format(status, date, token, extension),
+            "pageUrl": self.url,
+            "live": True
+        })
+
+    def _get_streams(self):
+        if self.live:
+            return self._get_live_streams()
+        else:
+            return self._get_sarpurinn_streams()
+
+
+__plugin__ = Ruv
diff --git a/src/livestreamer/plugins/servustv.py b/src/livestreamer/plugins/servustv.py
new file mode 100644
index 0000000..c141a26
--- /dev/null
+++ b/src/livestreamer/plugins/servustv.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+import re
+
+from livestreamer.plugin import Plugin
+from livestreamer.stream import HDSStream
+
+_channel = dict(
+    at="servustvhd_1@51229",
+    de="servustvhdde_1@75540"
+)
+
+STREAM_INFO_URL = "http://hdiosstv-f.akamaihd.net/z/{channel}/manifest.f4m"
+_url_re = re.compile(r"http://(?:www.)?servustv.com/(de|at)/.*")
+
+
+class ServusTV(Plugin):
+    @classmethod
+    def can_handle_url(cls, url):
+        match = _url_re.match(url)
+        return match
+
+    def _get_streams(self):
+        url_match = _url_re.match(self.url)
+        if url_match:
+            if url_match.group(1) in _channel:
+                return HDSStream.parse_manifest(self.session, STREAM_INFO_URL.format(channel=_channel[url_match.group(1)]))
+
+
+__plugin__ = ServusTV
diff --git a/src/livestreamer/plugins/streamupcom.py b/src/livestreamer/plugins/streamupcom.py
new file mode 100644
index 0000000..e93cd28
--- /dev/null
+++ b/src/livestreamer/plugins/streamupcom.py
@@ -0,0 +1,24 @@
+import re
+
+from livestreamer.compat import urljoin
+from livestreamer.plugin import Plugin
+from livestreamer.plugin.api import http, validate
+from livestreamer.stream import RTMPStream, HLSStream
+
+_url_re = re.compile("http(s)?://(\w+\.)?streamup.com/(?P<channel>[^/?]+)")
+_hls_manifest_re = re.compile('HlsManifestUrl:\\s*"//"\\s*\\+\\s*response\\s*\\+\\s*"(.+)"')
+
+class StreamupCom(Plugin):
+    @classmethod
+    def can_handle_url(cls, url):
+        return _url_re.match(url)
+
+    def _get_streams(self):
+        res = http.get(self.url)
+        if not res: return
+        match = _hls_manifest_re.search(res.text)
+        url = match.group(1)
+        hls_url = "http://video-cdn.streamup.com{}".format(url)
+        return HLSStream.parse_variant_playlist(self.session, hls_url)
+
+__plugin__ = StreamupCom
diff --git a/src/livestreamer/plugins/tvcatchup.py b/src/livestreamer/plugins/tvcatchup.py
index 7dbbe3d..3c4bebc 100644
--- a/src/livestreamer/plugins/tvcatchup.py
+++ b/src/livestreamer/plugins/tvcatchup.py
@@ -4,10 +4,9 @@ from livestreamer.plugin import Plugin
 from livestreamer.plugin.api import http
 from livestreamer.stream import HLSStream
 
-SUCCESS_HTTP_CODES = (200,)
-
-STREAM_URL_FORMAT = "http://tvcatchup.com/stream.php?chan={0}"
-_url_re = re.compile("http://(?:www\.)?tvcatchup.com/watch/(?P<channel_id>[0-9]+)")
+USER_AGENT = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
+_url_re = re.compile("http://(?:www\.)?tvcatchup.com/watch/\w+")
+_stream_re = re.compile(r"\"(?P<stream_url>https?://.*m3u8\?.*clientKey=[^\"]*)\";")
 
 
 class TVCatchup(Plugin):
@@ -17,17 +16,21 @@ class TVCatchup(Plugin):
 
     def _get_streams(self):
         """
-        Finds the stream from tvcatchup, they only provide a single 480p stream per channel.
+        Finds the streams from tvcatchup.com.
         """
-        match = _url_re.match(self.url).groupdict()
-        channel_id = match["channel_id"]
+        http.headers.update({"User-Agent": USER_AGENT})
+        res = http.get(self.url)
 
-        res = http.get(STREAM_URL_FORMAT.format(channel_id))
+        match = _stream_re.search(res.text, re.IGNORECASE | re.MULTILINE)
 
-        stream_url = http.json(res).get('url')
+        if match:
+            stream_url = match.groupdict()["stream_url"]
 
-        if stream_url:
-            return {"480p": HLSStream(self.session, stream_url)}
+            if stream_url:
+                if "_adp" in stream_url:
+                    return HLSStream.parse_variant_playlist(self.session, stream_url)
+                else:
+                    return {'576p': HLSStream(self.session, stream_url)}
 
 
 __plugin__ = TVCatchup
diff --git a/src/livestreamer/plugins/tvplayer.py b/src/livestreamer/plugins/tvplayer.py
index 8d18feb..400ef8d 100644
--- a/src/livestreamer/plugins/tvplayer.py
+++ b/src/livestreamer/plugins/tvplayer.py
@@ -5,7 +5,9 @@ from livestreamer.plugin import Plugin
 from livestreamer.plugin.api import http, validate
 from livestreamer.stream import HLSStream
 
-
+USER_AGENT_STRING = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) "
+                     "AppleWebKit/537.36 (KHTML, like Gecko) "
+                     "Chrome/43.0.2357.65 Safari/537.36")
 STREAM_INFO_URL = "http://lapi.cdn.tvplayer.com/tvplayer/stream/live/id/{id}"
 _url_re = re.compile(r"http://(?:www.)?tvplayer.com/watch/(.+)")
 _channel_map_re = re.compile(r'href="/watch/([a-z]+?)".*?img.*?src=".*?/(\d+).png"', re.S)
@@ -32,7 +34,9 @@ class TVPlayer(Plugin):
             res = http.get(STREAM_INFO_URL.format(id=channel_id))
             stream_data = http.json(res, schema=_channel_schema)
 
-            return HLSStream.parse_variant_playlist(self.session, stream_data['stream'])
+            return HLSStream.parse_variant_playlist(self.session,
+                                                    stream_data['stream'],
+                                                    headers={'user-agent': USER_AGENT_STRING})
 
 
 __plugin__ = TVPlayer
diff --git a/src/livestreamer/plugins/twitch.py b/src/livestreamer/plugins/twitch.py
index 0adc872..f94ac09 100644
--- a/src/livestreamer/plugins/twitch.py
+++ b/src/livestreamer/plugins/twitch.py
@@ -30,7 +30,7 @@ QUALITY_WEIGHTS = {
 _url_re = re.compile(r"""
     http(s)?://
     (?:
-        (?P<subdomain>\w+)
+        (?P<subdomain>[\w\-]+)
         \.
     )?
     twitch.tv
@@ -133,6 +133,7 @@ class UsherService(object):
             "type": "any",
             "allow_source": "true",
             "allow_audio_only": "true",
+            "allow_spectre": "false",
         }
         params.update(extra_params)
 
diff --git a/src/livestreamer/plugins/ustreamtv.py b/src/livestreamer/plugins/ustreamtv.py
index 74c04c7..8c9cba2 100644
--- a/src/livestreamer/plugins/ustreamtv.py
+++ b/src/livestreamer/plugins/ustreamtv.py
@@ -71,25 +71,31 @@ _recorded_schema = validate.Schema({
         }]
     )
 })
-_stream_schema = validate.Schema({
-    "name": validate.text,
-    "url": validate.text,
-    "streams": validate.all(
-        _amf3_array,
-        [{
-            "chunkId": validate.any(int, float),
-            "chunkRange": {validate.text: validate.text},
-            "chunkTime": validate.any(int, float),
-            "offset": validate.any(int, float),
-            "offsetInMs": validate.any(int, float),
-            "streamName": validate.text,
-            validate.optional("bitrate"): validate.any(int, float),
-            validate.optional("height"): validate.any(int, float),
-            validate.optional("description"): validate.text,
-            validate.optional("isTranscoded"): bool
-        }],
-    )
-})
+_stream_schema = validate.Schema(
+    validate.any({
+        "name": validate.text,
+        "url": validate.text,
+        "streams": validate.all(
+            _amf3_array,
+            [{
+                "chunkId": validate.any(int, float),
+                "chunkRange": {validate.text: validate.text},
+                "chunkTime": validate.any(int, float),
+                "offset": validate.any(int, float),
+                "offsetInMs": validate.any(int, float),
+                "streamName": validate.text,
+                validate.optional("bitrate"): validate.any(int, float),
+                validate.optional("height"): validate.any(int, float),
+                validate.optional("description"): validate.text,
+                validate.optional("isTranscoded"): bool
+            }],
+        )
+    },
+    {
+        "name": validate.text,
+        "varnishUrl": validate.text
+    })
+)
 _channel_schema = validate.Schema({
     validate.optional("stream"): validate.any(
         validate.all(
@@ -502,6 +508,8 @@ class UStreamTV(Plugin):
 
         streams = {}
         for provider in channel["stream"]:
+            if provider["name"] == u"uhs_akamai":  # not heavily tested, but got a stream working
+                continue
             provider_url = provider["url"]
             provider_name = provider["name"]
             for stream_index, stream_info in enumerate(provider["streams"]):
diff --git a/src/livestreamer/plugins/vaughnlive.py b/src/livestreamer/plugins/vaughnlive.py
index dd265b9..d5b3b36 100644
--- a/src/livestreamer/plugins/vaughnlive.py
+++ b/src/livestreamer/plugins/vaughnlive.py
@@ -24,7 +24,7 @@ def decode_token(token):
     return token.replace("0m0", "")
 
 _schema = validate.Schema(
-    validate.transform(lambda s: s.split(";:mvnkey%")),
+    validate.transform(lambda s: s.split(";:mvnkey-")),
     validate.length(2),
     validate.union({
         "server": validate.all(
diff --git a/src/livestreamer/plugins/younow.py b/src/livestreamer/plugins/younow.py
new file mode 100644
index 0000000..555fa83
--- /dev/null
+++ b/src/livestreamer/plugins/younow.py
@@ -0,0 +1,52 @@
+"""Plugin for younow.com by WeinerRinkler"""
+
+import re
+
+from livestreamer.plugin import Plugin, PluginError
+from livestreamer.plugin.api import http
+from livestreamer.stream import RTMPStream
+
+jsonapi= "http://www.younow.com/php/api/broadcast/info/curId=0/user="
+
+# http://younow.com/channel/
+_url_re = re.compile("http(s)?://(\w+.)?younow.com/(?P<channel>[^/&?]+)")
+
+def getStreamURL(channel):
+    url = jsonapi + channel
+    res = http.get(url)
+    streamerinfo = http.json(res)
+    #print(streamerinfo)
+
+    if not any("media" in s for s in streamerinfo):
+        print ("User offline or invalid")
+        return
+    else:
+        streamdata = streamerinfo['media']
+        #print(streamdata)
+        streamurl = "rtmp://" + streamdata['host'] + streamdata['app'] + "/" + streamdata['stream']
+        #print (streamurl)
+
+    return streamurl
+
+class younow(Plugin):
+    @classmethod
+    def can_handle_url(self, url):
+        return _url_re.match(url)
+
+    def _get_streams(self):
+        match = _url_re.match(self.url)
+        channel = match.group("channel")
+
+        streamurl = getStreamURL(channel)
+        if not streamurl:
+            return
+        streams = {}
+        streams["live"] = RTMPStream(self.session, {
+            "rtmp": streamurl,
+            "live": True
+        })
+        
+        return streams
+
+
+__plugin__ = younow
diff --git a/src/livestreamer_cli/argparser.py b/src/livestreamer_cli/argparser.py
index 343bdfb..0dcdbaa 100644
--- a/src/livestreamer_cli/argparser.py
+++ b/src/livestreamer_cli/argparser.py
@@ -938,6 +938,16 @@ plugin.add_argument(
     """
 )
 plugin.add_argument(
+    "--crunchyroll-locale",
+    metavar="LOCALE",
+    help="""
+    Indicate which locale to use for Crunchyroll subtitles.
+
+    The locale is formatted as [language_code]_[country_code], by default
+    en_US is used.
+    """
+)
+plugin.add_argument(
     "--livestation-email",
     metavar="EMAIL",
     help="""
diff --git a/src/livestreamer_cli/main.py b/src/livestreamer_cli/main.py
index 97731ef..1c116ca 100644
--- a/src/livestreamer_cli/main.py
+++ b/src/livestreamer_cli/main.py
@@ -26,7 +26,7 @@ from .utils import NamedPipe, HTTPServer, ignored, progress, stream_to_url
 ACCEPTABLE_ERRNO = (errno.EPIPE, errno.EINVAL, errno.ECONNRESET)
 QUIET_OPTIONS = ("json", "stream_url", "subprocess_cmdline", "quiet")
 
-args = console = livestreamer = plugin = stream_fd = None
+args = console = livestreamer = plugin = stream_fd = output = None
 
 
 def check_file_output(filename, force):
@@ -123,6 +123,7 @@ def iter_http_requests(server, player):
 
 def output_stream_http(plugin, initial_streams, external=False, port=0):
     """Continuously output the stream over HTTP."""
+    global output
 
     if not external:
         if not args.player:
@@ -131,9 +132,9 @@ def output_stream_http(plugin, initial_streams, external=False, port=0):
                          "executable with --player.")
 
         server = create_http_server()
-        player = PlayerOutput(args.player, args=args.player_args,
-                              filename=server.url,
-                              quiet=not args.verbose_player)
+        player = output = PlayerOutput(args.player, args=args.player_args,
+                                       filename=server.url,
+                                       quiet=not args.verbose_player)
 
         try:
             console.logger.info("Starting player: {0}", args.player)
@@ -192,15 +193,16 @@ def output_stream_http(plugin, initial_streams, external=False, port=0):
 
 def output_stream_passthrough(stream):
     """Prepares a filename to be passed to the player."""
+    global output
 
     filename = '"{0}"'.format(stream_to_url(stream))
-    out = PlayerOutput(args.player, args=args.player_args,
-                       filename=filename, call=True,
-                       quiet=not args.verbose_player)
+    output = PlayerOutput(args.player, args=args.player_args,
+                          filename=filename, call=True,
+                          quiet=not args.verbose_player)
 
     try:
         console.logger.info("Starting player: {0}", args.player)
-        out.open()
+        output.open()
     except OSError as err:
         console.exit("Failed to start player: {0} ({1})", args.player, err)
         return False
@@ -239,6 +241,7 @@ def open_stream(stream):
 
 def output_stream(stream):
     """Open stream, create output and finally write the stream to output."""
+    global output
 
     for i in range(args.retry_open):
         try:
@@ -422,18 +425,22 @@ def resolve_stream_name(streams, stream_name):
     return stream_name
 
 
-def format_valid_streams(streams):
+def format_valid_streams(plugin, streams):
     """Formats a dict of streams.
 
     Filters out synonyms and displays them next to
     the stream they point to.
 
+    Streams are sorted according to their quality
+    (based on plugin.stream_weight).
+
     """
 
     delimiter = ", "
     validstreams = []
 
-    for name, stream in sorted(streams.items()):
+    for name, stream in sorted(streams.items(),
+                               key=lambda stream: plugin.stream_weight(stream[0])):
         if name in STREAM_SYNONYMS:
             continue
 
@@ -484,7 +491,7 @@ def handle_url():
         args.stream = args.default_stream
 
     if args.stream:
-        validstreams = format_valid_streams(streams)
+        validstreams = format_valid_streams(plugin, streams)
         for stream_name in args.stream:
             if stream_name in streams:
                 console.logger.info("Available streams: {0}", validstreams)
@@ -504,7 +511,7 @@ def handle_url():
         if console.json:
             console.msg_json(dict(streams=streams, plugin=plugin.module))
         else:
-            validstreams = format_valid_streams(streams)
+            validstreams = format_valid_streams(plugin, streams)
             console.msg("Available streams: {0}", validstreams)
 
 
@@ -785,6 +792,10 @@ def setup_plugin_options():
         livestreamer.set_plugin_option("crunchyroll", "purge_credentials",
                                        args.crunchyroll_purge_credentials)
 
+    if args.crunchyroll_locale:
+        livestreamer.set_plugin_option("crunchyroll", "locale",
+                                       args.crunchyroll_locale)
+
     if args.livestation_email:
         livestreamer.set_plugin_option("livestation", "email",
                                        args.livestation_email)
@@ -885,6 +896,10 @@ def main():
             setup_plugin_options()
             handle_url()
         except KeyboardInterrupt:
+            # Close output
+            if output:
+                output.close()
+
             # Make sure current stream gets properly cleaned up
             if stream_fd:
                 console.msg("Interrupted! Closing currently open stream...")
@@ -896,5 +911,12 @@ def main():
                 console.msg("Interrupted! Exiting...")
     elif args.twitch_oauth_authenticate:
         authenticate_twitch_oauth()
-    else:
+    elif args.help:
         parser.print_help()
+    else:
+        usage = parser.format_usage()
+        msg = (
+            "{usage}\nUse -h/--help to see the available options or "
+            "read the manual at http://docs.livestreamer.io/"
+        ).format(usage=usage)
+        console.msg(msg)
diff --git a/src/livestreamer_cli/utils/http_server.py b/src/livestreamer_cli/utils/http_server.py
index 0cf50af..64ff469 100644
--- a/src/livestreamer_cli/utils/http_server.py
+++ b/src/livestreamer_cli/utils/http_server.py
@@ -83,10 +83,13 @@ class HTTPServer(object):
             conn.close()
             raise OSError("Invalid request method: {0}".format(req.command))
 
-        conn.send(b"HTTP/1.1 200 OK\r\n")
-        conn.send(b"Server: Livestreamer\r\n")
-        conn.send(b"Content-Type: video/unknown\r\n")
-        conn.send(b"\r\n")
+        try:
+            conn.send(b"HTTP/1.1 200 OK\r\n")
+            conn.send(b"Server: Livestreamer\r\n")
+            conn.send(b"Content-Type: video/unknown\r\n")
+            conn.send(b"\r\n")
+        except socket.error:
+            raise OSError("Failed to write data to socket")
 
         # We don't want to send any data on HEAD requests.
         if req.command == "HEAD":
diff --git a/win32/build-with-bootstrap.sh b/win32/build-with-bootstrap.sh
index 676296b..1d2df38 100755
--- a/win32/build-with-bootstrap.sh
+++ b/win32/build-with-bootstrap.sh
@@ -33,3 +33,6 @@ unzip -d "$BUILD_TARGET_DIR/$egg" "dist/$egg"
 
 cd $BUILD_DIR
 zip -r $DIST_TARGET "$(basename "$BUILD_TARGET_DIR")"
+
+cd $SCRIPT_DIR
+makensis -DPROGRAM_VERSION=$(git describe) -DLIVESTREAMER_PYTHON_BBFREEZE_OUTPUT_DIR="$BUILD_TARGET_DIR" "livestreamer-win32-installer-from-bootstrap.nsi"
diff --git a/win32/livestreamer-win32-installer-from-bootstrap.nsi b/win32/livestreamer-win32-installer-from-bootstrap.nsi
new file mode 100644
index 0000000..d527c72
--- /dev/null
+++ b/win32/livestreamer-win32-installer-from-bootstrap.nsi
@@ -0,0 +1,149 @@
+# Livestreamer Windows installer script
+
+# Set default compressor
+SetCompressor lzma
+
+# Livestreamer program information
+!define PROGRAM_NAME "Livestreamer"
+!define PROGRAM_WEB_SITE "http://livestreamer.io/"
+
+# EnvVarUpdate
+!include EnvVarUpdate.nsh
+!include AdvReplaceInFile.nsh
+
+# --- Interface settings ---
+
+# Modern User Interface 2
+!include MUI2.nsh
+
+# Installer
+!define MUI_COMPONENTSPAGE_SMALLDESC
+!define MUI_FINISHPAGE_NOAUTOCLOSE
+!define MUI_ABORTWARNING
+
+# Uninstaller
+!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\modern-uninstall.ico"
+!define MUI_UNFINISHPAGE_NOAUTOCLOSE
+
+# --- Start of Modern User Interface ---
+
+# Welcome page
+!define MUI_WELCOMEPAGE_TITLE_3LINES
+!insertmacro MUI_PAGE_WELCOME
+
+# License page
+!insertmacro MUI_PAGE_LICENSE "..\LICENSE"
+
+# Components page
+!insertmacro MUI_PAGE_COMPONENTS
+
+# Let the user select the installation directory
+!insertmacro MUI_PAGE_DIRECTORY
+
+# Run installation
+!insertmacro MUI_PAGE_INSTFILES
+
+# Display 'finished' page
+!define MUI_FINISHPAGE_NOREBOOTSUPPORT
+!define MUI_FINISHPAGE_RUN "notepad.exe"
+!define MUI_FINISHPAGE_RUN_TEXT "Edit configuration file"
+!define MUI_FINISHPAGE_RUN_PARAMETERS "$APPDATA\livestreamer\livestreamerrc"
+!insertmacro MUI_PAGE_FINISH
+
+# Uninstaller pages
+!insertmacro MUI_UNPAGE_CONFIRM
+!insertmacro MUI_UNPAGE_INSTFILES
+!insertmacro MUI_UNPAGE_FINISH
+
+# Language files
+!insertmacro MUI_LANGUAGE "English"
+
+
+# --- Functions ---
+
+Function un.onUninstSuccess
+  HideWindow
+  MessageBox MB_ICONINFORMATION|MB_OK "$(^Name) was successfully removed from your computer."
+FunctionEnd
+
+Function un.onInit
+  MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 "Do you want to completely remove $(^Name) and all of its components?" IDYES +2
+  Abort
+FunctionEnd
+
+
+# --- Installation sections ---
+
+# Compare versions
+!include "WordFunc.nsh"
+
+!define PROGRAM_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PROGRAM_NAME}"
+!define PROGRAM_UNINST_ROOT_KEY "HKLM"
+
+# Branding text
+BrandingText "Livestreamer"
+
+Name "${PROGRAM_NAME} ${PROGRAM_VERSION}"
+OutFile "../dist/livestreamer-${PROGRAM_VERSION}-win32-setup.exe"
+
+InstallDir "$PROGRAMFILES\Livestreamer"
+
+ShowInstDetails show
+ShowUnInstDetails show
+
+SectionGroup /e "Livestreamer"
+# Install main application
+Section "Livestreamer CLI" Section1
+  SectionIn RO
+
+  SetOutPath $INSTDIR
+  File /r "${LIVESTREAMER_PYTHON_BBFREEZE_OUTPUT_DIR}\*.*"
+  File "rtmpdump\librtmp.dll"
+
+  SetOutPath "$APPDATA\livestreamer"
+
+  SetOverwrite off
+  File "livestreamerrc"
+
+  Push @INSTDIR@
+  Push $INSTDIR
+  Push all
+  Push all
+  Push "$APPDATA\livestreamer\livestreamerrc"
+  Call AdvReplaceInFile
+
+  ${EnvVarUpdate} $0 "PATH" "A" "HKLM" "$INSTDIR"
+SectionEnd
+
+Section "RTMPDump" Section2
+  SetOutPath "$INSTDIR\rtmpdump"
+  File /r "rtmpdump\*.exe"
+SectionEnd
+SectionGroupEnd
+
+Section -Uninstaller
+  WriteUninstaller "$INSTDIR\uninstall-livestreamer.exe"
+  WriteRegStr ${PROGRAM_UNINST_ROOT_KEY} "${PROGRAM_UNINST_KEY}" "DisplayName" "$(^Name)"
+  WriteRegStr ${PROGRAM_UNINST_ROOT_KEY} "${PROGRAM_UNINST_KEY}" "UninstallString" "$INSTDIR\uninstall-livestreamer.exe"
+SectionEnd
+
+
+LangString DESC_Section1 ${LANG_ENGLISH} "Install the Livestreamer CLI"
+LangString DESC_Section2 ${LANG_ENGLISH} "Install RTMPDump, which is needed for RTMP streams"
+
+!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
+  !insertmacro MUI_DESCRIPTION_TEXT ${Section1} $(DESC_Section1)
+  !insertmacro MUI_DESCRIPTION_TEXT ${Section2} $(DESC_Section2)
+!insertmacro MUI_FUNCTION_DESCRIPTION_END
+
+
+# --- Uninstallation section(s) ---
+
+Section Uninstall
+  RmDir /r "$INSTDIR"
+
+  SetShellVarContext all
+
+  DeleteRegKey ${PROGRAM_UNINST_ROOT_KEY} "${PROGRAM_UNINST_KEY}"
+  ${un.EnvVarUpdate} $0 "PATH" "R" "HKLM" "$INSTDIR"
+SectionEnd