Repository: streamlink/streamlink
Pull request #1986 (2018-08-04 15:03:48)

Plugin Request: new Plugin for Latvian live TV channels on ltv.lsm.lv
# Plugin request
- [x] I have read the contribution and plugin request guidelines. 
## Description
LTV.LSM.LV is the official website of the Latvian state-owned television. It contains the live streams of 3 TV channels.
## Example stream URLs
1. Live stream: https://ltv.lsm.lv/lv/tieshraide/ltv1
2. Live stream: https://ltv.lsm.lv/lv/tieshraide/ltv7/live.782/
3. Live stream: https://ltv.lsm.lv/lv/tieshraide/visiemltv.lv/live.1480/
## Comments, screenshots, etc.
Some of the auditions are geo-resticted to Latvia.
Only handling the live TV channels streams has been implemented.
Although a single stream contains the links to 3 different resolutions (and handling setting them has been implemented) at the moment the broadcaster set all of them seem point to the same stream.
For testers - the 3-rd stream seems to be the least frequently geo-restricted.


Patch:
diff --git a/.github/ISSUE_TEMPLATE/Bug_Report.md b/.github/ISSUE_TEMPLATE/bug_report.md
similarity index 100%
rename from .github/ISSUE_TEMPLATE/Bug_Report.md
rename to .github/ISSUE_TEMPLATE/bug_report.md
diff --git a/.github/ISSUE_TEMPLATE/Feature_Request.md b/.github/ISSUE_TEMPLATE/feature_request.md
similarity index 100%
rename from .github/ISSUE_TEMPLATE/Feature_Request.md
rename to .github/ISSUE_TEMPLATE/feature_request.md
diff --git a/.github/ISSUE_TEMPLATE/Plugin_Issue.md b/.github/ISSUE_TEMPLATE/plugin_issue.md
similarity index 100%
rename from .github/ISSUE_TEMPLATE/Plugin_Issue.md
rename to .github/ISSUE_TEMPLATE/plugin_issue.md
diff --git a/.github/ISSUE_TEMPLATE/Plugin_Request.md b/.github/ISSUE_TEMPLATE/plugin_request.md
similarity index 100%
rename from .github/ISSUE_TEMPLATE/Plugin_Request.md
rename to .github/ISSUE_TEMPLATE/plugin_request.md
diff --git a/.github/issue_template.md b/.github/issue_template.md
new file mode 100644
index 00000000..220e250a
--- /dev/null
+++ b/.github/issue_template.md
@@ -0,0 +1,66 @@
+<!--
+Thanks for reporting an issue!
+USE THE TEMPLATE. Otherwise your issue may be rejected.
+
+This template should only be used if your issue doesn't match the other more specific ones.
+
+First, see the contribution guidelines:
+https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink
+
+Also check the list of open and closed issues:
+https://github.com/streamlink/streamlink/issues?q=is%3Aissue
+
+Please see the text preview to avoid unnecessary formatting errors.
+-->
+
+
+## Issue
+
+<!-- Replace [ ] with [x] in order to check the box -->
+- [ ] This is not a bug report, feature request, or plugin issue/request.
+- [ ] I have read the contribution guidelines.
+
+
+### Description
+
+<!-- Explain the issue as thoroughly as you can. -->
+
+
+### Expected / Actual behavior
+
+<!-- What do you expect to happen, and what is actually happening? -->
+
+
+### Reproduction steps / Explicit stream URLs to test
+
+<!-- How can we reproduce this? Please note the exact steps below using the list format supplied. If you need more steps please add them. -->
+
+1. ...
+2. ...
+3. ...
+
+
+### Logs
+
+<!--
+TEXT LOG OUTPUT IS OPTIONAL for generic issues!
+However, depending on the issue, log output can be useful for the developers to understand the problem. It also includes information about your platform. If you don't intend to include log output, please at least provide these platform details.
+Thanks!
+
+Use the `--loglevel debug` parameter and avoid using parameters which suppress log output.
+https://streamlink.github.io/cli.html#cmdoption-l
+
+Make sure to **remove usernames and passwords**
+You can copy the output to https://gist.github.com/ or paste it below.
+-->
+
+```
+REPLACE THIS TEXT WITH THE LOG OUTPUT
+```
+
+
+### Additional comments, screenshots, etc.
+
+
+
+[Love Streamlink? Please consider supporting our collective. Thanks!](https://opencollective.com/streamlink/donate)
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
deleted file mode 100644
index 61b74d7a..00000000
--- a/ISSUE_TEMPLATE.md
+++ /dev/null
@@ -1,56 +0,0 @@
-<!--
-Thanks for reporting an issue!
-
-This template should only be used if your issue does not fall into one of the existing templates.
-
-Please read the contribution guidelines (https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink) first!
-
-Also check the list of known issues before reporting an issue!
-
-Please fill out the following template. Be as detailed as possible.
-
-Please see the text preview to avoid unnecessary formatting errors.
--->
-
-## Issue
-
-- [x] This is not a bug report, feature request, or plugin (improvement) request.
-- [ ] I have read the contribution guidelines. <!-- Replace the space with an x to check the box: [x] -->
-
-### Description
-
-<!-- Explain the issue as clearly as you can. How is functionality impacted? Which OS/Player/Streamlink version are you using? Etc. -->
-
-
-### Expected / Actual behavior
-
-<!-- What do you expect to happen, and what is actually happening? -->
-
-
-### Reproduction steps / Explicit stream URLs to test
-
-<!-- How can we reproduce this (if it can be)? Please note the exact steps below using the list format supplied, if you need more steps please add them. -->
-
-1. ...
-2. ...
-3. ...
-
-### Logs
-
-<!--
-While logs are not required for an issue they can be helpful. Use `-l debug` [(help)](https://streamlink.github.io/cli.html#cmdoption-l) and avoid using `--stream-url` as that will suppress log output.
-
-Make sure to **remove username and password**
-
-You can upload your logs to https://gist.github.com/ or place your logs below.
--->
-
-```
-REPLACE THIS TEXT WITH YOUR LOG
-```
-
-### Additional comments, screenshots, etc.
-
-
-
-[Love Streamlink? Please consider supporting our collective. Thanks!](https://opencollective.com/streamlink/donate)
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 1e22aeae..0b88f176 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -7,4 +7,3 @@ mock
 requests-mock
 pynsist
 freezegun
-unittest2; python_version < '2.7'
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index 36b74f19..459469f8 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -118,6 +118,7 @@ liveedu                 - liveedu.tv         Yes   --    Some streams require a
 liveme                  liveme.com           Yes   --
 livestream              new.livestream.com   Yes   --
 lrt                     lrt.lt               Yes   No
+ltv_lsm_lv              ltv.lsm.lv           Yes   No    Streams may be geo-restricted to Latvia.
 mediaklikk              mediaklikk.hu        Yes   No    Streams may be geo-restricted to Hungary.
 mips                    mips.tv              Yes   --    Requires rtmpdump with K-S-V patches.
 mitele                  mitele.es            Yes   No    Streams may be geo-restricted to Spain.
diff --git a/script/makeinstaller.sh b/script/makeinstaller.sh
index 68b2f385..1b406777 100755
--- a/script/makeinstaller.sh
+++ b/script/makeinstaller.sh
@@ -33,7 +33,7 @@ entry_point=streamlink_cli.main:main
 icon=../win32/doggo.ico
 
 [Python]
-version=3.5.2
+version=3.6.6
 format=bundled
 
 [Include]
@@ -66,7 +66,7 @@ packages=pkg_resources
          socks
          sockshandler
          isodate
-pypi_wheels=pycryptodome==3.4.3
+pypi_wheels=pycryptodome==3.6.4
 
 files=../win32/LICENSE.txt > \$INSTDIR
       ../build/lib/streamlink > \$INSTDIR\pkgs
diff --git a/src/streamlink/__init__.py b/src/streamlink/__init__.py
index d401efa4..8cb33913 100644
--- a/src/streamlink/__init__.py
+++ b/src/streamlink/__init__.py
@@ -9,19 +9,6 @@ An API is also provided that allows direct access to stream data.
 Full documentation is available at https://streamlink.github.io.
 
 """
-import warnings
-from sys import version_info
-
-
-if version_info[:2] == (2, 6):
-    warnings.warn(
-        "Python 2.6 is no longer supported by the Python core team, please "
-        "upgrade your Python. A future version of streamlink will drop "
-        "support for Python 2.6",
-        DeprecationWarning
-    )
-
-
 from ._version import get_versions
 __version__ = get_versions()['version']
 del get_versions
@@ -87,4 +74,3 @@ from .api import streams
 from .exceptions import (StreamlinkError, PluginError, NoStreamsError,
                          NoPluginError, StreamError)
 from .session import Streamlink
-
diff --git a/src/streamlink/plugin/plugin.py b/src/streamlink/plugin/plugin.py
index 0cfa5d0c..1f5513d7 100644
--- a/src/streamlink/plugin/plugin.py
+++ b/src/streamlink/plugin/plugin.py
@@ -311,22 +311,6 @@ class Plugin(object):
         :param sorting_excludes: Specify which streams to exclude from
                                  the best/worst synonyms.
 
-
-        .. versionchanged:: 1.4.2
-           Added *priority* parameter.
-
-        .. versionchanged:: 1.5.0
-           Renamed *priority* to *stream_types* and changed behaviour
-           slightly.
-
-        .. versionchanged:: 1.5.0
-           Added *sorting_excludes* parameter.
-
-        .. versionchanged:: 1.6.0
-           *sorting_excludes* can now be a list of filter expressions
-           or a function that is passed to filter().
-
-
         """
 
         try:
@@ -421,15 +405,6 @@ class Plugin(object):
 
         return final_sorted_streams
 
-    def get_streams(self, *args, **kwargs):
-        """Deprecated since version 1.9.0.
-
-        Has been renamed to :func:`Plugin.streams`, this is an alias
-        for backwards compatibility.
-        """
-
-        return self.streams(*args, **kwargs)
-
     def _get_streams(self):
         raise NotImplementedError
 
diff --git a/src/streamlink/plugins/drdk.py b/src/streamlink/plugins/drdk.py
index 9182dd19..bdd848af 100644
--- a/src/streamlink/plugins/drdk.py
+++ b/src/streamlink/plugins/drdk.py
@@ -6,8 +6,8 @@ from streamlink.plugin import Plugin
 from streamlink.plugin.api import validate
 from streamlink.stream import HLSStream, HDSStream
 
-LIVE_CHANNELS_API_URL = "http://www.dr.dk/tv/external/channels?mediaType=tv"
-VOD_API_URL = "http://www.dr.dk/mu/programcard/expanded/{0}"
+LIVE_CHANNELS_API_URL = "https://www.dr.dk/mu-online/api/1.4/channel/{0}"
+VOD_API_URL = "https://www.dr.dk/mu/programcard/expanded/{0}"
 
 STREAMING_TYPES = {
     "HDS": HDSStream.parse_manifest,
@@ -29,34 +29,31 @@ _url_re = re.compile(r"""
 
 _channels_schema = validate.Schema(
     {
-        "Data": [{
-            "Slug": validate.text,
-            "StreamingServers": validate.all(
-                [{
-                    "LinkType": validate.text,
-                    "Qualities": [
-                        validate.all(
-                            {
-                                "Streams": validate.all(
-                                    [
-                                        validate.all(
-                                            {"Stream": validate.text},
-                                            validate.get("Stream")
-                                        )
-                                    ],
-                                    validate.get(0)
-                                )
-                            },
-                            validate.get("Streams")
-                        )
-                    ],
-                    "Server": validate.text
-                }],
-                validate.filter(lambda s: s["LinkType"] in STREAMING_TYPES)
-            )
-        }]
+        "StreamingServers": validate.all(
+            [{
+                "LinkType": validate.text,
+                "Qualities": [
+                    validate.all(
+                        {
+                            "Streams": validate.all(
+                                [
+                                    validate.all(
+                                        {"Stream": validate.text},
+                                        validate.get("Stream")
+                                    )
+                                ],
+                                validate.get(0)
+                            )
+                        },
+                        validate.get("Streams")
+                    )
+                ],
+                "Server": validate.text
+            }],
+            validate.filter(lambda s: s["LinkType"] in STREAMING_TYPES)
+        )
     },
-    validate.get("Data", {})
+    validate.get("StreamingServers", {})
 )
 
 _video_schema = validate.Schema(
@@ -85,7 +82,6 @@ class DRDK(Plugin):
     def can_handle_url(cls, url):
         return _url_re.match(url)
 
-    @Plugin.broken()
     def _get_streams(self):
         match = _url_re.match(self.url)
         if not match:
@@ -114,12 +110,9 @@ class DRDK(Plugin):
         return streams
 
     def _get_live_streams(self, slug):
-        res = self.session.http.get(LIVE_CHANNELS_API_URL)
-        res = self.session.http.json(res, schema=_channels_schema)
-
-        for channel in filter(lambda c: c["Slug"] == slug, res):
-            servers = channel["StreamingServers"]
-            return self._parse_streaming_servers(servers)
+        res = self.session.http.get(LIVE_CHANNELS_API_URL.format(slug))
+        servers = self.session.http.json(res, schema=_channels_schema)
+        return self._parse_streaming_servers(servers)
 
     def _parse_streaming_servers(self, servers):
         streams = {}
diff --git a/src/streamlink/plugins/ltv_lsm_lv.py b/src/streamlink/plugins/ltv_lsm_lv.py
new file mode 100644
index 00000000..b8a72186
--- /dev/null
+++ b/src/streamlink/plugins/ltv_lsm_lv.py
@@ -0,0 +1,54 @@
+import logging
+import re
+
+from streamlink.compat import urlparse
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import useragents
+from streamlink.plugin.api.utils import itertags
+from streamlink.stream import HLSStream
+
+log = logging.getLogger(__name__)
+
+
+class LtvLsmLv(Plugin):
+    """
+    Support for Latvian live channels streams on ltv.lsm.lv
+    """
+    url_re = re.compile(r"https?://ltv\.lsm\.lv/lv/tieshraide")
+
+    @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({
+           "Referer": self.url,
+           "User-Agent": useragents.FIREFOX
+        })
+
+        iframe_url = None
+        res = self.session.http.get(self.url)
+        for iframe in itertags(res.text, "iframe"):
+            if "embed.lsm.lv" in iframe.attributes.get("src"):
+                iframe_url = iframe.attributes.get("src")
+                break
+
+        if not iframe_url:
+            log.error("Could not find player iframe")
+            return
+
+        log.debug("Found iframe: {0}".format(iframe_url))
+        res = self.session.http.get(iframe_url)
+        for source in itertags(res.text, "source"):
+            if source.attributes.get("src"):
+                stream_url = source.attributes.get("src")
+                url_path = urlparse(stream_url).path
+                if url_path.endswith(".m3u8"):
+                    for s in HLSStream.parse_variant_playlist(self.session,
+                                                              stream_url).items():
+                        yield s
+                else:
+                    log.debug("Not used URL path: {0}".format(url_path))
+
+
+__plugin__ = LtvLsmLv
diff --git a/src/streamlink/plugins/sportal.py b/src/streamlink/plugins/sportal.py
index 977a4dc0..fc9cde33 100644
--- a/src/streamlink/plugins/sportal.py
+++ b/src/streamlink/plugins/sportal.py
@@ -1,56 +1,35 @@
-from __future__ import print_function
+import logging
 import re
 
 from streamlink.plugin import Plugin
-from streamlink.plugin.api import validate
-from streamlink.stream import RTMPStream
-from streamlink.plugins.common_jwplayer import _js_to_json
-from streamlink.utils import parse_json
+from streamlink.plugin.api import useragents
+from streamlink.stream import HLSStream
+
+log = logging.getLogger(__name__)
 
 
 class Sportal(Plugin):
-    swf_url = "http://img2.sportal.bg/images/svp/videoplayer.swf"
-    url_re = re.compile(r"https?://(?:www\.)?sportal\.bg/sportal_live_tv.php.*")
-    _playlist_re = re.compile(r"\(\{.*playlist: (\[.*\]),.*?\}\);", re.DOTALL)
-    _playlist_schema = validate.Schema(
-        validate.transform(_playlist_re.search),
-        validate.any(
-            None,
-            validate.all(
-                validate.get(1),
-                validate.transform(_js_to_json),
-                validate.transform(parse_json),
-                [{
-                    "streamer": validate.url(scheme="rtmp"),
-                    "levels": [
-                        {"bitrate": int, "file": validate.text}
-                    ]
-                }],
-                validate.get(0)
-            )
-        )
-    )
+
+    _url_re = re.compile(
+        r'https?://(?:www\.)?sportal\.bg/sportal_live_tv.php.*')
+    _hls_re = re.compile(r'''["'](?P<url>[^"']+\.m3u8[^"']*?)["']''')
 
     @classmethod
     def can_handle_url(cls, url):
-        return cls.url_re.match(url) is not None
+        return cls._url_re.match(url) is not None
 
-    @Plugin.broken(1165)
     def _get_streams(self):
+        self.session.http.headers.update({'User-Agent': useragents.FIREFOX})
         res = self.session.http.get(self.url)
-
-        playlist = self._playlist_schema.validate(res.text)
-        if playlist:
-            for level in playlist["levels"]:
-                q = "{0}k".format(level["bitrate"])
-                s = RTMPStream(self.session,
-                               redirect=True,
-                               params={"rtmp": playlist["streamer"],
-                                       "playpath": level["file"],
-                                       "pageUrl": self.url,
-                                       "z": True,
-                                       "swfUrl": self.swf_url})
-                yield q, s
+        m = self._hls_re.search(res.text)
+        if not m:
+            return
+
+        hls_url = m.group('url')
+        log.debug('URL={0}'.format(hls_url))
+        log.warning('SSL certificate verification is disabled.')
+        return HLSStream.parse_variant_playlist(
+            self.session, hls_url, verify=False).items()
 
 
 __plugin__ = Sportal
diff --git a/src/streamlink/plugins/twitch.py b/src/streamlink/plugins/twitch.py
index 4ef9487f..5cb3f41c 100644
--- a/src/streamlink/plugins/twitch.py
+++ b/src/streamlink/plugins/twitch.py
@@ -47,7 +47,7 @@ _url_re = re.compile(r"""
     )
     (?:
         /
-        (?P<video_type>[bcv])
+        (?P<video_type>[bcv])(?:ideo)?
         /
         (?P<video_id>\d+)
     )?
diff --git a/src/streamlink/stream/dash.py b/src/streamlink/stream/dash.py
index 1d1c5b65..7339735c 100644
--- a/src/streamlink/stream/dash.py
+++ b/src/streamlink/stream/dash.py
@@ -227,7 +227,7 @@ class DASHStream(Stream):
             stream_name = []
 
             if vid:
-                stream_name.append("{:0.0f}{}".format(vid.height or vid.bandwidth, "p" if vid.height else "k"))
+                stream_name.append("{:0.0f}{}".format(vid.height or vid.bandwidth_rounded, "p" if vid.height else "k"))
             if audio and len(audio) > 1:
                 stream_name.append("a{:0.0f}k".format(aud.bandwidth))
             ret['+'.join(stream_name)] = stream
diff --git a/src/streamlink/stream/dash_manifest.py b/src/streamlink/stream/dash_manifest.py
index a227e894..cb3d76aa 100644
--- a/src/streamlink/stream/dash_manifest.py
+++ b/src/streamlink/stream/dash_manifest.py
@@ -8,6 +8,8 @@ import time
 
 from collections import defaultdict, namedtuple
 from itertools import repeat, count
+
+import math
 from isodate import parse_datetime, parse_duration, Duration
 from contextlib import contextmanager
 from streamlink.compat import urlparse, urljoin, urlunparse, izip, urlsplit, urlunsplit
@@ -580,6 +582,10 @@ class Representation(MPDNode):
         self.segmentList = self.children(SegmentList)
         self.segmentTemplate = self.only_child(SegmentTemplate)
 
+    @property
+    def bandwidth_rounded(self):
+        return round(self.bandwidth, 1 - int(math.log10(self.bandwidth)))
+
     def segments(self, **kwargs):
         """
         Segments are yielded when they are available
diff --git a/src/streamlink/stream/hls.py b/src/streamlink/stream/hls.py
index 87890f4f..8a67f564 100644
--- a/src/streamlink/stream/hls.py
+++ b/src/streamlink/stream/hls.py
@@ -318,9 +318,6 @@ class HLSStream(HTTPStream):
     - :attr:`args` A :class:`dict` containing keyword arguments passed
       to :meth:`requests.request`, such as headers and cookies.
 
-    .. versionchanged:: 1.7.0
-       Added *args* attribute.
-
     """
 
     __shortname__ = "hls"
diff --git a/src/streamlink/utils/__init__.py b/src/streamlink/utils/__init__.py
index 01b67a06..25899edd 100644
--- a/src/streamlink/utils/__init__.py
+++ b/src/streamlink/utils/__init__.py
@@ -8,7 +8,7 @@ try:
 except ImportError:  # pragma: no cover
     import xml.etree.ElementTree as ET
 
-from streamlink.compat import urljoin, urlparse, parse_qsl, is_py2, urlunparse, is_py3
+from streamlink.compat import urljoin, urlparse, parse_qsl, is_py2, is_py3
 from streamlink.exceptions import PluginError
 from streamlink.utils.named_pipe import NamedPipe
 from streamlink.utils.lazy_formatter import LazyFormatter
@@ -202,91 +202,6 @@ def load_module(name, path=None):
             if fd:
                 fd.close()
 
-#####################################
-# Deprecated functions, do not use. #
-#####################################
-
-import requests
-
-
-def urlget(url, *args, **kwargs):  # pragma: no cover
-    """This function is deprecated."""
-    data = kwargs.pop("data", None)
-    exception = kwargs.pop("exception", PluginError)
-    method = kwargs.pop("method", "GET")
-    session = kwargs.pop("session", None)
-    timeout = kwargs.pop("timeout", 20)
-
-    if data is not None:
-        method = "POST"
-
-    try:
-        if session:
-            res = session.request(method, url, timeout=timeout, data=data,
-                                  *args, **kwargs)
-        else:
-            res = requests.request(method, url, timeout=timeout, data=data,
-                                   *args, **kwargs)
-
-        res.raise_for_status()
-    except (requests.exceptions.RequestException, IOError) as rerr:
-        err = exception("Unable to open URL: {url} ({err})".format(url=url,
-                                                                   err=rerr))
-        err.err = rerr
-        raise err
-
-    return res
-
-
-urlopen = urlget
-
-
-def urlresolve(url):  # pragma: no cover
-    """This function is deprecated."""
-    res = urlget(url, stream=True, allow_redirects=False)
-
-    if res.status_code == 302 and "location" in res.headers:
-        return res.headers["location"]
-    else:
-        return url
-
-
-def res_xml(res, *args, **kw):  # pragma: no cover
-    """This function is deprecated."""
-    return parse_xml(res.text, *args, **kw)
-
-
-def res_json(res, jsontype="JSON", exception=PluginError):  # pragma: no cover
-    """This function is deprecated."""
-    try:
-        jsondata = res.json()
-    except ValueError as err:
-        if len(res.text) > 35:
-            snippet = res.text[:35] + "..."
-        else:
-            snippet = res.text
-
-        raise exception("Unable to parse {0}: {1} ({2})".format(jsontype, err,
-                                                                snippet))
-
-    return jsondata
-
-
-import hmac
-import hashlib
-
-SWF_KEY = b"Genuine Adobe Flash Player 001"
-
-
-def swfverify(url):  # pragma: no cover
-    """This function is deprecated."""
-    res = urlopen(url)
-    swf = swfdecompress(res.content)
-
-    h = hmac.new(SWF_KEY, swf, hashlib.sha256)
-
-    return h.hexdigest(), len(swf)
-
 
 def escape_librtmp(value):  # pragma: no cover
     if isinstance(value, bool):
@@ -301,8 +216,8 @@ def escape_librtmp(value):  # pragma: no cover
     return value
 
 
-__all__ = ["urlopen", "urlget", "urlresolve", "swfdecompress", "swfverify",
-           "verifyjson", "absolute_url", "parse_qsd", "parse_json", "res_json",
-           "parse_xml", "res_xml", "rtmpparse", "prepend_www", "NamedPipe",
+__all__ = ["swfdecompress", "update_scheme", "url_equal",
+           "verifyjson", "absolute_url", "parse_qsd", "parse_json",
+           "parse_xml", "rtmpparse", "prepend_www", "NamedPipe",
            "escape_librtmp", "LazyFormatter", "get_filesystem_encoding",
-           "maybe_decode", "maybe_encode"]
\ No newline at end of file
+           "maybe_decode", "maybe_encode"]
diff --git a/src/streamlink_cli/argparser.py b/src/streamlink_cli/argparser.py
index 9858b5a7..f2db5c9b 100644
--- a/src/streamlink_cli/argparser.py
+++ b/src/streamlink_cli/argparser.py
@@ -1105,22 +1105,6 @@ def build_parser():
     )
 
     # Deprecated options
-    stream.add_argument(
-        "--best-stream-default",
-        action="store_true",
-        help=argparse.SUPPRESS
-    )
-    player.add_argument(
-        "-q", "--quiet-player",
-        action="store_true",
-        help=argparse.SUPPRESS
-    )
-    transport.add_argument(
-        "--hds-fragment-buffer",
-        type=int,
-        metavar="fragments",
-        help=argparse.SUPPRESS
-    )
     http.add_argument(
         "--http-cookies",
         metavar="COOKIES",
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index 75917198..3fe315ce 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -433,8 +433,8 @@ def handle_stream(plugin, streams, stream_name):
 def fetch_streams(plugin):
     """Fetches streams using correct parameters."""
 
-    return plugin.get_streams(stream_types=args.stream_types,
-                              sorting_excludes=args.stream_sorting_excludes)
+    return plugin.streams(stream_types=args.stream_types,
+                          sorting_excludes=args.stream_sorting_excludes)
 
 
 def fetch_streams_with_retry(plugin, interval, count):
@@ -563,9 +563,6 @@ def handle_url():
     if not streams:
         console.exit("No playable streams found on this URL: {0}", args.url)
 
-    if args.best_stream_default:
-        args.default_stream = ["best"]
-
     if args.default_stream and not args.stream and not args.json:
         args.stream = args.default_stream
 
@@ -689,17 +686,6 @@ def setup_console(output):
 
     # All console related operations is handled via the ConsoleOutput class
     console = ConsoleOutput(output, streamlink)
-
-    if args.quiet_player:
-        log.warning("The option --quiet-player is deprecated since "
-                    "version 1.4.3 as hiding player output is now "
-                    "the default.")
-
-    if args.best_stream_default:
-        log.warning("The option --best-stream-default is deprecated "
-                    "since version 1.9.0, use '--default-stream best' "
-                    "instead.")
-
     console.json = args.json
 
     # Handle SIGTERM just like SIGINT
@@ -859,12 +845,6 @@ def setup_options():
     streamlink.set_option("subprocess-errorlog-path", args.subprocess_errorlog_path)
     streamlink.set_option("locale", args.locale)
 
-    # Deprecated options
-    if args.hds_fragment_buffer:
-        log.warning("The option --hds-fragment-buffer is deprecated "
-                    "and will be removed in the future. Use "
-                    "--ringbuffer-size instead")
-
 
 def setup_plugin_args(session, parser):
     """Sets Streamlink plugin options."""


Test patch:
diff --git a/tests/plugins/test_ltv_lsm_lv.py b/tests/plugins/test_ltv_lsm_lv.py
new file mode 100644
index 00000000..02b6b7a1
--- /dev/null
+++ b/tests/plugins/test_ltv_lsm_lv.py
@@ -0,0 +1,21 @@
+import unittest
+
+from streamlink.plugins.ltv_lsm_lv import LtvLsmLv
+
+
+class TestPluginLtvLsmLv(unittest.TestCase):
+    def test_can_handle_url(self):
+        self.assertTrue(LtvLsmLv.can_handle_url("https://ltv.lsm.lv/lv/tieshraide/example/"))
+        self.assertTrue(LtvLsmLv.can_handle_url("http://ltv.lsm.lv/lv/tieshraide/example/"))
+        self.assertTrue(LtvLsmLv.can_handle_url("https://ltv.lsm.lv/lv/tieshraide/example/live.123/"))
+        self.assertTrue(LtvLsmLv.can_handle_url("http://ltv.lsm.lv/lv/tieshraide/example/live.123/"))
+
+    def test_can_handle_url_negative(self):
+        self.assertFalse(LtvLsmLv.can_handle_url("https://ltv.lsm.lv"))
+        self.assertFalse(LtvLsmLv.can_handle_url("http://ltv.lsm.lv"))
+        self.assertFalse(LtvLsmLv.can_handle_url("https://ltv.lsm.lv/lv"))
+        self.assertFalse(LtvLsmLv.can_handle_url("http://ltv.lsm.lv/lv"))
+        self.assertFalse(LtvLsmLv.can_handle_url("https://ltv.lsm.lv/other-site/"))
+        self.assertFalse(LtvLsmLv.can_handle_url("http://ltv.lsm.lv/other-site/"))
+        self.assertFalse(LtvLsmLv.can_handle_url("https://ltv.lsm.lv/lv/other-site/"))
+        self.assertFalse(LtvLsmLv.can_handle_url("http://ltv.lsm.lv/lv/other-site/"))
diff --git a/tests/plugins/test_stream.py b/tests/plugins/test_stream.py
index ae8e53c9..c52c29a2 100644
--- a/tests/plugins/test_stream.py
+++ b/tests/plugins/test_stream.py
@@ -1,7 +1,8 @@
+import unittest
+
 from streamlink import Streamlink
 from streamlink.plugin.plugin import stream_weight, parse_params
-from streamlink.stream import *
-import unittest
+from streamlink.stream import AkamaiHDStream, HLSStream, HTTPStream, RTMPStream
 from tests.mock import patch
 
 
@@ -15,7 +16,7 @@ class TestPluginStream(unittest.TestCase):
 
     def _test_akamaihd(self, surl, url):
         channel = self.session.resolve_url(surl)
-        streams = channel.get_streams()
+        streams = channel.streams()
 
         self.assertTrue("live" in streams)
 
@@ -28,7 +29,7 @@ class TestPluginStream(unittest.TestCase):
         mock_parse.return_value = {}
 
         channel = self.session.resolve_url(surl)
-        streams = channel.get_streams()
+        streams = channel.streams()
 
         self.assertTrue("live" in streams)
         mock_parse.assert_called_with(self.session, url)
@@ -42,7 +43,7 @@ class TestPluginStream(unittest.TestCase):
         mock_parse.return_value = {"best": HLSStream(self.session, url)}
 
         channel = self.session.resolve_url(surl)
-        streams = channel.get_streams()
+        streams = channel.streams()
 
         mock_parse.assert_called_with(self.session, url)
 
@@ -55,7 +56,7 @@ class TestPluginStream(unittest.TestCase):
 
     def _test_rtmp(self, surl, url, params):
         channel = self.session.resolve_url(surl)
-        streams = channel.get_streams()
+        streams = channel.streams()
 
         self.assertTrue("live" in streams)
 
@@ -66,7 +67,7 @@ class TestPluginStream(unittest.TestCase):
 
     def _test_http(self, surl, url, params):
         channel = self.session.resolve_url(surl)
-        streams = channel.get_streams()
+        streams = channel.streams()
 
         self.assertTrue("live" in streams)
 
diff --git a/tests/plugins/test_tvplayer.py b/tests/plugins/test_tvplayer.py
index 8b010122..83568ffb 100644
--- a/tests/plugins/test_tvplayer.py
+++ b/tests/plugins/test_tvplayer.py
@@ -55,7 +55,7 @@ class TestPluginTVPlayer(unittest.TestCase):
         TVPlayer.bind(self.session, "test.tvplayer")
         plugin = TVPlayer("http://tvplayer.com/watch/dave")
 
-        streams = plugin.get_streams()
+        streams = plugin.streams()
 
         self.assertTrue("test" in streams)
 
@@ -77,7 +77,7 @@ class TestPluginTVPlayer(unittest.TestCase):
         TVPlayer.bind(self.session, "test.tvplayer")
         plugin = TVPlayer("http://tvplayer.com/watch/dave")
 
-        streams = plugin.get_streams()
+        streams = plugin.streams()
 
         self.assertEqual({}, streams)
 
diff --git a/tests/plugins/test_twitch.py b/tests/plugins/test_twitch.py
index 1dce1177..53553010 100644
--- a/tests/plugins/test_twitch.py
+++ b/tests/plugins/test_twitch.py
@@ -9,6 +9,8 @@ class TestPluginTwitch(unittest.TestCase):
             'https://www.twitch.tv/twitch',
             'https://www.twitch.tv/videos/150942279',
             'https://clips.twitch.tv/ObservantBenevolentCarabeefPhilosoraptor',
+            'https://www.twitch.tv/twitch/video/292713971',
+            'https://www.twitch.tv/twitch/v/292713971',
         ]
         for url in should_match:
             self.assertTrue(Twitch.can_handle_url(url))
diff --git a/tests/resources/dash/test_9.mpd b/tests/resources/dash/test_9.mpd
new file mode 100644
index 00000000..7e7f8559
--- /dev/null
+++ b/tests/resources/dash/test_9.mpd
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+     xmlns="urn:mpeg:dash:schema:mpd:2011"
+     xmlns:xlink="http://www.w3.org/1999/xlink"
+     xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
+     profiles="urn:mpeg:dash:profile:isoff-live:2011"
+     type="dynamic"
+     minimumUpdatePeriod="PT3.502S"
+     publishTime="2018-08-06T10:14:52Z"
+     availabilityStartTime="2018-07-19T09:10:45Z"
+     timeShiftBufferDepth="PT50.0S"
+     suggestedPresentationDelay="PT20.0S"
+     minBufferTime="PT6.0S">
+<ProgramInformation>
+    <Title>ltv9</Title>
+</ProgramInformation>
+<Location>https://595ac1e01d44e.streamlock.net/ltv9/ltv9/manifest_TOKEN.mpd</Location>
+<Period id="0" start="PT0.0S">
+    <AdaptationSet id="0" group="1" mimeType="video/mp4" width="848" height="480" par="16:9" frameRate="25" segmentAlignment="true" startWithSAP="1" subsegmentAlignment="true" subsegmentStartsWithSAP="1">
+        <SegmentTemplate timescale="90000" media="chunk_ctvideo_cfm4s_rid$RepresentationID$_cs$Time$_TOKEN_mpd.m4s" initialization="chunk_ctvideo_cfm4s_rid$RepresentationID$_cinit_TOKEN_mpd.m4s">
+            <SegmentTimeline>
+                <S t="140309188560" d="900000"/>
+                <S d="900000"/>
+                <S d="900000"/>
+                <S d="900000"/>
+                <S d="900000"/>
+            </SegmentTimeline>
+        </SegmentTemplate>
+        <Representation id="p0va0br2467130" codecs="avc1.42c01e" sar="1:1" bandwidth="2467130" />
+    </AdaptationSet>
+    <AdaptationSet id="1" group="2" mimeType="audio/mp4" lang="eng" segmentAlignment="true" startWithSAP="1" subsegmentAlignment="true" subsegmentStartsWithSAP="1">
+        <AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
+        <SegmentTemplate timescale="48000" media="chunk_ctaudio_cfm4s_rid$RepresentationID$_cs$Time$_TOKEN_mpd.m4s" initialization="chunk_ctaudio_cfm4s_rid$RepresentationID$_cinit_TOKEN_mpd.m4s">
+            <SegmentTimeline>
+                <S t="74831568240" d="479232"/>
+                <S d="480240"/>
+                <S d="480288"/>
+                <S d="480240"/>
+                <S d="479232"/>
+            </SegmentTimeline>
+        </SegmentTemplate>
+        <Representation id="p0aa0br61417" codecs="mp4a.40.2" audioSamplingRate="48000" bandwidth="61417">
+        </Representation>
+    </AdaptationSet>
+</Period>
+<UTCTiming schemeIdUri="urn:mpeg:dash:utc:direct:2014" value="2018-08-06T10:14:52Z"/>
+</MPD>
diff --git a/tests/streams/test_dash.py b/tests/streams/test_dash.py
index cf406185..9d9bd0df 100644
--- a/tests/streams/test_dash.py
+++ b/tests/streams/test_dash.py
@@ -3,7 +3,9 @@ import unittest
 from streamlink import PluginError
 from streamlink.stream import *
 from streamlink.stream.dash import DASHStreamWorker
+from streamlink.stream.dash_manifest import MPD
 from tests.mock import MagicMock, patch, ANY, Mock, call
+from tests.resources import xml
 
 
 class TestDASHStream(unittest.TestCase):
@@ -233,6 +235,16 @@ class TestDASHStream(unittest.TestCase):
         self.assertSequenceEqual(muxer.mock_calls, [call(self.session, open_reader, open_reader, copyts=True),
                                                     call().open()])
 
+    @patch('streamlink.stream.dash.MPD')
+    def test_segments_number_time(self, mpdClass):
+        with xml("dash/test_9.mpd") as mpd_xml:
+            mpdClass.return_value = MPD(mpd_xml, base_url="http://test.bar", url="http://test.bar/foo.mpd")
+
+            streams = DASHStream.parse_manifest(self.session, self.test_url)
+            mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd")
+
+            self.assertSequenceEqual(list(streams.keys()), ['2500k'])
+
 
 class TestDASHStreamWorker(unittest.TestCase):
     @patch("streamlink.stream.dash_manifest.time.sleep")
@@ -322,5 +334,7 @@ class TestDASHStreamWorker(unittest.TestCase):
         self.assertEqual(representation_aud, DASHStreamWorker.get_representation(mpd, 1, "audio/aac"))
 
 
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/streams/test_dash_parser.py b/tests/streams/test_dash_parser.py
index 2aa3801a..9270552a 100644
--- a/tests/streams/test_dash_parser.py
+++ b/tests/streams/test_dash_parser.py
@@ -7,8 +7,9 @@ from operator import attrgetter
 
 from freezegun import freeze_time
 from freezegun.api import FakeDatetime
+from tests.mock import MagicMock, Mock
 
-from streamlink.stream.dash_manifest import MPD, MPDParsers, MPDParsingError, utc
+from streamlink.stream.dash_manifest import MPD, MPDParsers, MPDParsingError, utc, Representation
 from tests.resources import xml
 
 
@@ -220,3 +221,20 @@ class TestMPDParser(unittest.TestCase):
                                       'http://test.se/video-time=8000-2799000-0.m4s?z32=CENSORED_SESSION',
                                       ])
 
+    def test_bitrate_rounded(self):
+        def mock_rep(bandwidth):
+            node = Mock(
+                tag="Representation",
+                attrib={
+                    "id": "test",
+                    "bandwidth": bandwidth,
+                    "mimeType": "video/mp4"
+                }
+            )
+            node.findall.return_value = []
+            return Representation(node)
+
+        self.assertEqual(mock_rep(1.2*1000.0).bandwidth_rounded, 1.2)
+        self.assertEqual(mock_rep(45.6*1000.0).bandwidth_rounded, 46.0)
+        self.assertEqual(mock_rep(134.0*1000.0).bandwidth_rounded, 130.0)
+        self.assertEqual(mock_rep(1324.0*1000.0).bandwidth_rounded, 1300.0)
diff --git a/tests/test_plugin_utils.py b/tests/test_plugin_utils.py
index a64b8c77..d92721e9 100644
--- a/tests/test_plugin_utils.py
+++ b/tests/test_plugin_utils.py
@@ -1,15 +1,11 @@
 from __future__ import unicode_literals
 import sys
+import unittest
 
 import pytest
 
 from streamlink.plugin.api.utils import itertags
 
-if sys.version_info[0:2] == (2, 6):
-    import unittest2 as unittest
-else:
-    import unittest
-
 
 class TestPluginUtil(unittest.TestCase):
     test_html = """
@@ -48,7 +44,6 @@ href="http://test.se/foo">bar</a>
         self.assertEqual(script[1].text.strip(), """Tester.ready(function () {\nalert("Hello, world!"); });""")
         self.assertEqual(script[1].attributes, {})
 
-
     @pytest.mark.xfail(sys.version_info >= (3, 7),
                        reason="python3.7 issue, see bpo-34294")
     def test_itertags_multi_attrs(self):
@@ -88,5 +83,3 @@ href="http://test.se/foo">bar</a>
         self.assertEqual(links[0].tag, "p")
         self.assertEqual(links[0].text.strip(), '<a \nhref="http://test.se/foo">bar</a>')
         self.assertEqual(links[0].attributes, {})
-
-
diff --git a/tests/test_session.py b/tests/test_session.py
index 7d9686e5..573aae37 100644
--- a/tests/test_session.py
+++ b/tests/test_session.py
@@ -5,7 +5,7 @@ from streamlink import Streamlink, NoPluginError
 from streamlink.plugin.plugin import HIGH_PRIORITY, LOW_PRIORITY
 from streamlink.plugins import Plugin
 from streamlink.session import print_small_exception
-from streamlink.stream import *
+from streamlink.stream import AkamaiHDStream, HLSStream, HTTPStream, RTMPStream
 from tests.mock import patch, call
 
 
@@ -76,7 +76,7 @@ class TestSession(unittest.TestCase):
 
     def test_plugin(self):
         channel = self.session.resolve_url("http://test.se/channel")
-        streams = channel.get_streams()
+        streams = channel.streams()
 
         self.assertTrue("best" in streams)
         self.assertTrue("worst" in streams)
@@ -89,33 +89,33 @@ class TestSession(unittest.TestCase):
 
     def test_plugin_stream_types(self):
         channel = self.session.resolve_url("http://test.se/channel")
-        streams = channel.get_streams(stream_types=["http", "rtmp"])
+        streams = channel.streams(stream_types=["http", "rtmp"])
 
         self.assertTrue(isinstance(streams["480p"], HTTPStream))
         self.assertTrue(isinstance(streams["480p_rtmp"], RTMPStream))
 
-        streams = channel.get_streams(stream_types=["rtmp", "http"])
+        streams = channel.streams(stream_types=["rtmp", "http"])
 
         self.assertTrue(isinstance(streams["480p"], RTMPStream))
         self.assertTrue(isinstance(streams["480p_http"], HTTPStream))
 
     def test_plugin_stream_sorted_excludes(self):
         channel = self.session.resolve_url("http://test.se/channel")
-        streams = channel.get_streams(sorting_excludes=["1080p", "3000k"])
+        streams = channel.streams(sorting_excludes=["1080p", "3000k"])
 
         self.assertTrue("best" in streams)
         self.assertTrue("worst" in streams)
         self.assertTrue(streams["best"] is streams["1500k"])
 
-        streams = channel.get_streams(sorting_excludes=[">=1080p", ">1500k"])
+        streams = channel.streams(sorting_excludes=[">=1080p", ">1500k"])
         self.assertTrue(streams["best"] is streams["1500k"])
 
-        streams = channel.get_streams(sorting_excludes=lambda q: not q.endswith("p"))
+        streams = channel.streams(sorting_excludes=lambda q: not q.endswith("p"))
         self.assertTrue(streams["best"] is streams["3000k"])
 
     def test_plugin_support(self):
         channel = self.session.resolve_url("http://test.se/channel")
-        streams = channel.get_streams()
+        streams = channel.streams()
 
         self.assertTrue("support" in streams)
         self.assertTrue(isinstance(streams["support"], HTTPStream))
diff --git a/tests/test_utils_url.py b/tests/test_utils_url.py
index b77dcba4..7144254e 100644
--- a/tests/test_utils_url.py
+++ b/tests/test_utils_url.py
@@ -1,10 +1,8 @@
-import sys
-from streamlink.utils.url import *
+import unittest
 
-if sys.version_info[0:2] == (2, 6):
-    import unittest2 as unittest
-else:
-    import unittest
+from streamlink.utils.url import (
+    update_scheme, url_equal, url_concat, update_qsd
+)
 
 
 class TestUtilURL(unittest.TestCase):