Repository: streamlink/streamlink
Pull request #2152 (2018-11-04 23:00:50)

 Fix recording added in #920
Fix recording added in #920 by @grazzolini

 - Remove `-r` as short option for `--rtmp-rtmpdump`

 - Add options `--record` (`-r`) and `--record-and-pipe` (`-R`) as suggested by @bastimeyer in discussion on #920

 - Add info message that the download started as suggested by @back-to in discussion on #920

 - Add tests for `streamlink_cli.main.create_output` where these arguments are used

---

Since original author abandoned his PR, and I think this is a really useful feature, I have gone ahead and changed their code to implement suggested changes and add necessary tests. 


Patch:
diff --git a/.travis.yml b/.travis.yml
index ca3421c2..188d6eaa 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -53,6 +53,8 @@ addons:
   apt:
     packages:
     - nsis
+    - imagemagick
+    - inkscape
 
 before_deploy:
   - ./script/bintrayconfig.sh
diff --git a/NOTICE b/NOTICE
index 61540e3e..67fbc3eb 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,5 +1,4 @@
 Streamlink includes some bundled python libraries to ease installation.
-Streamlink also makes use of an image (doggo.ico) created by Jinho Jung, distributed under the CC BY-SA 2.0 license.
 
 pbs License
 ===========
@@ -47,56 +46,3 @@ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-doggo.ico License
-=================
-THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
-
-BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS.
-
-1. Definitions
-
-"Collective Work" means a work, such as a periodical issue, anthology or encyclopedia, in which the Work in its entirety in unmodified form, along with a number of other contributions, constituting separate and independent works in themselves, are assembled into a collective whole. A work that constitutes a Collective Work will not be considered a Derivative Work (as defined below) for the purposes of this License.
-"Derivative Work" means a work based upon the Work or upon the Work and other pre-existing works, such as a translation, musical arrangement, dramatization, fictionalization, motion picture version, sound recording, art reproduction, abridgment, condensation, or any other form in which the Work may be recast, transformed, or adapted, except that a work that constitutes a Collective Work will not be considered a Derivative Work for the purpose of this License. For the avoidance of doubt, where the Work is a musical composition or sound recording, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered a Derivative Work for the purpose of this License.
-"Licensor" means the individual or entity that offers the Work under the terms of this License.
-"Original Author" means the individual or entity who created the Work.
-"Work" means the copyrightable work of authorship offered under the terms of this License.
-"You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation.
-"License Elements" means the following high-level license attributes as selected by Licensor and indicated in the title of this License: Attribution, ShareAlike.
-2. Fair Use Rights. Nothing in this license is intended to reduce, limit, or restrict any rights arising from fair use, first sale or other limitations on the exclusive rights of the copyright owner under copyright law or other applicable laws.
-
-3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below:
-
-to reproduce the Work, to incorporate the Work into one or more Collective Works, and to reproduce the Work as incorporated in the Collective Works;
-to create and reproduce Derivative Works;
-to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission the Work including as incorporated in Collective Works;
-to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission Derivative Works.
-For the avoidance of doubt, where the work is a musical composition:
-
-Performance Royalties Under Blanket Licenses. Licensor waives the exclusive right to collect, whether individually or via a performance rights society (e.g. ASCAP, BMI, SESAC), royalties for the public performance or public digital performance (e.g. webcast) of the Work.
-Mechanical Rights and Statutory Royalties. Licensor waives the exclusive right to collect, whether individually or via a music rights society or designated agent (e.g. Harry Fox Agency), royalties for any phonorecord You create from the Work ("cover version") and distribute, subject to the compulsory license created by 17 USC Section 115 of the US Copyright Act (or the equivalent in other jurisdictions).
-Webcasting Rights and Statutory Royalties. For the avoidance of doubt, where the Work is a sound recording, Licensor waives the exclusive right to collect, whether individually or via a performance-rights society (e.g. SoundExchange), royalties for the public digital performance (e.g. webcast) of the Work, subject to the compulsory license created by 17 USC Section 114 of the US Copyright Act (or the equivalent in other jurisdictions).
-The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. All rights not expressly granted by Licensor are hereby reserved.
-
-4. Restrictions.The license granted in Section 3 above is expressly made subject to and limited by the following restrictions:
-
-You may distribute, publicly display, publicly perform, or publicly digitally perform the Work only under the terms of this License, and You must include a copy of, or the Uniform Resource Identifier for, this License with every copy or phonorecord of the Work You distribute, publicly display, publicly perform, or publicly digitally perform. You may not offer or impose any terms on the Work that alter or restrict the terms of this License or the recipients' exercise of the rights granted hereunder. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties. You may not distribute, publicly display, publicly perform, or publicly digitally perform the Work with any technological measures that control access or use of the Work in a manner inconsistent with the terms of this License Agreement. The above applies to the Work as incorporated in a Collective Work, but this does not require the Collective Work apart from the Work itself to be made subject to the terms of this License. If You create a Collective Work, upon notice from any Licensor You must, to the extent practicable, remove from the Collective Work any reference to such Licensor or the Original Author, as requested. If You create a Derivative Work, upon notice from any Licensor You must, to the extent practicable, remove from the Derivative Work any reference to such Licensor or the Original Author, as requested.
-You may distribute, publicly display, publicly perform, or publicly digitally perform a Derivative Work only under the terms of this License, a later version of this License with the same License Elements as this License, or a Creative Commons iCommons license that contains the same License Elements as this License (e.g. Attribution-ShareAlike 2.0 Japan). You must include a copy of, or the Uniform Resource Identifier for, this License or other license specified in the previous sentence with every copy or phonorecord of each Derivative Work You distribute, publicly display, publicly perform, or publicly digitally perform. You may not offer or impose any terms on the Derivative Works that alter or restrict the terms of this License or the recipients' exercise of the rights granted hereunder, and You must keep intact all notices that refer to this License and to the disclaimer of warranties. You may not distribute, publicly display, publicly perform, or publicly digitally perform the Derivative Work with any technological measures that control access or use of the Work in a manner inconsistent with the terms of this License Agreement. The above applies to the Derivative Work as incorporated in a Collective Work, but this does not require the Collective Work apart from the Derivative Work itself to be made subject to the terms of this License.
-If you distribute, publicly display, publicly perform, or publicly digitally perform the Work or any Derivative Works or Collective Works, You must keep intact all copyright notices for the Work and give the Original Author credit reasonable to the medium or means You are utilizing by conveying the name (or pseudonym if applicable) of the Original Author if supplied; the title of the Work if supplied; to the extent reasonably practicable, the Uniform Resource Identifier, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and in the case of a Derivative Work, a credit identifying the use of the Work in the Derivative Work (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). Such credit may be implemented in any reasonable manner; provided, however, that in the case of a Derivative Work or Collective Work, at a minimum such credit will appear where any other comparable authorship credit appears and in a manner at least as prominent as such other comparable authorship credit.
-5. Representations, Warranties and Disclaimer
-
-UNLESS OTHERWISE AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE MATERIALS, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
-
-6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
-
-7. Termination
-
-This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Derivative Works or Collective Works from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
-Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above.
-8. Miscellaneous
-
-Each time You distribute or publicly digitally perform the Work or a Collective Work, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License.
-Each time You distribute or publicly digitally perform a Derivative Work, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License.
-If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.
-No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent.
-This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You.
diff --git a/docs/index.rst b/docs/index.rst
index d9a2bbae..fd774572 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -14,7 +14,6 @@ no longer maintained.
 - Issue tracker: https://github.com/streamlink/streamlink/issues
 - PyPI: https://pypi.org/project/streamlink/
 - Free software: Simplified BSD license
-- Icon: https://www.flickr.com/photos/phploveme/27078045626 (cc-by-sa 2.0)
 
 Features
 --------
diff --git a/docs/players.rst b/docs/players.rst
index 7a888bc2..4a122f85 100644
--- a/docs/players.rst
+++ b/docs/players.rst
@@ -95,7 +95,19 @@ The stable version of VLC cannot play Youtube Live streams. This a known bug
 that is fixed in VLC 3, which is still in development. You can also try
 using a different player.
 
+Youtube Live does not work with Mplayer
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Some versions of Mplayer cannot play Youtube Live streams. And errors like:
+
+.. code-block:: console
+
+    Cannot seek backward in linear streams!
+    Seek failed
+
+Switching to a recent fork such as mpv resolves the issue.
+
 When using OMXPlayer the stream stops unexpectedly
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 When reading from a fifo pipe OMXPlayer will quit when there is no data, to fix
 this you can supply the timeout option to OMXPlayer using :option:`--player "omxplayer --timeout 20" <--player>`.
+For live streams it might be beneficial to also add the omxplayer parameter ``--live``.
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 81d3ec1d..6bd754a1 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -15,8 +15,7 @@ Name                    URL(s)               Live  VOD   Notes
 abematv                 abema.tv             Yes   Yes   Streams are geo-restricted to Japan.
 abweb                   abweb.com            Yes   No    Requires a login and a subscription.
 adultswim               adultswim.com        Yes   Yes   Streams may be geo-restricted, some VOD streams are protected by DRM.
-afreeca                 - afreecatv.com      Yes   No
-                        - afreeca.tv
+afreeca                 play.afreecatv.com   Yes   No
 aljazeeraen             aljazeera.com        Yes   Yes   English version of the site.
 animelab                animelab.com         --    Yes   Requires a login. Streams may be geo-restricted to Australia and New Zealand.
 app17                   17app.co             Yes   --
@@ -39,8 +38,12 @@ btv                     btv.bg               Yes   No    Requires login, and geo
 canalplus               mycanal.fr           No    Yes   Streams may be geo-restricted to France.
 cdnbg                   - tv.bnt.bg          Yes   No    Streams may be geo-restricted to Bulgaria.
                         - bgonair.bg
-                        - kanal3.bg
                         - bitelevision.com
+                        - bloombergtv.bg
+                        - inlife.bg
+                        - kanal3.bg
+                        - mmtvmusic.com
+                        - mu-vi.tv
                         - nova.bg
 ceskatelevize           ceskatelevize.cz     Yes   Yes   Streams may be geo-restricted to Czechia.
 cinergroup              - showtv.com.tr      Yes   No
@@ -165,6 +168,7 @@ rtpplay                 rtp.pt/play          Yes   Yes   Streams may be geo-rest
 rtve                    rtve.es              Yes   No
 rtvs                    rtvs.sk              Yes   No    Streams may be geo-restricted to Slovakia.
 ruv                     ruv.is               Yes   Yes   Streams may be geo-restricted to Iceland.
+sbscokr                 play.sbs.co.kr       Yes   No    Streams may be geo-restricted to South Korea.
 schoolism               schoolism.com        --    Yes   Requires a login and a subscription.
 senategov               senate.gov           --    Yes   Supports hearing streams.
 showroom                showroom-live.com    Yes   No    Only RTMP streams are available.
@@ -184,6 +188,7 @@ streamingvideoprovider  streamingvid... [2]_ Yes   --    RTMP streams requires r
                                                          K-S-V patches.
 streamme                stream.me            Yes   --
 streann                 ott.streann.com      Yes   Yes
+stv                     player.stv.tv        Yes   No    Streams are geo-restricted to Great Britain.
 svtplay                 - svtplay.se         Yes   Yes   Streams may be geo-restricted to Sweden.
                         - svtflow.se
                         - oppetarkiv.se
@@ -250,6 +255,7 @@ vidio                   vidio.com            Yes   Yes
 vinhlongtv              thvli.vn             Yes   No    Streams are geo-restricted to Vietnam
 vk                      vk.com               Yes   Yes
 vrtbe                   vrt.be/vrtnu         Yes   Yes
+vtvgo                   vtvgo.vn             Yes   No
 webcast_india_gov       webcast.gov.in       Yes   No    You can use #Channel to indicate CH number.
 webtv                   web.tv               Yes   --
 welt                    welt.de              Yes   Yes   Streams may be geo-restricted to Germany.
diff --git a/icon.svg b/icon.svg
new file mode 100644
index 00000000..45201351
--- /dev/null
+++ b/icon.svg
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg width="345.6" height="345.6" version="1.1" viewBox="0 0 345.6 345.6" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <metadata>
+  <rdf:RDF>
+   <cc:Work rdf:about="">
+    <dc:format>image/svg+xml</dc:format>
+    <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+    <dc:title/>
+   </cc:Work>
+  </rdf:RDF>
+ </metadata>
+ <g transform="translate(-4.7157e-6)" stroke-width=".32">
+  <path d="m155.36 327.08c-69.371-39.726-96.696-55.372-109.6-62.753l-14.88-8.5118v-166.25l141.43-80.542 2.1012 1.1696c1.1557 0.6433 33.378 18.806 71.606 40.361l69.505 39.192-0.0806 82.992-0.0806 82.992-15.84 8.9675c-16.981 9.6135-40.253 22.817-57.6 32.679-5.896 3.3522-12.622 7.218-14.946 8.5909-2.3244 1.3728-5.6364 3.242-7.36 4.1538-1.7236 0.91176-3.8538 2.1225-4.7338 2.6905-2.1205 1.3687-10.635 6.2294-10.912 6.2294-0.11965 0-0.95347 0.4559-1.8529 1.0131-0.89947 0.55722-4.2274 2.5026-7.3954 4.3232-3.168 1.8205-5.976 3.4495-6.24 3.62-0.264 0.17046-1.38 0.81706-2.48 1.4368-1.1 0.61978-2.9488 1.6668-4.1086 2.3268-1.1597 0.66-2.9237 1.6411-3.92 2.1802-0.99629 0.53911-2.5169 1.3671-3.3791 1.84-0.86221 0.47287-1.7982 0.85284-2.08 0.84436-0.28178-8e-3 -8.0003-4.3035-17.152-9.5445z" fill="#628eb9"/>
+  <path d="m155.66 327.1c-9.1418-5.2332-40.993-23.464-70.781-40.512l-54.16-30.997 0.0034-82.917 0.0034-82.916 69.948-39.84c38.471-21.912 70.35-40.039 70.842-40.282 0.87598-0.4327 2.3721 0.39113 72.289 39.805l71.394 40.247 0.0491 35.555c0.027 19.555 0.0797 54.563 0.11717 77.795 0.0374 23.232-0.0207 44.602-0.12915 47.488l-0.19717 5.2479-24 13.603c-13.2 7.4817-43.466 24.632-67.258 38.112s-43.659 24.833-44.149 25.229c-0.49054 0.396-1.1198 0.72-1.3984 0.72-0.27859 0-0.64333 0.12304-0.81053 0.27338-0.1672 0.15036-1.3924 0.86521-2.7226 1.5885l-2.4186 1.3151z" fill="#0073bd"/>
+  <path d="m178.7 332.95c0.33915-0.25658 0.69498-0.38816 0.79073-0.29242 0.20086 0.20087-0.61187 0.77207-1.0874 0.76423-0.176-3e-3 -0.0425-0.2152 0.29664-0.47181zm-71.097-47.105-64.4-37.003v-151.76l64.525-37.211c49.387-28.481 64.658-37.148 65.09-36.943 0.3103 0.14773 29.809 16.901 65.553 37.229l64.989 36.96 6.4e-4 75.84 6.4e-4 75.84-65.283 36.96c-35.906 20.328-65.461 36.989-65.679 37.025s-29.376-16.586-64.796-36.937zm13.041-29.231c3.8025-0.48409 9.2453-2.0461 13.058-3.7475 3.2004-1.4281 23.106-12.671 27.227-15.379 4.7612-3.1279 6.8245-9.3627 4.7522-14.36-1.7019-4.1042-4.5322-6.7893-8.1785-7.7588-2.0724-0.55102-5.8035-0.45862-7.4489 0.18447-0.776 0.3033-6.653 3.5694-13.06 7.258-6.407 3.6886-12.396 7.0484-13.309 7.4663-5.5116 2.5233-13.111 2.7508-18.666 0.55885-3.7691-1.4872-6.0089-2.8973-8.8041-5.5425-3.523-3.3339-5.7289-7.1535-6.9397-12.017-0.62704-2.5183-0.69522-3.4225-0.54292-7.2 0.15265-3.7861 0.29819-4.6608 1.1775-7.0777 1.4656-4.028 3.7338-7.5069 6.6567-10.21 2.199-2.0333 4.989-3.7431 24.877-15.245 12.32-7.1252 23.361-13.444 24.535-14.043 3.4213-1.7434 6.8171-2.4884 11.305-2.4803 3.5247 6e-3 5.697 0.31387 7.36 1.0419 4.3549 1.9066 5.7464 2.6431 7.52 3.9801 2.4515 1.848 4.0669 3.5358 5.6976 5.953 2.0749 3.0757 2.941 4.0811 4.6544 5.403 3.432 2.6478 8.3234 2.9627 12.827 0.82575 1.8109-0.85933 4.3727-3.5568 5.4595-5.7487 0.8729-1.7605 0.95217-2.1991 0.95432-5.28 2e-3 -3.1387-0.0658-3.4995-1.0316-5.4786-3.7748-7.7345-13.072-16.319-22.641-20.905-2.689-1.2889-9.7494-3.5358-12.64-4.0227-3.3571-0.56546-11.936-0.66217-15.2-0.17135-5.6085 0.84343-10.421 2.3034-15.106 4.583-3.8856 1.8905-47.814 27.455-50.703 29.507-14.242 10.117-22.168 27.534-20.461 44.962 0.63828 6.5138 2.0207 11.721 4.547 17.126 1.0413 2.2281 4.0964 7.3637 4.759 8 0.09164 0.088 0.59582 0.736 1.1204 1.44 2.909 3.9039 8.5259 8.7077 13.993 11.968 3.7707 2.2483 6.1335 3.2831 10.561 4.6253 7.6063 2.3059 13.671 2.8046 21.689 1.7838zm75.839-43.846c1.144-0.16217 2.296-0.33976 2.56-0.39465 3.8475-0.79992 6.7718-1.7107 10.23-3.1864 2.7942-1.1922 47.608-26.838 51.419-29.426 7.2548-4.9259 13.036-11.489 16.704-18.964 2.3914-4.8727 3.0244-6.5653 4.0093-10.72 1.2992-5.4808 1.5906-8.3212 1.3644-13.299-0.46696-10.277-2.6063-17.002-8.2906-26.061-2.5779-4.1083-5.1052-6.9983-9.3742-10.719-3.2507-2.8334-11.058-7.3237-15.023-8.6401-6.7299-2.2346-9.327-2.7103-15.68-2.8722-4.7897-0.12208-6.4269-0.02573-10.56 0.62156-3.2658 0.51144-9.655 2.6674-13.302 4.4884-0.95599 0.47738-6.9066 3.8411-13.223 7.4749-12.933 7.4397-14.292 8.4584-16.061 12.037-1.0353 2.0949-1.1544 2.6029-1.1744 5.0106-0.0187 2.249 0.11129 2.962 0.80921 4.4394 1.7987 3.8075 3.4003 5.5151 6.5062 6.937 2.2445 1.0276 5.7627 1.2549 8.4356 0.54497 1.13-0.30012 5.9986-2.9205 13.738-7.3941 15.649-9.0456 16.762-9.4902 23.712-9.4742 3.6949 9e-3 4.2319 0.0904 7.04 1.0726 5.3196 1.8607 8.7146 4.0602 11.701 7.5809 4.9675 5.8554 6.8721 13.311 5.4595 21.373-0.91435 5.218-4.3667 11.066-8.5085 14.412-1.2394 1.0014-11.971 7.4104-25.464 15.208-26.562 15.349-26.552 15.344-32.824 15.59-4.7001 0.18411-7.8561-0.41643-11.47-2.1827-5.1688-2.5259-8.2052-5.1439-10.972-9.4605-1.5439-2.4087-4.0922-4.8939-6.1447-5.9928-1.117-0.59799-1.8068-0.69352-4.9767-0.68923-3.42 5e-3 -3.8104 0.0707-5.525 0.93437-2.3032 1.1602-4.8051 3.7698-5.9245 6.1796-0.74464 1.603-0.85618 2.2547-0.86284 5.041-9e-3 3.7824 0.47917 5.0724 3.6985 9.7728 5.1463 7.5138 15.38 15.413 23.334 18.012 6.7656 2.2105 7.4278 2.3693 12.273 2.9418 3.6154 0.42722 8.5191 0.34952 12.367-0.19598zm118.87-23.331c0-9.944 0.0406-14.012 0.0902-9.04s0.0496 13.108 0 18.08-0.0902 0.904-0.0902-9.04z" fill="#064d90"/>
+  <path d="m178.56 333.12c0.264-0.17062 0.624-0.31021 0.8-0.31021s0.104 0.13959-0.16 0.31021-0.624 0.31021-0.8 0.31021-0.104-0.13959 0.16-0.31021zm-52.837-36.988c-25.588-14.693-54.619-31.354-64.515-37.024l-17.992-10.309 0.0026-75.84 0.0026-75.84 62.736-36.16c34.505-19.888 63.558-36.624 64.562-37.191l1.8254-1.0311 130.86 74.398v151.66l-64 36.254c-35.2 19.94-64.665 36.601-65.477 37.024l-1.477 0.76995zm-5.163-39.312c0.572-0.0991 1.436-0.23863 1.92-0.31011 3.108-0.45901 8.7511-2.2843 12.522-4.0502 4.9684-2.3268 25.625-14.436 27.446-16.089 2.2893-2.0788 3.8762-5.2839 4.1442-8.3706 0.16797-1.934 0.0794-2.5438-0.64836-4.4637-2.4302-6.411-7.7037-9.6278-13.932-8.4985-2.5617 0.46452-2.2569 0.30741-16.242 8.3715-12.775 7.3661-15.129 8.3965-20.049 8.7733-7.328 0.56126-14.855-2.2213-19.881-7.3498-5.9243-6.0451-8.1778-13.52-6.7329-22.334 0.99882-6.0925 5.2901-12.661 10.508-16.085 3.9211-2.5727 46.102-26.737 47.991-27.493 3.2092-1.2839 5.8633-1.7921 9.3542-1.7912 4.0614 9.6e-4 6.8116 0.58696 10.193 2.1715 5.149 2.4129 8.3529 5.1503 11.226 9.5914 1.8463 2.8541 4.5818 5.3092 6.8869 6.181 2.4238 0.91665 7.0934 0.74019 9.4007-0.35524 2.3941-1.1367 4.8424-3.5726 6.046-6.0156 0.90428-1.8354 1.0624-2.522 1.1546-5.0149 0.12936-3.4958-0.44261-5.2842-2.8896-9.0348-4.1813-6.4089-9.8138-11.657-17.161-15.99-7.1273-4.2034-14.497-6.275-23.618-6.639-6.6909-0.26702-15.041 1.3473-20.918 4.044-0.792 0.36341-1.8937 0.85265-2.4483 1.0872-1.8478 0.7815-45.981 26.377-49.224 28.547-1.7556 1.1753-4.6685 3.5561-6.473 5.2907-7.2447 6.964-11.928 15.19-14.243 25.021-1.021 4.3348-1.051 4.6618-1.0256 11.185 0.02208 5.6666 0.1381 7.2467 0.74014 10.08 3.073 14.462 11.205 25.621 24.354 33.416 2.6589 1.5765 5.8959 2.981 9.28 4.0266 1.848 0.57096 3.5836 1.1473 3.8568 1.2809 0.27324 0.13351 1.7852 0.40367 3.36 0.60037 1.5748 0.19669 3.2232 0.41188 3.6632 0.4782 1.1048 0.16651 10.164-0.0411 11.44-0.26216zm74.96-43.715c2.9786-0.37595 4.2374-0.64098 8-1.6843 5.2358-1.4518 7.7616-2.7311 24.309-12.312 8.3663-4.8441 18.811-10.891 23.211-13.438s9.008-5.3274 10.24-6.1789c11.376-7.863 18.728-19.699 21.149-34.049 0.87439-5.1818 0.44474-13.541-1.0052-19.557-1.8503-7.6767-7.1617-17.135-12.738-22.682-9.815-9.7644-22.816-15.16-36.206-15.025-7.8125 0.07872-15.284 1.915-22.404 5.5064-4.5951 2.3177-24.281 13.848-25.706 15.056-2.9127 2.47-4.7955 7.0316-4.413 10.692 0.2246 2.1489 1.8042 5.4947 3.5472 7.5137 1.8264 2.1155 4.639 3.3364 8.1474 3.5364 2.1457 0.12234 3.2709 0.0321 4.4632-0.35785 0.8768-0.28679 7.0416-3.6646 13.7-7.5063 12.435-7.175 14.129-8.0216 17.705-8.8484 3.5059-0.81054 10.313-0.55721 12.64 0.47039 0.352 0.15546 1.5182 0.62536 2.5916 1.0442 6.4442 2.5147 12.015 8.6963 14.197 15.755 1.4586 4.7174 1.1309 11.817-0.76016 16.471-1.8231 4.4867-5.7572 9.3458-9.4582 11.682-5.1502 3.2512-46.61 26.951-48.183 27.543-6.294 2.369-12.794 2.2482-18.728-0.34812-4.4732-1.9571-8.6052-5.3296-11.092-9.0532-2.6318-3.9405-4.6266-5.8474-7.2634-6.9436-1.6232-0.6748-4.3696-0.87299-6.5693-0.4741-4.0244 0.72976-6.9074 2.8371-8.8464 6.4663-1.2337 2.3092-1.2482 2.3772-1.2482 5.8564 0 3.314 0.0602 3.643 1.0286 5.6219 3.0158 6.1626 9.9029 13.26 17.611 18.148 5.2104 3.3046 12.886 6.0983 18.961 6.9015 3.7694 0.49838 9.9788 0.58992 13.12 0.19343z" fill="#0a3d81"/>
+  <path d="m178.56 333.12c0.264-0.17062 0.624-0.31021 0.8-0.31021s0.104 0.13959-0.16 0.31021-0.624 0.31021-0.8 0.31021-0.104-0.13959 0.16-0.31021zm-51.697-36.376c-24.982-14.348-53.991-31.009-64.463-37.024l-19.04-10.937v-151.64l64.475-37.183c57.53-33.177 64.549-37.143 65.158-36.812 0.37584 0.20399 29.79 16.926 65.365 37.16l64.682 36.789v151.75l-64.37 36.454c-35.403 20.05-64.823 36.695-65.377 36.99l-1.0077 0.53549zm-6.3827-39.921c3.5511-0.50499 3.6793-0.53097 6.4-1.2972 6.1679-1.7369 7.8837-2.5789 22.937-11.256 12.458-7.1812 13.824-8.1918 15.382-11.383 2.3769-4.8687 1.8345-9.167-1.7467-13.843-1.083-1.4139-1.8733-2.0406-3.7551-2.9772-2.2488-1.1194-2.5561-1.1864-5.44-1.1864-1.8812 0-3.4873 0.17296-4.1766 0.4498-0.616 0.24739-6.4968 3.5453-13.068 7.3287-12.153 6.9966-14.1 7.9767-17.653 8.8869-3.4513 0.88417-11.732 0.58824-13.279-0.47456-0.176-0.12091-1.184-0.5589-2.24-0.97332-8.4178-3.3035-14.559-11.884-15.087-21.079-0.20199-3.5173 0.38276-7.7989 1.4513-10.627 1.5757-4.1697 4.8108-8.4364 8.476-11.179 2.381-1.7814 46.375-27.166 48.852-28.188 4.4932-1.8534 9.9188-2.4012 14.388-1.4525 2.5665 0.54482 7.2994 2.5581 9.44 4.0157 2.2884 1.5581 5.1948 4.6026 6.976 7.3073 1.8432 2.7988 4.0274 4.9393 5.9816 5.8619 1.3724 0.64791 4.4236 1.2564 5.4424 1.0853 0.264-0.0443 1.2-0.18469 2.08-0.3119 6.3468-0.91744 11.117-7.6557 10.062-14.211-0.39014-2.4225-0.98586-3.8328-2.7877-6.5995-5.6098-8.6139-15.711-16.659-24.714-19.682-6.8735-2.3083-9.7538-2.8435-16.32-3.0323-8.1014-0.23298-16.408 1.5952-23.459 5.1631-2.5446 1.2876-46.904 26.95-48.537 28.078-8.9208 6.169-15.413 14.253-19.237 23.954-2.8036 7.1128-3.8714 17.142-2.7 25.359 1.5924 11.17 6.4131 20.609 14.561 28.508 2.9664 2.8761 4.8907 4.3509 9.047 6.9337 4.3353 2.694 6.8667 3.7811 13.204 5.6704 5.0764 1.5133 13.483 2.0086 19.52 1.15zm74.88-43.706c5.1289-0.48179 10.195-1.9085 15.36-4.3259 3.3992-1.5909 47.206-26.92 50.857-29.406 3.668-2.4972 9.3718-8.1599 12.027-11.941 5.9939-8.5337 9.276-18.628 9.276-28.528 0-11.607-2.3635-19.235-9.2279-29.784-1.7429-2.6783-6.5471-7.8151-9.3321-9.9781-5.1899-4.0309-10.125-6.669-15.84-8.4667-1.936-0.60904-3.9634-1.2474-4.5052-1.4185-2.838-0.89641-11.96-1.4052-16.455-0.91776-5.0024 0.54257-11.346 2.3091-15.68 4.3664-4.5337 2.1522-26.239 14.864-27.844 16.306-1.0668 0.95916-1.9807 2.2244-2.7912 3.8643-1.1266 2.2794-1.2051 2.628-1.2051 5.3506 0 2.684 0.0862 3.0822 1.0991 5.0788 0.60449 1.1915 1.6383 2.7789 2.2973 3.5276 1.2752 1.4487 4.1964 3.1717 6.0397 3.5624 2.03 0.43024 5.5625 0.22679 7.2249-0.41613 0.86846-0.33586 6.5722-3.5055 12.675-7.0436 13.475-7.8122 13.62-7.8863 17.307-8.8743 4.6847-1.2551 9.8914-1.0685 14.243 0.5104 8.8067 3.195 14.73 9.8624 16.526 18.602 0.62287 3.0307 0.63627 5.9564 0.0438 9.5696-0.98476 6.0061-4.6535 11.897-9.7769 15.698-1.9731 1.464-46.768 27.21-48.727 28.006-3.2689 1.3283-5.5787 1.7656-9.3295 1.7663-4.0354 6.4e-4 -6.6845-0.52381-9.8739-1.9553-4.9474-2.2206-8.7826-5.5147-11.871-10.197-1.587-2.4055-4.2448-4.8476-6.2472-5.7403-1.8375-0.81909-7.076-0.72884-9.325 0.16066-2.505 0.99079-5.0885 3.3985-6.3102 5.8808-1.9115 3.884-1.9542 7.7321-0.12852 11.588 1.8304 3.8658 7.8021 10.946 12.053 14.29 2.286 1.7985 8.2666 5.587 10.438 6.6119 2.6147 1.2343 9.643 3.4359 12.442 3.8975 1.144 0.18864 3.448 0.43578 5.12 0.5492s3.0658 0.22626 3.0974 0.25077c0.0944 0.0732 2.9331-0.12696 6.3426-0.44724z" fill="#0b2f6e"/>
+  <path d="m178.79 333.08c0.21707-0.21869 0.47528-0.31703 0.5738-0.2185 0.0985 0.0985-0.0791 0.27747-0.39466 0.39763-0.45607 0.17367-0.49282 0.1369-0.17914-0.17913zm-54.315-37.764c-26.312-15.118-55.292-31.766-64.4-36.994l-16.56-9.5068 7.11e-4 -75.848 7.13e-4 -75.848 64.079-36.956c35.244-20.326 64.367-36.957 64.719-36.958 0.352-8.32e-4 29.904 16.629 65.67 36.956l65.03 36.958 7e-3 75.84 7e-3 75.84-65 36.8c-35.75 20.24-65.161 36.891-65.357 37.002-0.1969 0.1111-21.886-12.167-48.198-27.286zm-4.4667-38.355c4.1068-0.47783 9.8571-2.0634 13.689-3.7745 3.3096-1.4778 26.013-14.387 28.159-16.012 2.6654-2.0175 4.8589-6.618 4.8589-10.191 0-2.254-1.4128-5.7034-3.3402-8.1551-2.1031-2.6752-4.6911-3.8787-8.7197-4.0549-2.3397-0.10233-3.3388-6e-3 -4.5126 0.4359-0.82915 0.31203-6.6195 3.5221-12.868 7.1335s-12.368 7.0292-13.6 7.5952c-3.0089 1.3823-6.2363 2.0614-9.797 2.0614-3.8749 0-6.4826-0.48022-9.5676-1.7619-10.774-4.4762-16.734-14.626-15.356-26.153 0.70321-5.8836 3.3097-11.002 7.6429-15.009 2.1471-1.9853 5.1091-3.8004 24.838-15.221 12.32-7.1316 23.552-13.511 24.96-14.176 3.6875-1.7423 6.3914-2.3435 10.56-2.3482 3.276-4e-3 6.651 0.46365 7.8209 1.0829 0.27449 0.14528 1.5563 0.73277 2.8484 1.3055 4.3116 1.9112 7.8018 4.9697 10.856 9.5134 2.0291 3.0183 4.8225 5.4974 6.8946 6.1189 7.6598 2.2973 14.989-2.1908 16.547-10.133 0.76318-3.8907-0.48757-7.5634-4.3517-12.778-2.4538-3.3116-5.1524-6.1558-8.2953-8.7429-2.2222-1.8292-6.9609-5.1386-7.3579-5.1386-0.11374 0-0.80875-0.40224-1.5445-0.89387-1.5332-1.0245-2.4315-1.4397-5.3376-2.4668-4.0408-1.4281-7.2275-2.3398-9.6-2.7464-2.6008-0.44577-11.659-0.58688-15.079-0.23492-3.1807 0.32735-9.0309 1.9677-12.878 3.6109-3.9367 1.6815-48.711 27.433-53.167 30.578-9.3672 6.6123-16.65 17.569-19.351 29.111-1.3092 5.5951-1.5302 8.0495-1.2538 13.922 0.37281 7.9226 1.65 13.074 4.9506 19.968 4.0418 8.4422 10.585 15.494 19.338 20.84 3.5385 2.1613 5.081 2.9172 8 3.9208 5.6534 1.9436 9.0596 2.7058 13.44 3.0075 1.496 0.10304 2.7637 0.22411 2.8171 0.26904 0.16922 0.14235 4.917-0.15435 7.7562-0.48469zm75.187-43.715c3.3928-0.39536 3.9111-0.49324 7.3972-1.3969 2.883-0.7473 6.5656-2.1624 9.5628-3.6747 2.8657-1.4459 46.47-26.692 48.604-28.14 6.2537-4.2457 11.95-10.379 15.632-16.83 2.9658-5.1962 5.1788-11.72 6.189-18.244 0.5624-3.6322 0.19549-13.46-0.64511-17.28-1.3632-6.1943-2.6685-9.3874-6.4203-15.706-2.3807-4.0094-4.182-6.3534-7.1213-9.2673-3.7406-3.7082-9.6434-7.8627-14.299-10.064-2.2742-1.0752-10.211-3.5658-12.66-3.9727-9.9971-1.6612-20.666-0.18706-29.76 4.1118-3.5543 1.6803-25.64 14.363-27.285 15.668-1.4569 1.1562-3.1297 3.7784-3.9354 6.1689-1.0484 3.1107-0.80453 5.8797 0.79533 9.0317 1.5891 3.1307 3.2279 4.8085 5.8992 6.0394 1.8498 0.85236 2.4998 0.98279 5.0059 1.0044 2.1134 0.0183 3.3059-0.13808 4.48-0.58741 0.88-0.33677 6.64-3.5224 12.8-7.0791 13.293-7.6753 13.802-7.9394 17.021-8.8369 1.9644-0.54769 3.3546-0.69538 6.499-0.69042 4.9645 8e-3 7.0014 0.50599 11.649 2.849 6.7402 3.3979 11.513 9.8557 12.859 17.4 0.54842 3.0736 0.54821 4.9089-9.6e-4 8.4895-0.96673 6.3033-4.2315 11.833-9.2002 15.582-2.3581 1.7795-44.807 26.451-48.416 28.14-4.5278 2.1187-10.525 2.7746-15.664 1.713-4.1197-0.85095-9.8421-4.0722-12.872-7.2459-0.72954-0.76412-2.0576-2.4884-2.9512-3.8317-1.7567-2.6407-4.2025-5.0388-6.2657-6.1434-1.1174-0.59821-1.8057-0.69339-4.9767-0.68827-3.3538 5e-3 -3.8392 0.0836-5.4758 0.88232-4.8689 2.3761-8.0246 8.571-7.0154 13.772 1.6832 8.6737 15.402 21.888 26.731 25.748 6.3043 2.148 8.0522 2.5373 15.2 3.3861 1.2152 0.1443 6.322-0.0376 8.64-0.30768z" fill="#121657"/>
+  <path d="m126.95 296.68c-24.775-14.227-45.124-25.944-45.218-26.039-0.19186-0.19187-8.2563-24.115-9.898-29.362-0.57816-1.848-1.9326-5.9376-3.0098-9.0881-1.0772-3.1504-1.9123-5.7744-1.8557-5.8309 0.05656-0.0566 0.50549 0.79308 0.99762 1.8881 3.9329 8.7509 10.68 16.29 19.467 21.753 3.9857 2.4779 5.6638 3.235 11.17 5.0392 5.0976 1.6704 8.2763 2.2092 14.033 2.3787 3.2834 0.0967 9.6016-0.43262 11.193-0.93758 0.32797-0.1041 0.87518-0.24297 1.216-0.30861 1.6888-0.32524 5.653-1.6594 8.3816-2.8209 1.6774-0.71402 8.6658-4.5235 15.53-8.4656 13.773-7.9099 14.759-8.6559 16.614-12.567 0.88647-1.8696 0.98623-2.3875 0.98623-5.12 0-2.7944-0.086-3.2159-1.0643-5.2167-1.5039-3.0757-4.1151-5.6457-6.7136-6.6077-2.6553-0.98303-6.5224-1.0296-8.8962-0.10708-0.89873 0.34927-7.0187 3.7458-13.6 7.5478-14.9 8.608-15.843 8.9819-22.686 8.9962-4.2943 9e-3 -4.8308-0.058-7.2-0.89791-5.5178-1.9562-9.3678-4.5932-12.442-8.5218-3.9997-5.1118-5.7513-11.425-4.9884-17.981 0.42043-3.613 0.97074-5.4365 2.5868-8.5716 1.6492-3.1993 3.6272-5.6535 6.2665-7.7749 2.329-1.872 46.796-27.733 49.537-28.809 3.8954-1.5298 5.5504-1.8014 10.4-1.7068 4.4984 0.0878 4.7318 0.12675 7.6487 1.2771 5.743 2.265 9.5464 5.2635 12.713 10.023 1.9882 2.9881 4.356 5.2766 6.5181 6.2998 1.1833 0.55995 2.039 0.68183 4.8 0.68361 3.1371 2e-3 3.5002-0.0663 5.4741-1.0297 3.3998-1.6593 5.863-4.7882 6.8729-8.7303 1.2684-4.9512-0.35946-9.2366-6.107-16.077-2.572-3.0609-3.7282-4.2173-6.5075-6.5083l-2.3475-1.935-1.4216-4.1363c-2.8848-8.3934-4.6031-13.683-4.4836-13.803 0.0675-0.0675 0.55794 0.64839 1.0898 1.5909 1.9976 3.5396 5.1618 5.3387 9.8427 5.5964 2.5446 0.14009 3.1217 0.066 4.8-0.6166 1.0492-0.42672 7.1637-3.8039 13.588-7.5049 12.311-7.0925 13.763-7.826 17.014-8.5922 3.4029-0.80221 10.692-0.55524 12.906 0.43729 0.352 0.15778 1.4293 0.5962 2.3941 0.97427 6.4285 2.5192 12.051 8.5575 14.078 15.119 1.5465 5.0069 1.465 11.512-0.19978 15.939-1.806 4.8033-5.5647 9.5907-9.7118 12.37-0.792 0.53077-11.52 6.7804-23.84 13.888-26.639 15.369-26.249 15.191-33.28 15.154-5.2371-0.0272-6.7766-0.37634-11.072-2.5107-4.4162-2.1944-7.3147-4.724-10.002-8.7292-1.7502-2.6084-2.4699-3.4235-4.3936-4.976-2.0929-1.6891-3.7945-2.2275-7.0122-2.2187-4.9449 0.0135-8.8012 2.2474-10.955 6.3463-3.033 5.7719-2.2334 10.518 2.9292 17.386 2.1632 2.878 6.1142 6.9924 8.8597 9.226 2.346 1.9086 7.2746 5.0348 9.9075 6.2842 3.4181 1.622 10.818 3.9016 14.059 4.3307 5.8606 0.77607 13.337 0.57808 17.92-0.47454 3.1808-0.73055 8.0537-2.3464 10.4-3.4487 4.0223-1.8896 49.258-28.109 52.111-30.204 7.7668-5.7042 14.4-14.539 17.733-23.621 0.7498-2.0426 2.0882-7.3736 2.6513-10.56 0.50265-2.8447 0.1334-12.301-0.66224-16.96-0.16736-0.97992 2.6949 6.7653 4.3163 11.68 0.49356 1.496 4.4459 13.448 8.783 26.56l7.8856 23.84-0.045 58.88-65.207 36.88c-35.864 20.284-65.342 36.877-65.507 36.874-0.16469-3e-3 -20.57-11.646-45.346-25.873z" fill="#0d1041"/>
+ </g>
+</svg>
diff --git a/script/makeinstaller.sh b/script/makeinstaller.sh
index eff94639..1e84dc88 100755
--- a/script/makeinstaller.sh
+++ b/script/makeinstaller.sh
@@ -5,6 +5,8 @@ set -e # stop on error
 
 command -v makensis > /dev/null 2>&1 || { echo >&2 "makensis is required to build the installer. Aborting."; exit 1; }
 command -v pynsist > /dev/null 2>&1 || { echo >&2 "pynsist is required to build the installer. Aborting."; exit 1; }
+command -v convert > /dev/null 2>&1 || { echo >&2 "imagemagick is required to build the installer. Aborting."; exit 1; }
+command -v inkscape > /dev/null 2>&1 || { echo >&2 "inkscape is required to build the installer. Aborting."; exit 1; }
 
 # For travis nightly builds generate a version number with commit hash
 STREAMLINK_VERSION=$(python setup.py --version)
@@ -18,9 +20,10 @@ build_dir="$(pwd)/build"
 build_dir_plugins="${build_dir}/lib/streamlink/plugins"
 nsis_dir="${build_dir}/nsis"
 files_dir="${build_dir}/files"
+icons_dir="${files_dir}/icons"
 # get the dist directory from an environment variable, but default to the build/nsis directory
 dist_dir="${STREAMLINK_INSTALLER_DIST_DIR:-$nsis_dir}"
-mkdir -p "${build_dir}" "${dist_dir}" "${nsis_dir}" "${files_dir}"
+mkdir -p "${build_dir}" "${dist_dir}" "${nsis_dir}" "${files_dir}" "${icons_dir}"
 
 echo "Building streamlink-${STREAMLINK_VERSION} package..." 1>&2
 python setup.py build 1>&2
@@ -43,6 +46,14 @@ do
     touch "${build_dir_plugins}/$i.py"
 done
 
+echo "Creating images" 1>&2
+# Create images
+for size in 16 32 48 256; do
+  inkscape --without-gui --export-png="${icons_dir}/icon-${size}.png" -w ${size} -h ${size} icon.svg
+done
+convert "${icons_dir}"/icon-{16,32,48,256}.png "${icons_dir}/icon.ico"
+
+
 echo "Building ${STREAMLINK_INSTALLER} installer..." 1>&2
 
 cat > "${build_dir}/streamlink.cfg" <<EOF
@@ -50,7 +61,7 @@ cat > "${build_dir}/streamlink.cfg" <<EOF
 name=Streamlink
 version=${STREAMLINK_VERSION}
 entry_point=streamlink_cli.main:main
-icon=../win32/doggo.ico
+icon=${icons_dir}/icon.ico
 
 [Python]
 version=3.6.6
diff --git a/setup.py b/setup.py
index 54007193..2ec570ab 100644
--- a/setup.py
+++ b/setup.py
@@ -13,8 +13,8 @@ deps = [
     'futures;python_version<"3.0"',
     # Require singledispatch on Python <3.4
     'singledispatch;python_version<"3.4"',
-    "requests>=2.2,!=2.12.0,!=2.12.1,!=2.16.0,!=2.16.1,!=2.16.2,!=2.16.3,!=2.16.4,!=2.16.5,!=2.17.1,<3.0",
-    'urllib3[secure]<1.23,>=1.21.1;python_version<"3.0"',
+    "requests>=2.21.0,<3.0",
+    'urllib3[secure]>=1.23;python_version<"3.0"',
     "isodate",
     "websocket-client",
     # Support for SOCKS proxies
diff --git a/src/streamlink/logger.py b/src/streamlink/logger.py
index 4e13997c..57421948 100644
--- a/src/streamlink/logger.py
+++ b/src/streamlink/logger.py
@@ -96,7 +96,10 @@ root.setLevel(logging.WARNING)
 
 class StringFormatter(logging.Formatter):
     def __init__(self, fmt, datefmt=None, style='%', remove_base=None):
-        super(StringFormatter, self).__init__(fmt, datefmt=datefmt)
+        if is_py2:
+            super(StringFormatter, self).__init__(fmt, datefmt=datefmt)
+        else:
+            super(StringFormatter, self).__init__(fmt, datefmt=datefmt, style=style)
         if style not in ("{", "%"):
             raise ValueError("Only {} and % formatting styles are supported")
         self.style = style
diff --git a/src/streamlink/plugin/api/useragents.py b/src/streamlink/plugin/api/useragents.py
index 584ec86f..04fa94fd 100644
--- a/src/streamlink/plugin/api/useragents.py
+++ b/src/streamlink/plugin/api/useragents.py
@@ -1,22 +1,18 @@
 ANDROID = ("Mozilla/5.0 (Linux; Android 7.1.1; SM-J510FN Build/NMF26X) "
            "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Mobile Safari/537.36")
-CHROME = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
-          "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36")
+CHROME = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36"
 CHROME_OS = ("Mozilla/5.0 (X11; CrOS armv7l 10718.34.0) "
              "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.40 Safari/537.36")
-EDGE = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
-        "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134")
-FIREFOX = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0"
-FIREFOX_MAC = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:61.0) Gecko/20100101 Firefox/61.0"
+EDGE = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763"
+FIREFOX = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0"
+FIREFOX_MAC = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:64.0) Gecko/20100101 Firefox/64.0"
 IE_6 = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; WOW64; Trident/4.0; SLCC1)"
 IE_7 = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64; Trident/4.0; SLCC1)"
 IE_8 = "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; WOW64; Trident/4.0; SLCC1)"
 IE_9 = "Mozilla/5.0 (MSIE 9.0; Windows NT 6.1; Trident/5.0)"
 IE_11 = "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"
-IPAD = ("Mozilla/5.0 (iPad; CPU OS 11_4 like Mac OS X) "
-        "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1")
-IPHONE = IPHONE_6 = ("Mozilla/5.0 (iPhone; CPU iPhone OS 11_4 like Mac OS X) "
-                     "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1")
+IPAD = "Mozilla/5.0 (iPad; CPU OS 12_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1"
+IPHONE = IPHONE_6 = "Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1"
 OPERA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
          "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36 OPR/54.0.2952.64")
 SAFARI = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) "
diff --git a/src/streamlink/plugin/api/utils.py b/src/streamlink/plugin/api/utils.py
index f6f3f121..ca9c1893 100644
--- a/src/streamlink/plugin/api/utils.py
+++ b/src/streamlink/plugin/api/utils.py
@@ -7,9 +7,9 @@ from ...utils import parse_qsd as parse_query, parse_json, parse_xml
 __all__ = ["parse_json", "parse_xml", "parse_query"]
 
 
-tag_re = re.compile('''(?=<(?P<tag>[a-zA-Z]+)(?P<attr>.*?)(?P<end>/)?>(?:(?P<inner>.*?)</\s*(?P=tag)\s*>)?)''',
+tag_re = re.compile(r'''(?=<(?P<tag>[a-zA-Z]+)(?P<attr>.*?)(?P<end>/)?>(?:(?P<inner>.*?)</\s*(?P=tag)\s*>)?)''',
                     re.MULTILINE | re.DOTALL)
-attr_re = re.compile('''\s*(?P<key>[\w-]+)\s*(?:=\s*(?P<quote>["']?)(?P<value>.*?)(?P=quote)\s*)?''')
+attr_re = re.compile(r'''\s*(?P<key>[\w-]+)\s*(?:=\s*(?P<quote>["']?)(?P<value>.*?)(?P=quote)\s*)?''')
 Tag = namedtuple("Tag", "tag attributes text")
 
 
@@ -26,4 +26,3 @@ def itertags(html, tag):
         if match.group("tag") == tag:
             attrs = dict((a.group("key").lower(), a.group("value")) for a in attr_re.finditer(match.group("attr")))
             yield Tag(match.group("tag"), attrs, match.group("inner"))
-
diff --git a/src/streamlink/plugins/afreeca.py b/src/streamlink/plugins/afreeca.py
index a9fec3f6..319bc4f4 100644
--- a/src/streamlink/plugins/afreeca.py
+++ b/src/streamlink/plugins/afreeca.py
@@ -19,7 +19,7 @@ QUALITY_WEIGHTS = {
     "sd": 480
 }
 
-_url_re = re.compile(r"http(s)?://(?P<cdn>\w+\.)?afreeca(tv)?\.com/(?P<username>\w+)(/\d+)?")
+_url_re = re.compile(r"https?://play\.afreecatv\.com/(?P<username>\w+)(?:/\d+)?")
 
 _channel_schema = validate.Schema(
     {
@@ -86,10 +86,6 @@ class AfreecaTV(Plugin):
         return self.session.http.json(res, schema=_channel_schema)
 
     def _get_hls_key(self, broadcast, username, quality):
-        headers = {
-            "Referer": self.url
-        }
-
         data = {
             "bid": username,
             "bno": broadcast,
@@ -97,7 +93,7 @@ class AfreecaTV(Plugin):
             "quality": quality,
             "type": "pwd"
         }
-        res = self.session.http.post(CHANNEL_API_URL, data=data, headers=headers)
+        res = self.session.http.post(CHANNEL_API_URL, data=data)
         return self.session.http.json(res, schema=_channel_schema)
 
     def _get_stream_info(self, broadcast, quality, cdn, rmd):
@@ -139,6 +135,7 @@ class AfreecaTV(Plugin):
             return False
 
     def _get_streams(self):
+        self.session.http.headers.update({"Referer": self.url})
         if not self.session.get_option("hls-segment-ignore-names"):
             ignore_segment = ["preloading"]
             self.session.set_option("hls-segment-ignore-names", ignore_segment)
diff --git a/src/streamlink/plugins/bbciplayer.py b/src/streamlink/plugins/bbciplayer.py
index 8c79baf5..5e5d0f28 100644
--- a/src/streamlink/plugins/bbciplayer.py
+++ b/src/streamlink/plugins/bbciplayer.py
@@ -45,11 +45,11 @@ class BBCiPlayer(Plugin):
 
     mediator_schema = validate.Schema(
         {
-            "episode": {
+            "appStoreState": {
                 "versions": [{"id": validate.text}]
             }
         },
-        validate.get("episode"), validate.get("versions"), validate.get(0),
+        validate.get("appStoreState"), validate.get("versions"), validate.get(0),
         validate.get("id")
     )
     mediaselector_schema = validate.Schema(
diff --git a/src/streamlink/plugins/cdnbg.py b/src/streamlink/plugins/cdnbg.py
index 2c15eb2f..dfb5591d 100644
--- a/src/streamlink/plugins/cdnbg.py
+++ b/src/streamlink/plugins/cdnbg.py
@@ -1,5 +1,4 @@
-from __future__ import print_function
-
+import logging
 import re
 
 from streamlink.compat import urlparse
@@ -9,6 +8,8 @@ from streamlink.plugin.api import validate
 from streamlink.stream import HLSStream
 from streamlink.utils import update_scheme
 
+log = logging.getLogger(__name__)
+
 
 class CDNBG(Plugin):
     url_re = re.compile(r"""
@@ -18,11 +19,13 @@ class CDNBG(Plugin):
             nova\.bg/live|
             kanal3\.bg/live|
             bgonair\.bg/tvonline|
-            tvevropa\.com/na-zhivo|
+            inlife\.bg|
+            mmtvmusic\.com/live|
+            mu-vi\.tv/LiveStreams/pages/Live\.aspx|
             bloombergtv.bg/video
         )/?
     """, re.VERBOSE)
-    iframe_re = re.compile(r"iframe .*?src=\"((?:https?:)?//(?:\w+\.)?cdn.bg/live[^\"]+)\"", re.DOTALL)
+    iframe_re = re.compile(r"iframe .*?src=\"((?:https?(?::|&#58;))?//(?:\w+\.)?cdn.bg/live[^\"]+)\"", re.DOTALL)
     sdata_re = re.compile(r"sdata\.src.*?=.*?(?P<q>[\"'])(?P<url>http.*?)(?P=q)")
     hls_file_re = re.compile(r"(src|file): (?P<q>[\"'])(?P<url>(https?:)?//.+?m3u8.*?)(?P=q)")
     hls_src_re = re.compile(r"video src=(?P<url>http[^ ]+m3u8[^ ]*)")
@@ -43,23 +46,25 @@ class CDNBG(Plugin):
         p = urlparse(self.url)
         for url in self.iframe_re.findall(res.text):
             if "googletagmanager" not in url:
+                url = url.replace("&#58;", ":")
                 if url.startswith("//"):
                     return "{0}:{1}".format(p.scheme, url)
                 else:
                     return url
 
     def _get_streams(self):
-        self.session.http.headers = {"User-Agent": useragents.CHROME}
+        self.session.http.headers.update({"User-Agent": useragents.CHROME})
         res = self.session.http.get(self.url)
         iframe_url = self.find_iframe(res)
 
         if iframe_url:
-            self.logger.debug("Found iframe: {0}", iframe_url)
+            log.debug("Found iframe: {0}", iframe_url)
             res = self.session.http.get(iframe_url, headers={"Referer": self.url})
             stream_url = update_scheme(self.url, self.stream_schema.validate(res.text))
+            log.warning("SSL Verification disabled.")
             return HLSStream.parse_variant_playlist(self.session,
                                                     stream_url,
-                                                    headers={"User-Agent": useragents.CHROME})
+                                                    verify=False)
 
 
 __plugin__ = CDNBG
diff --git a/src/streamlink/plugins/filmon.py b/src/streamlink/plugins/filmon.py
index ffc0fbe6..1a2e77eb 100644
--- a/src/streamlink/plugins/filmon.py
+++ b/src/streamlink/plugins/filmon.py
@@ -164,7 +164,7 @@ class Filmon(Plugin):
             |
             (?:tv/)channel/(?:export\?)?
             |
-            tv/(?!channel)
+            tv/(?!channel/)
             |
             channel/
             |
diff --git a/src/streamlink/plugins/ine.py b/src/streamlink/plugins/ine.py
index 8ebc9109..e0389b7e 100644
--- a/src/streamlink/plugins/ine.py
+++ b/src/streamlink/plugins/ine.py
@@ -23,7 +23,7 @@ class INE(Plugin):
             validate.all(
                 validate.get(1),
                 validate.transform(json.loads),
-                {"playlist": str},
+                {"playlist": validate.text},
                 validate.get("playlist")
             )
         )
diff --git a/src/streamlink/plugins/npo.py b/src/streamlink/plugins/npo.py
index 7c63c44c..024f1029 100644
--- a/src/streamlink/plugins/npo.py
+++ b/src/streamlink/plugins/npo.py
@@ -5,9 +5,12 @@ Supports:
     - https://www.npo.nl/nos-journaal/07-07-2017/POW_03375651
     - https://www.zapp.nl/topdoks/gemist/VPWON_1276930
     - https://zappelin.nl/10-voor/gemist/VPWON_1271522
+    - https://www.npostart.nl/nos-journaal/07-07-2017/POW_03375651
    Live:
     - https://www.npo.nl/live/npo-1
     - https://zappelin.nl/tv-kijken
+    - https://www.npostart.nl/live/npo-1
+
 """
 
 import re
@@ -22,7 +25,7 @@ from streamlink.utils import parse_json
 
 class NPO(Plugin):
     api_url = "http://ida.omroep.nl/app.php/{endpoint}"
-    url_re = re.compile(r"https?://(\w+\.)?(npo\.nl|zapp\.nl|zappelin\.nl)/")
+    url_re = re.compile(r"https?://(\w+\.)?(npostart\.nl|npo\.nl|zapp\.nl|zappelin\.nl)/")
     media_id_re = re.compile(r'''<npo-player\smedia-id=["'](?P<media_id>[^"']+)["']''')
     prid_re = re.compile(r'''(?:data(-alt)?-)?prid\s*[=:]\s*(?P<q>["'])(\w+)(?P=q)''')
     react_re = re.compile(r'''data-react-props\s*=\s*(?P<q>["'])(?P<data>.*?)(?P=q)''')
diff --git a/src/streamlink/plugins/powerapp.py b/src/streamlink/plugins/powerapp.py
index 0ad0dda1..702505e5 100644
--- a/src/streamlink/plugins/powerapp.py
+++ b/src/streamlink/plugins/powerapp.py
@@ -7,7 +7,7 @@ from streamlink.stream import HLSStream
 
 
 class PowerApp(Plugin):
-    url_re = re.compile(r"https?://(?:www.)?powerapp.com.tr/tv/(\w+)")
+    url_re = re.compile(r"https?://(?:www.)?powerapp.com.tr/tvs?/(\w+)")
     api_url = "http://api.powergroup.com.tr/Channels/{0}/?appRef=iPowerWeb&apiVersion=11"
     api_schema = validate.Schema(validate.all({
         "errorCode": 0,
diff --git a/src/streamlink/plugins/sbscokr.py b/src/streamlink/plugins/sbscokr.py
new file mode 100644
index 00000000..52865869
--- /dev/null
+++ b/src/streamlink/plugins/sbscokr.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+import logging
+import random
+import re
+
+from streamlink.plugin import Plugin, PluginArguments, PluginArgument
+from streamlink.plugin.api import useragents, validate
+from streamlink.stream import HLSStream
+
+log = logging.getLogger(__name__)
+
+
+class SBScokr(Plugin):
+
+    api_channel = 'http://apis.sbs.co.kr/play-api/1.0/onair/channel/{0}'
+    api_channels = 'http://static.apis.sbs.co.kr/play-api/1.0/onair/channels'
+
+    _url_re = re.compile(r'https?://play\.sbs\.co\.kr/onair/pc/index.html')
+
+    _channels_schema = validate.Schema({
+        'list': [{
+            'channelname': validate.all(
+                validate.text,
+            ),
+            'channelid': validate.text,
+            validate.optional('type'): validate.text,
+        }]},
+        validate.get('list'),
+    )
+
+    _channel_schema = validate.Schema(
+        {
+            'onair': {
+                'info': {
+                    'onair_yn': validate.text,
+                    'overseas_yn': validate.text,
+                    'overseas_text': validate.text,
+                },
+                'source': {
+                    'mediasourcelist': validate.any([{
+                        validate.optional('default'): validate.text,
+                        'mediaurl': validate.text,
+                    }], [])
+                },
+            }
+        },
+        validate.get('onair'),
+    )
+
+    arguments = PluginArguments(
+        PluginArgument(
+            'id',
+            metavar='CHANNELID',
+            type=str.upper,
+            help='''
+            Channel ID to play.
+
+            Example:
+
+                %(prog)s http://play.sbs.co.kr/onair/pc/index.html best --sbscokr-id S01
+
+            '''
+        )
+    )
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    def _get_streams(self):
+        user_channel_id = self.get_option('id')
+
+        self.session.http.headers.update({'User-Agent': useragents.FIREFOX})
+        res = self.session.http.get(self.api_channels)
+        res = self.session.http.json(res, schema=self._channels_schema)
+
+        channels = {}
+        for channel in sorted(res, key=lambda x: x['channelid']):
+            if channel.get('type') in ('TV', 'Radio'):
+                channels[channel['channelid']] = channel['channelname']
+
+        log.info('Available IDs: {0}'.format(', '.join(
+            '{0} ({1})'.format(key, value) for key, value in channels.items())))
+        if not user_channel_id:
+            log.error('No channel selected, use --sbscokr-id CHANNELID')
+            return
+        elif user_channel_id and user_channel_id not in channels.keys():
+            log.error('Channel ID "{0}" is not available.'.format(user_channel_id))
+            return
+
+        params = {
+            'v_type': '2',
+            'platform': 'pcweb',
+            'protocol': 'hls',
+            'jwt-token': '',
+            'rnd': random.randint(50, 300)
+        }
+
+        res = self.session.http.get(self.api_channel.format(user_channel_id),
+                                    params=params)
+        res = self.session.http.json(res, schema=self._channel_schema)
+
+        for media in res['source']['mediasourcelist']:
+            if media['mediaurl']:
+                for s in HLSStream.parse_variant_playlist(self.session,
+                                                          media['mediaurl']).items():
+                    yield s
+        else:
+            if res['info']['onair_yn'] != 'Y':
+                log.error('This channel is currently unavailable')
+            elif res['info']['overseas_yn'] != 'Y':
+                log.error(res['info']['overseas_text'])
+
+
+__plugin__ = SBScokr
diff --git a/src/streamlink/plugins/skai.py b/src/streamlink/plugins/skai.py
index 61fba7ec..d43fcafd 100644
--- a/src/streamlink/plugins/skai.py
+++ b/src/streamlink/plugins/skai.py
@@ -3,20 +3,15 @@ import re
 from streamlink.plugin import Plugin
 from streamlink.plugin.api import validate
 
-YOUTUBE_URL = "https://www.youtube.com/watch?v={0}"
-_url_re = re.compile(r'http(s)?://www\.skai.gr/.*')
-_youtube_id = re.compile(r'<span\s+itemprop="contentUrl"\s+href="(.*)"></span>', re.MULTILINE)
-_youtube_url_schema = validate.Schema(
-    validate.all(
-        validate.transform(_youtube_id.search),
-        validate.any(
-            None,
-            validate.all(
-                validate.get(1),
-                validate.text
-            )
-        )
-    )
+
+_url_re = re.compile(r'http(s)?://www\.skai(?:tv)?.gr/.*')
+_api_url = "http://www.skaitv.gr/json/live.php"
+_api_res_schema = validate.Schema(validate.all(
+    validate.get("now"),
+    {
+        "livestream": validate.url()
+    },
+    validate.get("livestream"))
 )
 
 
@@ -26,9 +21,10 @@ class Skai(Plugin):
         return _url_re.match(url)
 
     def _get_streams(self):
-        channel_id = self.session.http.get(self.url, schema=_youtube_url_schema)
-        if channel_id:
-            return self.session.streams(YOUTUBE_URL.format(channel_id))
+        api_res = self.session.http.get(_api_url)
+        yt_url = self.session.http.json(api_res, schema=_api_res_schema)
+        if yt_url:
+            return self.session.streams(yt_url)
 
 
 __plugin__ = Skai
diff --git a/src/streamlink/plugins/stv.py b/src/streamlink/plugins/stv.py
new file mode 100644
index 00000000..1198b1ef
--- /dev/null
+++ b/src/streamlink/plugins/stv.py
@@ -0,0 +1,48 @@
+import logging
+import re
+
+from streamlink.exceptions import PluginError
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import useragents
+from streamlink.stream import HLSStream
+
+log = logging.getLogger(__name__)
+
+
+class STV(Plugin):
+    _url_re = re.compile(r'https?://player\.stv\.tv/live')
+
+    API_URL = 'https://player.api.stv.tv/v1/streams/stv/'
+
+    title = None
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    def get_title(self):
+        if self.title is None:
+            self._get_api_results()
+        return self.title
+
+    def _get_api_results(self):
+        self.session.http.headers.update({'User-Agent': useragents.FIREFOX})
+        res = self.session.http.get(self.API_URL)
+        data = self.session.http.json(res)
+
+        if data['success'] is False:
+            raise PluginError(data['reason']['message'])
+
+        try:
+            self.title = data['results']['now']['title']
+        except KeyError:
+            self.title = 'STV'
+
+        return data
+
+    def _get_streams(self):
+        hls_url = self._get_api_results()['results']['streamUrl']
+        return HLSStream.parse_variant_playlist(self.session, hls_url)
+
+
+__plugin__ = STV
diff --git a/src/streamlink/plugins/twitch.py b/src/streamlink/plugins/twitch.py
index 5cb3f41c..410a22b6 100644
--- a/src/streamlink/plugins/twitch.py
+++ b/src/streamlink/plugins/twitch.py
@@ -213,6 +213,12 @@ class TwitchAPI(object):
     def channel_info(self, channel, **params):
         return self.call("/kraken/channels/{0}".format(channel), **params)
 
+    def streams(self, channel_id, **params):
+        return self.call("/kraken/streams/{0}".format(channel_id), **params)
+
+    def clips(self, clip_name, **params):
+        return self.call("/kraken/clips/{0}".format(clip_name), **params)
+
     # Private API calls
 
     def access_token(self, endpoint, asset, **params):
@@ -286,6 +292,38 @@ class Twitch(Plugin):
     def can_handle_url(cls, url):
         return _url_re.match(url)
 
+    def _get_metadata(self):
+        if self.video_id:
+            api_res = self.api.videos(self.video_id)
+            self.title = api_res["title"]
+            self.author = api_res["channel"]["display_name"]
+            self.category = api_res["game"]
+        elif self.clip_name:
+            api_res = self.api.clips(self.clip_name)
+            self.title = api_res["title"]
+            self.author = api_res["broadcaster"]["display_name"]
+            self.category = api_res["game"]
+        elif self._channel:
+            api_res = self.api.streams(self.channel_id)["stream"]["channel"]
+            self.title = api_res["status"]
+            self.author = api_res["display_name"]
+            self.category = api_res["game"]
+
+    def get_title(self):
+        if self.title is None:
+            self._get_metadata()
+        return self.title
+
+    def get_author(self):
+        if self.author is None:
+            self._get_metadata()
+        return self.author
+
+    def get_category(self):
+        if self.category is None:
+            self._get_metadata()
+        return self.category
+
     def __init__(self, url):
         Plugin.__init__(self, url)
         self._hosted_chain = []
@@ -298,6 +336,9 @@ class Twitch(Plugin):
         self._channel_id = None
         self._channel = None
         self.clip_name = None
+        self.title = None
+        self.author = None
+        self.category = None
 
         if self.subdomain == "player":
             # pop-out player
diff --git a/src/streamlink/plugins/vtvgo.py b/src/streamlink/plugins/vtvgo.py
new file mode 100644
index 00000000..a2be57cd
--- /dev/null
+++ b/src/streamlink/plugins/vtvgo.py
@@ -0,0 +1,45 @@
+import logging
+import re
+
+from streamlink.exceptions import PluginError
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import useragents
+from streamlink.stream import HLSStream
+
+log = logging.getLogger(__name__)
+
+
+class VTVgo(Plugin):
+
+    AJAX_URL = 'https://vtvgo.vn/ajax-get-stream'
+
+    _url_re = re.compile(r'https://vtvgo\.vn/xem-truc-tuyen-kenh-')
+    _params_re = re.compile(r'''var\s(?P<key>(?:type_)?id|time|token)\s*=\s*["']?(?P<value>[^"']+)["']?;''')
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls._url_re.match(url) is not None
+
+    def _get_streams(self):
+        self.session.http.headers.update({
+            'Origin': 'https://vtvgo.vn',
+            'Referer': self.url,
+            'User-Agent': useragents.FIREFOX,
+            'X-Requested-With': 'XMLHttpRequest',
+        })
+        res = self.session.http.get(self.url)
+
+        params = self._params_re.findall(res.text)
+        if not params:
+            raise PluginError('No POST data')
+        elif len(params) != 4:
+            raise PluginError('Not enough POST data: {0!r}'.format(params))
+
+        log.trace('{0!r}'.format(params))
+        res = self.session.http.post(self.AJAX_URL, data=dict(params))
+        hls_url = self.session.http.json(res)['stream_url'][0]
+
+        return HLSStream.parse_variant_playlist(self.session, hls_url)
+
+
+__plugin__ = VTVgo
diff --git a/src/streamlink/stream/hds.py b/src/streamlink/stream/hds.py
index ffece1a7..938cda1c 100644
--- a/src/streamlink/stream/hds.py
+++ b/src/streamlink/stream/hds.py
@@ -82,7 +82,7 @@ class HDSStreamWriter(SegmentedStreamWriter):
                                          **request_params)
         except StreamError as err:
             log.error("Failed to open fragment {0}-{1}: {2}",
-                              fragment.segment, fragment.fragment, err)
+                      fragment.segment, fragment.fragment, err)
             return self.fetch(fragment, retries - 1)
 
     def write(self, fragment, res, chunk_size=8192):
@@ -100,12 +100,12 @@ class HDSStreamWriter(SegmentedStreamWriter):
                     break
         except F4VError as err:
             log.error("Failed to parse fragment {0}-{1}: {2}",
-                              fragment.segment, fragment.fragment, err)
+                      fragment.segment, fragment.fragment, err)
             return
 
         if not mdat:
             log.error("No MDAT box found in fragment {0}-{1}",
-                              fragment.segment, fragment.fragment)
+                      fragment.segment, fragment.fragment)
             return
 
         try:
@@ -116,16 +116,16 @@ class HDSStreamWriter(SegmentedStreamWriter):
                     break
             else:
                 log.debug("Download of fragment {0}-{1} complete",
-                                  fragment.segment, fragment.fragment)
+                          fragment.segment, fragment.fragment)
         except IOError as err:
             if "Unknown tag type" in str(err):
                 log.error("Unknown tag type found, this stream is "
-                                  "probably encrypted")
+                          "probably encrypted")
                 self.close()
                 return
 
             log.error("Error reading fragment {0}-{1}: {2}",
-                              fragment.segment, fragment.fragment, err)
+                      fragment.segment, fragment.fragment, err)
 
 
 class HDSStreamWorker(SegmentedStreamWorker):
@@ -182,7 +182,7 @@ class HDSStreamWorker(SegmentedStreamWorker):
                                        current_fragment - (fragment_buffer - 1))
 
                 log.debug("Live edge buffer {0} sec is {1} fragments",
-                                  self.live_edge, fragment_buffer)
+                          self.live_edge, fragment_buffer)
 
                 # Make sure we don't have a duration set when it's a
                 # live stream since it will just confuse players anyway.
@@ -327,7 +327,7 @@ class HDSStreamWorker(SegmentedStreamWorker):
                                     fragment_duration, fragment_url)
 
                 log.debug("Adding fragment {0}-{1} to queue",
-                                  fragment.segment, fragment.fragment)
+                          fragment.segment, fragment.fragment)
                 yield fragment
 
                 # End of stream
@@ -556,7 +556,7 @@ class HDSStream(Stream):
                     # manifest but not the child one.
                     bitrate = media.attrib.get("bitrate")
 
-                    if bitrate and not re.match("^(\d+)k$", name):
+                    if bitrate and not re.match(r"^(\d+)k$", name):
                         name = bitrate + "k"
 
                     streams[name] = stream
diff --git a/src/streamlink/stream/hls.py b/src/streamlink/stream/hls.py
index 8a67f564..c0285f73 100644
--- a/src/streamlink/stream/hls.py
+++ b/src/streamlink/stream/hls.py
@@ -45,6 +45,8 @@ class HLSStreamWriter(SegmentedStreamWriter):
         self.byterange_offsets = defaultdict(int)
         self.key_data = None
         self.key_uri = None
+        self.key_uri_override = options.get("hls-segment-key-uri")
+
         if self.ignore_names:
             # creates a regex from a list of segment names,
             # this will be used to ignore segments.
@@ -57,15 +59,18 @@ class HLSStreamWriter(SegmentedStreamWriter):
         if key.method != "AES-128":
             raise StreamError("Unable to decrypt cipher {0}", key.method)
 
-        if not key.uri:
+        if not self.key_uri_override and not key.uri:
             raise StreamError("Missing URI to decryption key")
 
-        if self.key_uri != key.uri:
-            res = self.session.http.get(key.uri, exception=StreamError,
+        key_uri = self.key_uri_override if self.key_uri_override else key.uri
+
+        if self.key_uri != key_uri:
+            res = self.session.http.get(key_uri, exception=StreamError,
                                         retries=self.retries,
                                         **self.reader.request_params)
+            res.encoding = "binary/octet-stream"
             self.key_data = res.content
-            self.key_uri = key.uri
+            self.key_uri = key_uri
 
         iv = key.iv or num_to_iv(sequence)
 
diff --git a/src/streamlink_cli/argparser.py b/src/streamlink_cli/argparser.py
index 6af91976..019a55cb 100644
--- a/src/streamlink_cli/argparser.py
+++ b/src/streamlink_cli/argparser.py
@@ -490,7 +490,7 @@ def build_parser():
         "-f", "--force",
         action="store_true",
         help="""
-        When using -o, always write to file even if it already exists.
+        When using -o or -r, always write to file even if it already exists.
         """
     )
     output.add_argument(
@@ -500,6 +500,24 @@ def build_parser():
         Write stream data to stdout instead of playing it.
         """
     )
+    output.add_argument(
+        "-r", "--record",
+        metavar="FILENAME",
+        help="""
+        Open the stream in the player, while at the same time writing it to FILENAME.
+
+        You will be prompted if the file already exists.
+        """
+    )
+    output.add_argument(
+        "-R", "--record-and-pipe",
+        metavar="FILENAME",
+        help="""
+        Write stream data to stdout, while at the same time writing it to FILENAME.
+
+        You will be prompted if the file already exists.
+        """
+    )
 
     stream = parser.add_argument_group("Stream options")
     stream.add_argument(
@@ -741,6 +759,19 @@ def build_parser():
         the ignored break.
         """
     )
+    transport.add_argument(
+        "--hls-segment-key-uri",
+        metavar="URI",
+        type=str,
+        help="""
+        URI to segment encryption key. If no URI is specified, the URI contained
+        in the segments will be used.
+
+        Example: --hls-segment-key-uri "https://example.com/hls/encryption_key"
+
+        Default is None.
+        """
+    )
     transport.add_argument(
         "--hls-audio-select",
         type=comma_list,
@@ -842,7 +873,7 @@ def build_parser():
         """
     )
     transport.add_argument(
-        "--rtmp-rtmpdump", "--rtmpdump", "-r",
+        "--rtmp-rtmpdump", "--rtmpdump",
         metavar="FILENAME",
         help="""
         RTMPDump is used to access RTMP streams. You can specify the
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index b43175a6..efbe790c 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -81,6 +81,9 @@ def create_output(plugin):
 
     """
 
+    if (args.output or args.stdout) and (args.record or args.record_and_pipe):
+        console.exit("Cannot use record options with other file output options.")
+
     if args.output:
         if args.output == "-":
             out = FileOutput(fd=stdout)
@@ -88,8 +91,11 @@ def create_output(plugin):
             out = check_file_output(args.output, args.force)
     elif args.stdout:
         out = FileOutput(fd=stdout)
+    elif args.record_and_pipe:
+        record = check_file_output(args.record_and_pipe, args.force)
+        out = FileOutput(fd=stdout, record=record)
     else:
-        http = namedpipe = None
+        http = namedpipe = record = None
 
         if not args.player:
             console.exit("The default player (VLC) does not seem to be "
@@ -108,12 +114,17 @@ def create_output(plugin):
             http = create_http_server()
 
         title = create_title(plugin)
+
+        if args.record:
+            record = check_file_output(args.record, args.force)
+
         log.info("Starting player: {0}", args.player)
+
         out = PlayerOutput(args.player, args=args.player_args,
                            quiet=not args.verbose_player,
                            kill=not args.player_no_close,
                            namedpipe=namedpipe, http=http,
-                           title=title)
+                           record=record, title=title)
 
     return out
 
@@ -327,6 +338,7 @@ def read_stream(stream, output, prebuffer, chunk_size=8192):
     is_http = isinstance(output, HTTPServer)
     is_fifo = is_player and output.namedpipe
     show_progress = isinstance(output, FileOutput) and output.fd is not stdout and sys.stdout.isatty()
+    show_record_progress = isinstance(output.record, FileOutput) and output.record.fd is not stdout and sys.stdout.isatty()
 
     stream_iterator = chain(
         [prebuffer],
@@ -335,6 +347,9 @@ def read_stream(stream, output, prebuffer, chunk_size=8192):
     if show_progress:
         stream_iterator = progress(stream_iterator,
                                    prefix=os.path.basename(args.output))
+    elif show_record_progress:
+        stream_iterator = progress(stream_iterator,
+                                   prefix=os.path.basename(args.record))
 
     try:
         for data in stream_iterator:
@@ -777,6 +792,9 @@ def setup_options():
     if args.hls_segment_ignore_names:
         streamlink.set_option("hls-segment-ignore-names", args.hls_segment_ignore_names)
 
+    if args.hls_segment_key_uri:
+        streamlink.set_option("hls-segment-key-uri", args.hls_segment_key_uri)
+
     if args.hls_timeout:
         streamlink.set_option("hls-timeout", args.hls_timeout)
 
@@ -962,7 +980,7 @@ def main():
 
     # Console output should be on stderr if we are outputting
     # a stream to stdout.
-    if args.stdout or args.output == "-":
+    if args.stdout or args.output == "-" or args.record_and_pipe:
         console_out = sys.stderr
     else:
         console_out = sys.stdout
diff --git a/src/streamlink_cli/output.py b/src/streamlink_cli/output.py
index 925a802c..e8fd698d 100644
--- a/src/streamlink_cli/output.py
+++ b/src/streamlink_cli/output.py
@@ -47,31 +47,39 @@ class Output(object):
 
 
 class FileOutput(Output):
-    def __init__(self, filename=None, fd=None):
+    def __init__(self, filename=None, fd=None, record=None):
         super(FileOutput, self).__init__()
         self.filename = filename
         self.fd = fd
+        self.record = record
 
     def _open(self):
         if self.filename:
             self.fd = open(self.filename, "wb")
 
+        if self.record:
+            self.record.open()
+
         if is_win32:
             msvcrt.setmode(self.fd.fileno(), os.O_BINARY)
 
     def _close(self):
         if self.fd is not stdout:
             self.fd.close()
+        if self.record:
+            self.record.close()
 
     def _write(self, data):
         self.fd.write(data)
+        if self.record:
+            self.record.write(data)
 
 
 class PlayerOutput(Output):
     PLAYER_TERMINATE_TIMEOUT = 10.0
 
     def __init__(self, cmd, args=DEFAULT_PLAYER_ARGUMENTS, filename=None, quiet=True, kill=True, call=False, http=None,
-                 namedpipe=None, title=None):
+                 namedpipe=None, record=None, title=None):
         super(PlayerOutput, self).__init__()
         self.cmd = cmd
         self.args = args
@@ -85,6 +93,7 @@ class PlayerOutput(Output):
         self.title = title
         self.player = None
         self.player_name = self.supported_player(self.cmd)
+        self.record = record
 
         if self.namedpipe or self.filename or self.http:
             self.stdin = sys.stdin
@@ -195,6 +204,8 @@ class PlayerOutput(Output):
 
     def _open(self):
         try:
+            if self.record:
+                self.record.open()
             if self.call and self.filename:
                 self._open_call()
             else:
@@ -207,7 +218,11 @@ class PlayerOutput(Output):
 
     def _open_call(self):
         args = self._create_arguments()
-        log.debug(u"Calling: {0}".format(subprocess.list2cmdline(args)))
+        if is_win32:
+            fargs = args
+        else:
+            fargs = subprocess.list2cmdline(args)
+        log.debug(u"Calling: {0}".format(fargs))
         subprocess.call(args,
                         stdout=self.stdout,
                         stderr=self.stderr)
@@ -216,7 +231,11 @@ class PlayerOutput(Output):
         # Force bufsize=0 on all Python versions to avoid writing the
         # unflushed buffer when closing a broken input pipe
         args = self._create_arguments()
-        log.debug(u"Opening subprocess: {0}".format(subprocess.list2cmdline(args)))
+        if is_win32:
+            fargs = args
+        else:
+            fargs = subprocess.list2cmdline(args)
+        log.debug(u"Opening subprocess: {0}".format(fargs))
         self.player = subprocess.Popen(args,
                                        stdin=self.stdin, bufsize=0,
                                        stdout=self.stdout,
@@ -240,6 +259,9 @@ class PlayerOutput(Output):
         elif not self.filename:
             self.player.stdin.close()
 
+        if self.record:
+            self.record.close()
+
         if self.kill:
             with ignored(Exception):
                 self.player.terminate()
@@ -254,6 +276,9 @@ class PlayerOutput(Output):
         self.player.wait()
 
     def _write(self, data):
+        if self.record:
+            self.record.write(data)
+
         if self.namedpipe:
             self.namedpipe.write(data)
         elif self.http:
diff --git a/win32/LICENSE.txt b/win32/LICENSE.txt
index 3818abb3..a299fcc1 100644
--- a/win32/LICENSE.txt
+++ b/win32/LICENSE.txt
@@ -1,6 +1,4 @@
 Streamlink is distributed with some bundles python libraries and some compiled binaries.
-Streamlink also makes use of an image (doggo.ico) created by Jinho Jung, distributed under the CC BY-SA 2.0 license.
-
 
 Python 3.5.2 License
 ====================
@@ -662,56 +660,3 @@ whether future versions of the GNU Lesser General Public License shall
 apply, that proxy's public statement of acceptance of any version is
 permanent authorization for you to choose that version for the
 Library.
-
-doggo.ico License
-=================
-THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
-
-BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS.
-
-1. Definitions
-
-"Collective Work" means a work, such as a periodical issue, anthology or encyclopedia, in which the Work in its entirety in unmodified form, along with a number of other contributions, constituting separate and independent works in themselves, are assembled into a collective whole. A work that constitutes a Collective Work will not be considered a Derivative Work (as defined below) for the purposes of this License.
-"Derivative Work" means a work based upon the Work or upon the Work and other pre-existing works, such as a translation, musical arrangement, dramatization, fictionalization, motion picture version, sound recording, art reproduction, abridgment, condensation, or any other form in which the Work may be recast, transformed, or adapted, except that a work that constitutes a Collective Work will not be considered a Derivative Work for the purpose of this License. For the avoidance of doubt, where the Work is a musical composition or sound recording, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered a Derivative Work for the purpose of this License.
-"Licensor" means the individual or entity that offers the Work under the terms of this License.
-"Original Author" means the individual or entity who created the Work.
-"Work" means the copyrightable work of authorship offered under the terms of this License.
-"You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation.
-"License Elements" means the following high-level license attributes as selected by Licensor and indicated in the title of this License: Attribution, ShareAlike.
-2. Fair Use Rights. Nothing in this license is intended to reduce, limit, or restrict any rights arising from fair use, first sale or other limitations on the exclusive rights of the copyright owner under copyright law or other applicable laws.
-
-3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below:
-
-to reproduce the Work, to incorporate the Work into one or more Collective Works, and to reproduce the Work as incorporated in the Collective Works;
-to create and reproduce Derivative Works;
-to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission the Work including as incorporated in Collective Works;
-to distribute copies or phonorecords of, display publicly, perform publicly, and perform publicly by means of a digital audio transmission Derivative Works.
-For the avoidance of doubt, where the work is a musical composition:
-
-Performance Royalties Under Blanket Licenses. Licensor waives the exclusive right to collect, whether individually or via a performance rights society (e.g. ASCAP, BMI, SESAC), royalties for the public performance or public digital performance (e.g. webcast) of the Work.
-Mechanical Rights and Statutory Royalties. Licensor waives the exclusive right to collect, whether individually or via a music rights society or designated agent (e.g. Harry Fox Agency), royalties for any phonorecord You create from the Work ("cover version") and distribute, subject to the compulsory license created by 17 USC Section 115 of the US Copyright Act (or the equivalent in other jurisdictions).
-Webcasting Rights and Statutory Royalties. For the avoidance of doubt, where the Work is a sound recording, Licensor waives the exclusive right to collect, whether individually or via a performance-rights society (e.g. SoundExchange), royalties for the public digital performance (e.g. webcast) of the Work, subject to the compulsory license created by 17 USC Section 114 of the US Copyright Act (or the equivalent in other jurisdictions).
-The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. All rights not expressly granted by Licensor are hereby reserved.
-
-4. Restrictions.The license granted in Section 3 above is expressly made subject to and limited by the following restrictions:
-
-You may distribute, publicly display, publicly perform, or publicly digitally perform the Work only under the terms of this License, and You must include a copy of, or the Uniform Resource Identifier for, this License with every copy or phonorecord of the Work You distribute, publicly display, publicly perform, or publicly digitally perform. You may not offer or impose any terms on the Work that alter or restrict the terms of this License or the recipients' exercise of the rights granted hereunder. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties. You may not distribute, publicly display, publicly perform, or publicly digitally perform the Work with any technological measures that control access or use of the Work in a manner inconsistent with the terms of this License Agreement. The above applies to the Work as incorporated in a Collective Work, but this does not require the Collective Work apart from the Work itself to be made subject to the terms of this License. If You create a Collective Work, upon notice from any Licensor You must, to the extent practicable, remove from the Collective Work any reference to such Licensor or the Original Author, as requested. If You create a Derivative Work, upon notice from any Licensor You must, to the extent practicable, remove from the Derivative Work any reference to such Licensor or the Original Author, as requested.
-You may distribute, publicly display, publicly perform, or publicly digitally perform a Derivative Work only under the terms of this License, a later version of this License with the same License Elements as this License, or a Creative Commons iCommons license that contains the same License Elements as this License (e.g. Attribution-ShareAlike 2.0 Japan). You must include a copy of, or the Uniform Resource Identifier for, this License or other license specified in the previous sentence with every copy or phonorecord of each Derivative Work You distribute, publicly display, publicly perform, or publicly digitally perform. You may not offer or impose any terms on the Derivative Works that alter or restrict the terms of this License or the recipients' exercise of the rights granted hereunder, and You must keep intact all notices that refer to this License and to the disclaimer of warranties. You may not distribute, publicly display, publicly perform, or publicly digitally perform the Derivative Work with any technological measures that control access or use of the Work in a manner inconsistent with the terms of this License Agreement. The above applies to the Derivative Work as incorporated in a Collective Work, but this does not require the Collective Work apart from the Derivative Work itself to be made subject to the terms of this License.
-If you distribute, publicly display, publicly perform, or publicly digitally perform the Work or any Derivative Works or Collective Works, You must keep intact all copyright notices for the Work and give the Original Author credit reasonable to the medium or means You are utilizing by conveying the name (or pseudonym if applicable) of the Original Author if supplied; the title of the Work if supplied; to the extent reasonably practicable, the Uniform Resource Identifier, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and in the case of a Derivative Work, a credit identifying the use of the Work in the Derivative Work (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). Such credit may be implemented in any reasonable manner; provided, however, that in the case of a Derivative Work or Collective Work, at a minimum such credit will appear where any other comparable authorship credit appears and in a manner at least as prominent as such other comparable authorship credit.
-5. Representations, Warranties and Disclaimer
-
-UNLESS OTHERWISE AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE MATERIALS, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
-
-6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
-
-7. Termination
-
-This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Derivative Works or Collective Works from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License.
-Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above.
-8. Miscellaneous
-
-Each time You distribute or publicly digitally perform the Work or a Collective Work, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License.
-Each time You distribute or publicly digitally perform a Derivative Work, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License.
-If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.
-No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent.
-This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You.


Test patch:
diff --git a/tests/plugins/test_afreeca.py b/tests/plugins/test_afreeca.py
index fffc7cab..ab7b98d6 100644
--- a/tests/plugins/test_afreeca.py
+++ b/tests/plugins/test_afreeca.py
@@ -6,15 +6,18 @@ from streamlink.plugins.afreeca import AfreecaTV
 class TestPluginAfreecaTV(unittest.TestCase):
     def test_can_handle_url(self):
         should_match = [
-            "http://afreecatv.com/exampleuser",
-            "http://afreecatv.com/exampleuser/123123123",
             "http://play.afreecatv.com/exampleuser",
             "http://play.afreecatv.com/exampleuser/123123123",
+            "https://play.afreecatv.com/exampleuser",
         ]
         for url in should_match:
             self.assertTrue(AfreecaTV.can_handle_url(url))
 
         should_not_match = [
+            "http://afreeca.com/exampleuser",
+            "http://afreeca.com/exampleuser/123123123",
+            "http://afreecatv.com/exampleuser",
+            "http://afreecatv.com/exampleuser/123123123",
             "http://www.afreecatv.com.tw",
             "http://www.afreecatv.jp",
         ]
diff --git a/tests/plugins/test_cdnbg.py b/tests/plugins/test_cdnbg.py
index 8b8c48d3..7b6026a6 100644
--- a/tests/plugins/test_cdnbg.py
+++ b/tests/plugins/test_cdnbg.py
@@ -19,7 +19,12 @@ class TestPluginCDNBG(unittest.TestCase):
         self.assertTrue(CDNBG.can_handle_url("http://tv.bnt.bg/bnt1/16x9"))
         self.assertTrue(CDNBG.can_handle_url("http://tv.bnt.bg/bnt2/16x9/"))
         self.assertTrue(CDNBG.can_handle_url("http://tv.bnt.bg/bnt2/16x9"))
+        self.assertTrue(CDNBG.can_handle_url("http://inlife.bg/"))
+        self.assertTrue(CDNBG.can_handle_url("https://mmtvmusic.com/live/"))
+        self.assertTrue(CDNBG.can_handle_url("http://mu-vi.tv/LiveStreams/pages/Live.aspx"))
+        self.assertTrue(CDNBG.can_handle_url("https://www.bloombergtv.bg/video"))
 
         # shouldn't match
         self.assertFalse(CDNBG.can_handle_url("http://www.tvcatchup.com/"))
         self.assertFalse(CDNBG.can_handle_url("http://www.youtube.com/"))
+        self.assertFalse(CDNBG.can_handle_url("https://www.tvevropa.com"))
diff --git a/tests/plugins/test_filmon.py b/tests/plugins/test_filmon.py
index a62d407d..ab30fc5d 100644
--- a/tests/plugins/test_filmon.py
+++ b/tests/plugins/test_filmon.py
@@ -10,24 +10,25 @@ class TestPluginFilmon(unittest.TestCase):
             'http://www.filmon.tv/index/popout?channel_id=5510&quality=low',
             'http://www.filmon.tv/tv/channel/export?channel_id=5510&autoPlay=1',
             'http://www.filmon.tv/tv/channel/grandstand-show',
+            'http://www.filmon.tv/tv/channel-4',
             'https://www.filmon.com/tv/bbc-news',
             'https://www.filmon.tv/tv/55',
             'http://www.filmon.tv/vod/view/10250-0-crime-boss',
             'http://www.filmon.tv/group/comedy',
         ]
         for url in should_match:
-            self.assertTrue(Filmon.can_handle_url(url))
+            self.assertTrue(Filmon.can_handle_url(url), url)
 
     def test_can_handle_url_negative(self):
         should_not_match = [
             'https://example.com/index.html',
         ]
         for url in should_not_match:
-            self.assertFalse(Filmon.can_handle_url(url))
+            self.assertFalse(Filmon.can_handle_url(url), url)
 
     def _test_regex(self, url, expected):
         m = Filmon.url_re.match(url)
-        self.assertIsNotNone(m)
+        self.assertIsNotNone(m, url)
         # expected must return [is_group, channel, vod_id]
         self.assertEqual(expected, list(m.groups()))
 
@@ -51,6 +52,10 @@ class TestPluginFilmon(unittest.TestCase):
         self._test_regex('https://www.filmon.com/tv/bbc-news',
                          [None, 'bbc-news', None])
 
+    def test_regex_live_stream_tv_with_channel_in_name(self):
+        self._test_regex('https://www.filmon.com/tv/channel-4',
+                         [None, 'channel-4', None])
+
     def test_regex_live_stream_tv_number(self):
         self._test_regex('https://www.filmon.tv/tv/55',
                          [None, '55', None])
diff --git a/tests/plugins/test_powerapp.py b/tests/plugins/test_powerapp.py
index 043e4df9..f000b501 100644
--- a/tests/plugins/test_powerapp.py
+++ b/tests/plugins/test_powerapp.py
@@ -9,9 +9,10 @@ class TestPluginPowerApp(unittest.TestCase):
             'http://powerapp.com.tr/tv/powertv4k',
             'http://powerapp.com.tr/tv/powerturktv4k',
             'http://powerapp.com.tr/tv/powerEarthTV',
+            'http://www.powerapp.com.tr/tvs/powertv'
         ]
         for url in should_match:
-            self.assertTrue(PowerApp.can_handle_url(url))
+            self.assertTrue(PowerApp.can_handle_url(url), url)
 
     def test_can_handle_url_negative(self):
         should_not_match = [
diff --git a/tests/plugins/test_sbscokr.py b/tests/plugins/test_sbscokr.py
new file mode 100644
index 00000000..1dff264e
--- /dev/null
+++ b/tests/plugins/test_sbscokr.py
@@ -0,0 +1,19 @@
+import unittest
+
+from streamlink.plugins.sbscokr import SBScokr
+
+
+class TestPluginSBScokr(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            'https://play.sbs.co.kr/onair/pc/index.html',
+            'http://play.sbs.co.kr/onair/pc/index.html',
+        ]
+        for url in should_match:
+            self.assertTrue(SBScokr.can_handle_url(url))
+
+        should_not_match = [
+            'https://example.com/index.html',
+        ]
+        for url in should_not_match:
+            self.assertFalse(SBScokr.can_handle_url(url))
diff --git a/tests/plugins/test_skai.py b/tests/plugins/test_skai.py
index f2347407..f07e61ee 100644
--- a/tests/plugins/test_skai.py
+++ b/tests/plugins/test_skai.py
@@ -7,6 +7,7 @@ class TestPluginSkai(unittest.TestCase):
     def test_can_handle_url(self):
         should_match = [
             'http://www.skai.gr/player/tvlive/',
+            'http://www.skaitv.gr/live',
         ]
         for url in should_match:
             self.assertTrue(Skai.can_handle_url(url))
diff --git a/tests/plugins/test_stv.py b/tests/plugins/test_stv.py
new file mode 100644
index 00000000..809bd2a2
--- /dev/null
+++ b/tests/plugins/test_stv.py
@@ -0,0 +1,12 @@
+import unittest
+
+from streamlink.plugins.stv import STV
+
+
+class TestPluginSTV(unittest.TestCase):
+    def test_can_handle_url(self):
+        self.assertTrue(STV.can_handle_url('https://player.stv.tv/live'))
+        self.assertTrue(STV.can_handle_url('http://player.stv.tv/live'))
+
+    def test_can_handle_url_negative(self):
+        self.assertFalse(STV.can_handle_url('http://example.com/live'))
diff --git a/tests/plugins/test_vtvgo.py b/tests/plugins/test_vtvgo.py
new file mode 100644
index 00000000..7cfacf5c
--- /dev/null
+++ b/tests/plugins/test_vtvgo.py
@@ -0,0 +1,32 @@
+import unittest
+
+from streamlink.plugins.vtvgo import VTVgo
+
+
+class TestPluginVTVgo(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv1-1.html',
+            'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv2-2.html',
+            'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv3-3.html',
+            'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv4-4.html',
+            'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv5-5.html',
+            'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv6-6.html',
+            'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv7-27.html',
+            'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv8-36.html',
+            'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv9-39.html',
+            'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv5-t%C3%A2y-nam-b%E1%BB%99-7.html',
+            'https://vtvgo.vn/xem-truc-tuyen-kenh-k%C3%AAnh-vtv5-t%C3%A2y-nguy%C3%AAn-163.html',
+        ]
+        for url in should_match:
+            self.assertTrue(VTVgo.can_handle_url(url))
+
+        should_not_match = [
+            'https://example.com/index.html',
+            # POST request will error with www.
+            'https://www.vtvgo.vn/xem-truc-tuyen-kenh-vtv1-1.html',
+            # POST request will error with http://
+            'http://vtvgo.vn/xem-truc-tuyen-kenh-vtv1-1.html',
+        ]
+        for url in should_not_match:
+            self.assertFalse(VTVgo.can_handle_url(url))
diff --git a/tests/test_api_validate.py b/tests/test_api_validate.py
index d3efc028..c441bcd2 100644
--- a/tests/test_api_validate.py
+++ b/tests/test_api_validate.py
@@ -89,7 +89,7 @@ class TestPluginAPIValidate(unittest.TestCase):
         assert validate(get("invalidkey", "default"), {"key": "value"}) == "default"
 
     def test_get_re(self):
-        m = re.match("(\d+)p", "720p")
+        m = re.match(r"(\d+)p", "720p")
         assert validate(get(1), m) == "720"
 
     def test_getattr(self):
diff --git a/tests/test_cli_main.py b/tests/test_cli_main.py
index 2b58ea5f..a4c0d0d1 100644
--- a/tests/test_cli_main.py
+++ b/tests/test_cli_main.py
@@ -1,15 +1,19 @@
 import os.path
-import os.path
 import tempfile
 
 import streamlink_cli.main
-from streamlink_cli.main import resolve_stream_name, format_valid_streams, check_file_output
-from streamlink_cli.output import FileOutput
+from streamlink_cli.main import resolve_stream_name, format_valid_streams, check_file_output, create_output
+from streamlink_cli.output import FileOutput, PlayerOutput
 from streamlink.plugin.plugin import Plugin
 import unittest
 from tests.mock import Mock, patch
 
 
+class FakePlugin:
+    @classmethod
+    def stream_weight(cls, stream):
+        return Plugin.stream_weight(stream)
+
 class TestCLIMain(unittest.TestCase):
     def test_check_file_output(self):
         streamlink_cli.main.console = Mock()
@@ -89,10 +93,6 @@ class TestCLIMain(unittest.TestCase):
         self.assertEqual(resolve_stream_name(streams, "best-unfiltered"), "1080p")
 
     def test_format_valid_streams(self):
-        class FakePlugin:
-            @classmethod
-            def stream_weight(cls, stream):
-                return Plugin.stream_weight(stream)
         a = Mock()
         b = Mock()
         c = Mock()
@@ -128,3 +128,70 @@ class TestCLIMain(unittest.TestCase):
                 "1080p (best-unfiltered)"
             ])
         )
+
+    def test_create_output_no_file_output_options(self):
+        streamlink_cli.main.console = Mock()
+        streamlink_cli.main.args = args = Mock()
+        args.output = None
+        args.stdout = None
+        args.record = None
+        args.record_and_pipe = None
+        args.title = None
+        args.player = "mpv"
+        self.assertIsInstance(create_output(FakePlugin), PlayerOutput)
+
+    def test_create_output_file_output(self):
+        tmpfile = tempfile.NamedTemporaryFile()
+        try:
+            streamlink_cli.main.args = args = Mock()
+            streamlink_cli.main.console = Mock()
+            args.output = tmpfile.name
+            self.assertTrue(os.path.exists(tmpfile.name))
+            self.assertIsInstance(create_output(FakePlugin), FileOutput)
+        finally:
+            tmpfile.close()
+
+    def test_create_output_stdout(self):
+        streamlink_cli.main.console = Mock()
+        streamlink_cli.main.args = args = Mock()
+        args.output = None
+        args.stdout = True
+        self.assertIsInstance(create_output(FakePlugin), FileOutput)
+
+    def test_create_output_record_and_pipe(self):
+        tmpfile = tempfile.NamedTemporaryFile()
+        try:
+            streamlink_cli.main.console = Mock()
+            streamlink_cli.main.args = args = Mock()
+            args.output = None
+            args.stdout = None
+            args.record_and_pipe = tmpfile.name
+            self.assertIsInstance(create_output(FakePlugin), FileOutput)
+        finally:
+            tmpfile.close()
+
+    def test_create_output_record(self):
+        tmpfile = tempfile.NamedTemporaryFile()
+        try:
+            streamlink_cli.main.console = Mock()
+            streamlink_cli.main.args = args = Mock()
+            args.output = None
+            args.stdout = None
+            args.record = tmpfile.name
+            args.record_and_pipe = None
+            args.title = None
+            args.player = "mpv"
+            args.player_fifo = None
+            self.assertIsInstance(create_output(FakePlugin), PlayerOutput)
+        finally:
+            tmpfile.close()
+
+    def test_create_output_record_and_other_file_output(self):
+        streamlink_cli.main.console = console = Mock()
+        streamlink_cli.main.args = args = Mock()
+        console.exit = Mock()
+        args.output = None
+        args.stdout = True
+        args.record_and_pipe = True
+        create_output(FakePlugin)
+        console.exit.assert_called_with("Cannot use record options with other file output options.")
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index fb7feea4..9221863b 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -7,6 +7,7 @@ import unittest
 
 import streamlink.plugins
 from streamlink import Streamlink
+from streamlink.utils import load_module
 
 
 class PluginTestMeta(type):
@@ -14,8 +15,7 @@ class PluginTestMeta(type):
         plugin_path = os.path.dirname(streamlink.plugins.__file__)
         plugins = []
         for loader, pname, ispkg in pkgutil.iter_modules([plugin_path]):
-            file, pathname, desc = imp.find_module(pname, [plugin_path])
-            module = imp.load_module(pname, file, pathname, desc)
+            module = load_module(pname, plugin_path)
             if hasattr(module, "__plugin__"):
                 plugins.append((pname))