Repository: streamlink/streamlink
Pull request #887 (2017-05-05 20:19:32)

plugins.streann: support for ott.streann.com + ellobo.com
Also supports ellobo.com, fixing #869. Not so straightforward :)


Patch:
diff --git a/.travis.yml b/.travis.yml
index 874d4514..c6871830 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -39,9 +39,6 @@ addons:
     packages:
     - nsis
 
-before_deploy:
-  - ./script/bintrayconfig.sh
-
 deploy:
   - provider: releases
     api_key: "${RELEASES_API_KEY}"
@@ -57,19 +54,15 @@ deploy:
     on:
       tags: true
       condition: $BUILD_INSTALLER = yes
-  - provider: bintray
-    file: build/bintray-nightly.json
-    user: "${BINTRAY_USER}"
-    key: "${BINTRAY_KEY}"
+  - provider: s3
     skip_cleanup: true
+    local_dir: $STREAMLINK_INSTALLER_DIST_DIR
+    bucket: streamlink-builds
+    upload-dir: nightly/windows
     on:
       branch: master
-      condition: $BUILD_INSTALLER = yes && $TRAVIS_EVENT_TYPE == cron
-  - provider: bintray
-    file: build/bintray-latest.json
-    user: "${BINTRAY_USER}"
-    key: "${BINTRAY_KEY}"
-    skip_cleanup: true
-    on:
-      branch: master
-      condition: $BUILD_INSTALLER = yes && $TRAVIS_EVENT_TYPE == cron
+      condition: $BUILD_INSTALLER = yes
+    secret_access_key:
+      secure: 3abBcpXNxhWDJyznMA/wDmQ+lUSaJVwi62aXHjFxA7Zrz/CidbHmXkNedViWTOivMyVcM4Ypej4JD1V74Uo78GN09TICRbSc9fTF08O8Hu+hKiAcVWepfmuV54nyoQHY5mcxBPwgUZwnTwSYLzKXCqDEmQXrUrM313m4f2cbDSQgo6VPoCqE+U7JTVaUI6asutsErvK7vfw9EqGSumndRNjQdD0triubs5kh6WPv5STpKrkUNWX95fjBbkuf5hy+DIQN22hLpHYwqV3DoTnhWzbPy8/OnCKoUjEAspLehkjr86brwuls1UfR1uC2uG53O2Rb3CCYkZSVC7GKdEw0SnbLNVfbhy500a8AiPbWC0AV75PidXNEe+1zye4n/xKdx0KA8aRXr7v/89x7KibHNpwf6rdpx7axYhlsWcFcyfoOTZJ3thhu7ib9QYJZ7gkRVGWbawZU6I370I5sMJLIwmzBXgZ2y8jFk7LUNbb2GUS3LKcw5gRYOqo+ZEHFdiLpPYXFHUslTkpaMNoPCNUR7Tsa2JPfJ29yEDMsdDs2lJbP+Km8hAnpkyJfpfPcVUcl8Ootd1cTe8iPdD1nqt3KvIy4sCjkCxvQFnmGOTWFqXY/UDD+Yqo3IVOG1aLPR3UxhzHtPX6M5FxtNB22kv8hrbN/QxhBEo/SfNhGIGZJNdI=
+    access_key_id:
+      secure: sjjc1tM756TvFbD8+0VCs/MOLFRhY+VZv/lq7pvNldtbGdv21lcJguvgg0gUwUZ7SihceyL1YagfUlDU2vWS1N5Si04z0VUcHO4lbj9nUJ4CXy27pMNFuk92HmblxYQ4bJw/xVqzsrT9Zeh4tIrbV+F3lYXtg7FDqjpm5bqrUJ5ULyWbhax31s2k8JRVnG2fxVmJiU+j4zwgzCqeZJXQkvXsfdzSe88mLyIrl7Otfjlp1rySocxStQ8qlmkvRAmsYmsKriUq+VgtX21HFAIJqHQp4WbL15eh0aFFzbmORf7OMIKmsmkyeYYeM6xp+Jm9m7ZcWHZ+7siplVM/9ksmumN3+CGp6pogduZ5zFH4BtRQa9o8N2SWnPiPvMrpoyLbHbQhg/uhcMJkYI1yrzj/s9MEH7DVnDEMmgc/UKsyTC2PRwZcgqYYPHxqRVWXzvBWDUe1wtxdNyaXWWKdD/1E7u8XF2+zPTPaUNAYAF1rSi5GNF38uRNMC+QSuEm8wZ/32KXFN4m+RW3NtpT9YHI8MCo4ofVxpRUMcEUwPDF/jV5zW7Krz7LEeL5/zcHMcNay/Ls8e3eApQShAcLAx1jZo+EjznVGXtlRbH9ABXmPO5AcxUH5+20tMiMoj0q6hJen03X8Jx+OMmcDNDufDgPikSym3KVC3Lvj65lG4G9pAyY=
diff --git a/docs/install.rst b/docs/install.rst
index b864ae00..f7385939 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -239,7 +239,7 @@ Windows binaries
 
 You can download the latest stable Windows installer from the `GitHub Releases Page <https://github.com/streamlink/streamlink/releases/latest>`__.
 
-Alternatively, you can download the latest `nightly Windows installer <https://dl.bintray.com/streamlink/streamlink-nightly/streamlink-latest.exe>`__.
+Alternatively, you can download the latest `nightly Windows installer <https://streamlink-builds.s3.amazonaws.com/nightly/windows/streamlink-latest.exe>`__.
 
 This is a installer which contains:
 
diff --git a/docs/plugin_matrix.rst b/docs/plugin_matrix.rst
index a9d11867..4fdc7e46 100644
--- a/docs/plugin_matrix.rst
+++ b/docs/plugin_matrix.rst
@@ -93,6 +93,7 @@ dplay               - dplay.se           --    Yes   Streams may be geo-restrict
                     - dplay.dk
 drdk                dr.dk                Yes   Yes   Streams may be geo-restricted to Denmark.
 earthcam            earthcam.com         Yes   Yes   Only works for the cams hosted on EarthCam.
+ellobo              ellobo106.com        Yes   -
 eltrecetv           eltrecetv.com.ar     Yes   Yes   Streams may be geo-restricted to Argentina.
 eurocom             eurocom.bg           Yes   No
 euronews            euronews.com         Yes   No
diff --git a/script/bintrayconfig.sh b/script/bintrayconfig.sh
deleted file mode 100755
index f5c2f7ef..00000000
--- a/script/bintrayconfig.sh
+++ /dev/null
@@ -1,84 +0,0 @@
-#!/usr/bin/env bash
-#
-# Script to generate bintray config for nightly builds
-#
-
-build_dir="$(pwd)/build"
-nsis_dir="${build_dir}/nsis"
-# get the dist directory from an environment variable, but default to the build/nsis directory
-dist_dir="${STREAMLINK_INSTALLER_DIST_DIR:-$nsis_dir}"
-
-# TODO: extract this common version detection code in to a separate function
-STREAMLINK_VERSION_PLAIN=$(python setup.py --version)
-# For travis nightly builds generate a version number with commit hash
-if [ -n "${TRAVIS_BRANCH}" ] && [ -z "${TRAVIS_TAG}" ]; then
-    STREAMLINK_VI_VERSION="${STREAMLINK_VERSION_PLAIN}.${TRAVIS_BUILD_NUMBER}"
-    STREAMLINK_INSTALLER="streamlink-${STREAMLINK_VERSION_PLAIN}-${TRAVIS_BUILD_NUMBER}-${TRAVIS_COMMIT:0:7}"
-    STREAMLINK_VERSION="${STREAMLINK_VERSION_PLAIN}+${TRAVIS_COMMIT:0:7}"
-else
-    STREAMLINK_VI_VERSION="${STREAMLINK_VERSION_PLAIN}.${TRAVIS_BUILD_NUMBER:-0}"
-    STREAMLINK_VERSION="${STREAMLINK_VERSION_PLAIN}"
-    STREAMLINK_INSTALLER="streamlink-${STREAMLINK_VERSION}"
-fi
-
-cat > "${build_dir}/bintray-latest.json" <<EOF
-{
-  "package": {
-    "subject": "streamlink",
-    "repo": "streamlink-nightly",
-    "name": "streamlink"
-  },
-
-  "version": {
-    "name": "latest",
-    "released": "$(date +'%Y-%m-%d')",
-    "desc": "Latest version of the installer (${STREAMLINK_VERSION})"
-  },
-
-  "files": [
-    {
-      "includePattern": "${dist_dir}/${STREAMLINK_INSTALLER}.exe",
-      "uploadPattern": "streamlink-latest.exe",
-      "matrixParams": {
-        "override": 1,
-        "publish": 1
-      }
-    }
-  ],
-
-  "publish": true
-}
-EOF
-
-echo "Wrote Bintray config to: ${build_dir}/bintray-latest.json"
-
-cat > "${build_dir}/bintray-nightly.json" <<EOF
-{
-  "package": {
-    "subject": "streamlink",
-    "repo": "streamlink-nightly",
-    "name": "streamlink"
-  },
-
-  "version": {
-    "name": "$(date +'%Y.%m.%d')",
-    "released": "$(date +'%Y-%m-%d')",
-    "desc": "Streamlink Nightly based on ${STREAMLINK_VERSION}"
-  },
-
-  "files": [
-    {
-      "includePattern": "${dist_dir}/${STREAMLINK_INSTALLER}.exe",
-      "uploadPattern": "streamlink-${STREAMLINK_VERSION_PLAIN}-$(date +'%Y%m%d').exe",
-      "matrixParams": {
-        "override": 1,
-        "publish": 1
-      }
-    }
-  ],
-
-  "publish": true
-}
-EOF
-
-echo "Wrote Bintray config to: ${build_dir}/bintray-nightly.json"
diff --git a/script/makeinstaller.sh b/script/makeinstaller.sh
index 259cce34..935df9ee 100755
--- a/script/makeinstaller.sh
+++ b/script/makeinstaller.sh
@@ -208,5 +208,10 @@ cp -r "win32/rtmpdump" "${nsis_dir}/"
 
 pynsist build/streamlink.cfg
 
+# Make a copy of this build for the "latest" nightly
+if [ -n "${TRAVIS_BRANCH}" ] && [ -z "${TRAVIS_TAG}" ]; then
+    cp "${dist_dir}/${STREAMLINK_INSTALLER}.exe" "${dist_dir}/streamlink-latest.exe"
+fi
+
 echo "Success!" 1>&2
 echo "The installer should be in ${dist_dir}." 1>&2
diff --git a/src/streamlink/plugins/ellobo.py b/src/streamlink/plugins/ellobo.py
new file mode 100644
index 00000000..1fcc7a99
--- /dev/null
+++ b/src/streamlink/plugins/ellobo.py
@@ -0,0 +1,30 @@
+import re
+
+from streamlink import NoPluginError
+from streamlink import PluginError
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http
+
+
+class ElLobo(Plugin):
+    url_re = re.compile(r"https?://(?:www\.)?ellobo106\.com/")
+    iframe_re = re.compile(r'<iframe.*?src="([^"]+)".*?>.*?</iframe>', re.DOTALL)
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls.url_re.match(url) is not None
+
+    def _get_streams(self):
+        res = http.get(self.url)
+        # Search for the iframe in the page
+        iframe_m = self.iframe_re.search(res.text)
+
+        ustream_url = iframe_m and iframe_m.group(1)
+        if ustream_url and "ott.streann.com" in ustream_url:
+            try:
+                return self.session.streams(ustream_url)
+            except NoPluginError:
+                raise PluginError("Could not play embedded stream: {0}".format(ustream_url))
+
+
+__plugin__ = ElLobo
diff --git a/src/streamlink/plugins/streann.py b/src/streamlink/plugins/streann.py
new file mode 100644
index 00000000..a57b0ce6
--- /dev/null
+++ b/src/streamlink/plugins/streann.py
@@ -0,0 +1,80 @@
+from __future__ import print_function
+
+import time
+
+import base64
+import random
+import re
+
+from streamlink.compat import urlparse
+from streamlink.plugin import Plugin
+from streamlink.plugin.api import http
+from streamlink.stream import HLSStream
+from streamlink.utils import parse_qsd
+from streamlink.utils.crypto import decrypt_openssl
+
+
+class Streann(Plugin):
+    url_re = re.compile(r"https?://ott\.streann.com/streaming/player\.html")
+    base_url = "http://ott.streann.com"
+    get_time_url = base_url + "/web/services/public/get-server-time"
+    token_url = base_url + "/loadbalancer/services/web-players/{playerId}/token/{type}/{dataId}/{deviceId}"
+    stream_url = base_url + "/loadbalancer/services/web-players/{type}s-reseller/{dataId}/{playerId}/{token}/{resellerId}/playlist.m3u8?date={time}&device-type=web&device-name=web&device-os=web&device-id={deviceId}"
+    passphrase_re = re.compile(r'''CryptoJS\.AES\.decrypt\(.*?,\s*(['"])(?P<passphrase>(?:(?!\1).)*)\1\s*?\);''')
+
+    def __init__(self, url):
+        super(Streann, self).__init__(url)
+        self._device_id = None
+
+    @classmethod
+    def can_handle_url(cls, url):
+        return cls.url_re.match(url) is not None
+
+    @property
+    def device_id(self):
+        """
+        Randomly generated deviceId.
+        :return:
+        """
+        if self._device_id is None:
+            self._device_id = "".join(
+                random.choice("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") for _ in range(50))
+        return self._device_id
+
+    @property
+    def time(self):
+        res = http.get(self.get_time_url)
+        data = http.json(res)
+        return str(data.get("serverTime", int(time.time() * 1000)))
+
+    def passphrase(self):
+        res = http.get(self.url)
+        passphrase_m = self.passphrase_re.search(res.text)
+        return passphrase_m and passphrase_m.group("passphrase").encode("utf8")
+
+    def get_token(self, **config):
+        pdata = dict(arg1=base64.b64encode("www.ellobo106.com".encode("utf8")),
+                     arg2=base64.b64encode(self.time.encode("utf8")))
+
+        res = http.post(self.token_url.format(deviceId=self.device_id, **config),
+                        data=pdata)
+        data = http.json(res)
+        return data["token"]
+
+    def _get_streams(self):
+        # Get the query string
+        encrypted_data = urlparse(self.url).query
+        data = base64.b64decode(encrypted_data)
+        # and decrypt it
+        passphrase = self.passphrase()
+        if passphrase:
+            params = decrypt_openssl(data, passphrase)
+            config = parse_qsd(params.decode("utf8"))
+
+            return HLSStream.parse_variant_playlist(self.session,
+                                                    self.stream_url.format(time=self.time,
+                                                                           deviceId=self.device_id,
+                                                                           token=self.get_token(**config),
+                                                                           **config))
+
+__plugin__ = Streann
diff --git a/src/streamlink/utils/crypto.py b/src/streamlink/utils/crypto.py
new file mode 100644
index 00000000..6683b218
--- /dev/null
+++ b/src/streamlink/utils/crypto.py
@@ -0,0 +1,34 @@
+import hashlib
+import math
+
+from Crypto.Cipher import AES
+
+from streamlink.compat import is_py3
+
+
+def evp_bytestokey(password, salt, key_len, iv_len):
+    """
+    Python implementation of OpenSSL's EVP_BytesToKey()
+    :param password: or passphrase
+    :param salt: 8 byte salt
+    :param key_len: length of key in bytes
+    :param iv_len:  length of IV in bytes
+    :return: (key, iv)
+    """
+    d = d_i = b''
+    while len(d) < key_len + iv_len:
+        d_i = hashlib.md5(d_i + password + salt).digest()
+        d += d_i
+    return d[:key_len], d[key_len:key_len+iv_len]
+
+
+def decrypt_openssl(data, passphrase, key_length=32):
+    if data.startswith(b"Salted__"):
+        salt = data[len(b"Salted__"):AES.block_size]
+        key, iv = evp_bytestokey(passphrase, salt, key_length, AES.block_size)
+        d = AES.new(key, AES.MODE_CBC, iv)
+        out = d.decrypt(data[AES.block_size:])
+        if is_py3:
+            return out[:-out[-1]]
+        else:
+            return out[:-ord(out[-1])]


Test patch:
diff --git a/tests/test_utils_crypto.py b/tests/test_utils_crypto.py
new file mode 100644
index 00000000..cd7a4dd8
--- /dev/null
+++ b/tests/test_utils_crypto.py
@@ -0,0 +1,26 @@
+import sys
+
+import base64
+
+from streamlink.utils.crypto import evp_bytestokey, decrypt_openssl
+
+if sys.version_info[0:2] == (2, 6):
+    import unittest2 as unittest
+else:
+    import unittest
+
+
+class TestUtil(unittest.TestCase):
+    def test_evp_bytestokey(self):
+        self.assertEqual((b']A@*\xbcK*v\xb9q\x9d\x91\x10\x17\xc5\x92',
+                          b'(\xb4n\xd3\xc1\x11\xe8Q\x02\x90\x9b\x1c\xfbP\xea\x0f'),
+                         evp_bytestokey(b"hello", b"", 16, 16))
+
+    def test_decrpyt(self):
+        # data generated with:
+        #   echo "this is a test" | openssl enc -aes-256-cbc -pass pass:"streamlink" -base64
+        data = base64.b64decode("U2FsdGVkX18nVyJ6Y+ksOASMSHKuRoQ9b4DKHuPbyQc=")
+        self.assertEqual(
+            b"this is a test\n",
+            decrypt_openssl(data, b"streamlink")
+        )