diff --git a/README.md b/README.md index 1e6fc1a8..103089cd 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ you can send a message to [our Facebook page](https://www.facebook.com/kodivrtnu ## Releases #### v1.7.0 (2019-03-23) +- Add full proxy support (@dagwieers) - Indicate when content will disappear in the next 3 months (@dagwieers) - Indicate when content is geo-blocked (@dagwieers) - Add fanart to menus (@dagwieers) diff --git a/plugin.video.vrt.nu/addon.xml b/plugin.video.vrt.nu/addon.xml index dc5e0c83..8ad30d28 100644 --- a/plugin.video.vrt.nu/addon.xml +++ b/plugin.video.vrt.nu/addon.xml @@ -26,6 +26,7 @@ GNU General Public License, v3 v1.7.0 (2019-03-23) +- Add full proxy support (@dagwieers) - Indicate when content will disappear in the next 3 months (@dagwieers) - Indicate when content is geo-blocked (@dagwieers) - Add fanart to menus (@dagwieers) diff --git a/plugin.video.vrt.nu/resources/language/resource.language.en_gb/strings.po b/plugin.video.vrt.nu/resources/language/resource.language.en_gb/strings.po index cdac3063..a2c6031f 100644 --- a/plugin.video.vrt.nu/resources/language/resource.language.en_gb/strings.po +++ b/plugin.video.vrt.nu/resources/language/resource.language.en_gb/strings.po @@ -63,6 +63,14 @@ msgctxt "#32054" msgid "Woops something went wrong, check the log for more details" msgstr "Woops something went wrong, check the log for more details" +msgctxt "#32061" +msgid "SOCKS proxies are currently unsupported" +msgstr "SOCKS proxies are currently unsupported" + +msgctxt "#32062" +msgid "Using a SOCKS proxy requires the requests[socks] library installed." +msgstr "Using a SOCKS proxy requires the requests[socks] library installed." + msgctxt "#32080" msgid "A-Z" msgstr "A-Z" diff --git a/plugin.video.vrt.nu/resources/language/resource.language.nl_nl/strings.po b/plugin.video.vrt.nu/resources/language/resource.language.nl_nl/strings.po index 5d6b7100..8dd39adc 100644 --- a/plugin.video.vrt.nu/resources/language/resource.language.nl_nl/strings.po +++ b/plugin.video.vrt.nu/resources/language/resource.language.nl_nl/strings.po @@ -64,6 +64,14 @@ msgctxt "#32054" msgid "Woops something went wrong, check the log for more details" msgstr "Oeps, er ging iets mis, check de log voor meer informatie" +msgctxt "#32061" +msgid "SOCKS proxies are currently unsupported" +msgstr "SOCKS proxies zijn momenteel niet ondersteund" + +msgctxt "#32062" +msgid "Using a SOCKS proxy requires the requests[socks] library installed." +msgstr "Het gebruik van SOCKS proxies vereist dat de requests[socks] library geïnstalleerd is." + msgctxt "#32080" msgid "A-Z" msgstr "A-Z" diff --git a/plugin.video.vrt.nu/resources/lib/kodiwrappers/kodiwrapper.py b/plugin.video.vrt.nu/resources/lib/kodiwrappers/kodiwrapper.py index 0c17bf13..633a3dfc 100644 --- a/plugin.video.vrt.nu/resources/lib/kodiwrappers/kodiwrapper.py +++ b/plugin.video.vrt.nu/resources/lib/kodiwrappers/kodiwrapper.py @@ -24,6 +24,7 @@ def __init__(self, handle, url, addon): self._handle = handle self._url = url self._addon = addon + self._addon_id = addon.getAddonInfo('id') def show_listing(self, list_items, sort=None, content_type='episodes'): listing = [] @@ -91,6 +92,53 @@ def get_setting(self, setting_id): def open_settings(self): self._addon.openSettings() + def get_global_setting(self, setting): + json_result = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "method": "Settings.GetSettingValue", "params": {"setting": "%s"}, "id": 1}' % setting) + return json.loads(json_result)['result']['value'] + + def get_proxies(self): + usehttpproxy = self.get_global_setting('network.usehttpproxy') + if usehttpproxy is False: + return dict() + + httpproxytype = self.get_global_setting('network.httpproxytype') + + if httpproxytype != 0: + title = self.get_localized_string(32061) + message = self.get_localized_string(32062) + self.show_ok_dialog(title, message) + + if httpproxytype == 0: + httpproxyscheme = 'http' + elif httpproxytype == 1: + httpproxyscheme = 'socks4' + elif httpproxytype == 2: + httpproxyscheme = 'socks4a' + elif httpproxytype == 3: + httpproxyscheme = 'socks5' + elif httpproxytype == 4: + httpproxyscheme = 'socks5h' + else: + httpproxyscheme = 'http' + + httpproxyserver = self.get_global_setting('network.httpproxyserver') + httpproxyport = self.get_global_setting('network.httpproxyport') + httpproxyusername = self.get_global_setting('network.httpproxyusername') + httpproxypassword = self.get_global_setting('network.httpproxypassword') + + if httpproxyserver and httpproxyport and httpproxyusername and httpproxypassword: + proxy_address = '%s://%s:%s@%s:%s' % (httpproxyscheme, httpproxyusername, httpproxypassword, httpproxyserver, httpproxyport) + elif httpproxyserver and httpproxyport and httpproxyusername: + proxy_address = '%s://%s@%s:%s' % (httpproxyscheme, httpproxyusername, httpproxyserver, httpproxyport) + elif httpproxyserver and httpproxyport: + proxy_address = '%s://%s:%s' % (httpproxyscheme, httpproxyserver, httpproxyport) + elif httpproxyserver: + proxy_address = '%s://%s' % (httpproxyscheme, httpproxyserver) + else: + return dict() + + return dict(http=proxy_address, https=proxy_address) + # NOTE: normally inputstream adaptive will always be installed, this only applies for people uninstalling inputstream adaptive while this addon is disabled def has_inputstream_adaptive_installed(self): return xbmc.getCondVisibility('System.HasAddon("{0}")'.format('inputstream.adaptive')) == 1 @@ -121,7 +169,9 @@ def delete_path(self, path): return xbmcvfs.delete(path) def log_notice(self, message): - xbmc.log(message, xbmc.LOGNOTICE) + ''' Log info messages to Kodi ''' + xbmc.log(msg='[%s] %s' % (self._addon_id, message), level=xbmc.LOGNOTICE) def log_error(self, message): - xbmc.log(message, xbmc.LOGERROR) + ''' Log error messages to Kodi ''' + xbmc.log(msg='[%s] %s' % (self._addon_id, message), level=xbmc.LOGERROR) diff --git a/plugin.video.vrt.nu/resources/lib/vrtplayer/streamservice.py b/plugin.video.vrt.nu/resources/lib/vrtplayer/streamservice.py index a6dc3418..effc5a55 100644 --- a/plugin.video.vrt.nu/resources/lib/vrtplayer/streamservice.py +++ b/plugin.video.vrt.nu/resources/lib/vrtplayer/streamservice.py @@ -18,6 +18,7 @@ class StreamService: def __init__(self, vrt_base, vrtnu_base_url, kodi_wrapper, token_resolver): self._kodi_wrapper = kodi_wrapper + self._proxies = self._kodi_wrapper.get_proxies() self.token_resolver = token_resolver self._vrt_base = vrt_base self._vrtnu_base_url = vrtnu_base_url @@ -26,7 +27,7 @@ def __init__(self, vrt_base, vrtnu_base_url, kodi_wrapper, token_resolver): self._license_url = self._get_license_url() def _get_license_url(self): - return requests.get(self._VUPLAY_API_URL).json()['drm_providers']['widevine']['la_url'] + return requests.get(self._VUPLAY_API_URL, proxies=self._proxies).json()['drm_providers']['widevine']['la_url'] def _create_settings_dir(self): settingsdir = self._kodi_wrapper.get_userdata_path() @@ -74,7 +75,7 @@ def _get_license_key(self, key_url, key_type='R', key_headers=None, key_value=No return ''.join((key_url, '|', header.strip('&'), '|', key_value, '|')) def _get_api_data(self, video_url): - html_page = requests.get(video_url).text + html_page = requests.get(video_url, proxies=self._proxies).text strainer = SoupStrainer('div', {'class': 'cq-dd-vrtvideo'}) soup = BeautifulSoup(html_page, 'html.parser', parse_only=strainer) video_data = soup.find(lambda tag: tag.name == 'div' and tag.get('class') == ['vrtvideo']).attrs @@ -105,7 +106,7 @@ def _get_video_json(self, api_data): # Construct api_url and get video json api_url = api_data.media_api_url + '/videos/' + api_data.publication_id + \ api_data.video_id + '?vrtPlayerToken=' + playertoken + '&client=' + api_data.client - video_json = requests.get(api_url).json() + video_json = requests.get(api_url, proxies=self._proxies).json() return video_json @@ -187,7 +188,7 @@ def _select_stream(self, stream_dict, vudrm_token): # Speed up HLS selection, workaround for slower kodi selection def _select_hls_substreams(self, master_hls_url): base_url = master_hls_url.split('.m3u8')[0] - m3u8 = requests.get(master_hls_url).text + m3u8 = requests.get(master_hls_url, proxies=self._proxies).text direct_audio_url = None direct_video_url = None direct_subtitle_url = None diff --git a/plugin.video.vrt.nu/resources/lib/vrtplayer/tokenresolver.py b/plugin.video.vrt.nu/resources/lib/vrtplayer/tokenresolver.py index 3dc4f5fd..4756e5bc 100644 --- a/plugin.video.vrt.nu/resources/lib/vrtplayer/tokenresolver.py +++ b/plugin.video.vrt.nu/resources/lib/vrtplayer/tokenresolver.py @@ -21,6 +21,7 @@ class TokenResolver: def __init__(self, kodi_wrapper): self._kodi_wrapper = kodi_wrapper + self._proxies = self._kodi_wrapper.get_proxies() def get_ondemand_playertoken(self, token_url, xvrttoken): token_path = self._kodi_wrapper.get_userdata_path() + self._ONDEMAND_COOKIE @@ -29,7 +30,7 @@ def get_ondemand_playertoken(self, token_url, xvrttoken): if token is None: cookie_value = 'X-VRT-Token=' + xvrttoken headers = {'Content-Type': 'application/json', 'Cookie': cookie_value} - token = TokenResolver._get_new_playertoken(token_path, token_url, headers) + token = self._get_new_playertoken(token_path, token_url, headers) return token def get_live_playertoken(self, token_url, xvrttoken): @@ -41,7 +42,7 @@ def get_live_playertoken(self, token_url, xvrttoken): headers = {'Content-Type': 'application/json', 'Cookie' : cookie_value} else: headers = {'Content-Type': 'application/json'} - token = TokenResolver._get_new_playertoken(token_path, token_url, headers) + token = self._get_new_playertoken(token_path, token_url, headers) return token def get_xvrttoken(self, get_roaming_token=False): @@ -59,9 +60,8 @@ def get_xvrttoken_from_cookiejar(cookiejar): if cookie.name == 'X-VRT-Token': yield cookie - @staticmethod - def _get_new_playertoken(path, token_url, headers): - playertoken = requests.post(token_url, headers=headers).json() + def _get_new_playertoken(self, path, token_url, headers): + playertoken = requests.post(token_url, proxies=self._proxies, headers=headers).json() json.dump(playertoken, open(path, 'w')) return playertoken['vrtPlayerToken'] @@ -86,19 +86,19 @@ def _get_new_xvrttoken(self, path, get_roaming_token): self._kodi_wrapper.open_settings() cred.reload() data = {'loginID': cred.username, 'password': cred.password, 'sessionExpiration': '-1', 'APIKey': self._API_KEY, 'targetEnv': 'jssdk'} - logon_json = requests.post(self._LOGIN_URL, data).json() + logon_json = requests.post(self._LOGIN_URL, data, proxies=self._proxies).json() token = None if logon_json['errorCode'] == 0: login_token = logon_json['sessionInfo']['login_token'] login_cookie = ''.join(('glt_', self._API_KEY, '=', login_token)) payload = {'uid': logon_json['UID'], 'uidsig': logon_json['UIDSignature'], 'ts': logon_json['signatureTimestamp'], 'email': cred.username} headers = {'Content-Type': 'application/json', 'Cookie': login_cookie} - cookie_jar = requests.post(self._TOKEN_GATEWAY_URL, headers=headers, json=payload).cookies + cookie_jar = requests.post(self._TOKEN_GATEWAY_URL, proxies=self._proxies, headers=headers, json=payload).cookies xvrttoken = TokenResolver._create_token_dictionary(cookie_jar) token = xvrttoken['X-VRT-Token'] if get_roaming_token: - xvrttoken = TokenResolver._get_roaming_xvrttoken(login_cookie, xvrttoken) + xvrttoken = self._get_roaming_xvrttoken(login_cookie, xvrttoken) token = xvrttoken['X-VRT-Token'] if xvrttoken is not None else None json.dump(xvrttoken, open(path, 'w')) else: @@ -107,19 +107,18 @@ def _get_new_xvrttoken(self, path, get_roaming_token): self._kodi_wrapper.show_ok_dialog(title, message) return token - @staticmethod - def _get_roaming_xvrttoken(login_cookie, xvrttoken): + def _get_roaming_xvrttoken(self, login_cookie, xvrttoken): url = 'https://token.vrt.be/vrtnuinitloginEU?destination=https://www.vrt.be/vrtnu/' cookie_value = 'X-VRT-Token=' + xvrttoken['X-VRT-Token'] headers = {'Cookie': cookie_value} - r = requests.get(url, headers=headers, allow_redirects=False) + r = requests.get(url, proxies=self._proxies, headers=headers, allow_redirects=False) url = r.headers.get('Location') - r = requests.get(url, headers=headers, allow_redirects=False) + r = requests.get(url, proxies=self._proxies, headers=headers, allow_redirects=False) url = r.headers.get('Location') headers = {'Cookie': login_cookie} roaming_xvrttoken = None if url is not None: - cookie_jar = requests.get(url, headers=headers).cookies + cookie_jar = requests.get(url, proxies=self._proxies, headers=headers).cookies roaming_xvrttoken = TokenResolver._create_token_dictionary(cookie_jar) return roaming_xvrttoken diff --git a/plugin.video.vrt.nu/resources/lib/vrtplayer/vrtapihelper.py b/plugin.video.vrt.nu/resources/lib/vrtplayer/vrtapihelper.py index 69b2eb53..d413de58 100644 --- a/plugin.video.vrt.nu/resources/lib/vrtplayer/vrtapihelper.py +++ b/plugin.video.vrt.nu/resources/lib/vrtplayer/vrtapihelper.py @@ -13,21 +13,22 @@ class VRTApiHelper: - def __init__(self, kodi_wrapper): - self._kodi_wrapper = kodi_wrapper - _VRT_BASE = 'https://www.vrt.be' _VRTNU_API_BASE = 'https://vrtnu-api.vrt.be' _VRTNU_SEARCH_URL = ''.join((_VRTNU_API_BASE, '/search')) _VRTNU_SUGGEST_URL = ''.join((_VRTNU_API_BASE, '/suggest')) _VRTNU_SCREENSHOT_URL = ''.join((_VRTNU_API_BASE, '/screenshots')) + def __init__(self, kodi_wrapper): + self._kodi_wrapper = kodi_wrapper + self._proxies = self._kodi_wrapper.get_proxies() + def get_tvshow_items(self, path): if path == 'az': api_url = ''.join((self._VRTNU_SUGGEST_URL, '?facets[transcodingStatus]=AVAILABLE')) else: api_url = ''.join((self._VRTNU_SUGGEST_URL, '?facets[categories]=', path)) - tvshows = requests.get(api_url).json() + tvshows = requests.get(api_url, proxies=self._proxies).json() tvshow_items = [] for tvshow in tvshows: metadata_creator = metadatacreator.MetadataCreator() @@ -61,11 +62,11 @@ def get_episode_items(self, path): sort_method = None if path == 'recent': api_url = ''.join((self._VRTNU_SEARCH_URL, '?i=video&size=50&facets[transcodingStatus]=AVAILABLE&facets[brands]=[een,canvas,sporza,radio1,klara,stubru,mnm]')) - api_json = requests.get(api_url).json() + api_json = requests.get(api_url, proxies=self._proxies).json() episode_items, sort_method = self._map_to_episode_items(api_json['results'], path) else: api_url = ''.join((self._VRTNU_SEARCH_URL, '?i=video&size=150&facets[programUrl]=//www.vrt.be', path.replace('.relevant', ''))) if '.relevant/' in path else path - api_json = requests.get(api_url).json() + api_json = requests.get(api_url, proxies=self._proxies).json() # Look for seasons items if not yet done if 'facets[seasonTitle]' not in path: episode_items = self._get_season_items(api_url, api_json['facets']['facets']) diff --git a/plugin.video.vrt.nu/resources/lib/vrtplayer/vrtplayer.py b/plugin.video.vrt.nu/resources/lib/vrtplayer/vrtplayer.py index 436dd84c..44bd8791 100644 --- a/plugin.video.vrt.nu/resources/lib/vrtplayer/vrtplayer.py +++ b/plugin.video.vrt.nu/resources/lib/vrtplayer/vrtplayer.py @@ -23,6 +23,7 @@ class VRTPlayer: def __init__(self, addon_path, kodi_wrapper, stream_service, api_helper): self._addon_path = addon_path self._kodi_wrapper = kodi_wrapper + self._proxies = self._kodi_wrapper.get_proxies() self._api_helper = api_helper self._stream_service = stream_service @@ -93,7 +94,7 @@ def __get_media(self, file_name): return os.path.join(self._addon_path, 'resources', 'media', file_name) def __get_category_menu_items(self, url, soupstrainer_parser_selector, routing_action, video_dict_action=None): - response = requests.get(url) + response = requests.get(url, proxies=self._proxies) tiles = SoupStrainer('a', soupstrainer_parser_selector) soup = BeautifulSoup(response.content, 'html.parser', parse_only=tiles) listing = [] diff --git a/plugin.video.vrt.nu/vrtnutests/vrtplayertests.py b/plugin.video.vrt.nu/vrtnutests/vrtplayertests.py index 39b1f2ca..05bbca23 100644 --- a/plugin.video.vrt.nu/vrtnutests/vrtplayertests.py +++ b/plugin.video.vrt.nu/vrtnutests/vrtplayertests.py @@ -2,41 +2,37 @@ # GNU General Public License v2.0 (see COPYING or https://www.gnu.org/licenses/gpl-2.0.txt) +import mock import unittest -from resources.lib.vrtplayer import vrtplayer -from resources.lib.vrtplayer import vrtapihelper -from mock import MagicMock + +from resources.lib.vrtplayer import vrtapihelper, vrtplayer class TestVRTPlayer(unittest.TestCase): + _kodi_wrapper = mock.MagicMock() + _kodi_wrapper.get_proxies = mock.MagicMock(return_value=dict()) + def test_show_videos_single_episode_shows_videos(self): - mock = MagicMock() - mock.show_listing() - player = vrtplayer.VRTPlayer(None, mock, None, vrtapihelper.VRTApiHelper(mock)) + player = vrtplayer.VRTPlayer(None, self._kodi_wrapper, None, vrtapihelper.VRTApiHelper(self._kodi_wrapper)) + self._kodi_wrapper.show_listing() player.show_episodes('/vrtnu/a-z/tussen-nu-en-morgen/2018/tussen-nu-en-morgen.relevant/') - self.assertTrue(mock.show_listing.called) + self.assertTrue(self._kodi_wrapper.show_listing.called) def test_show_videos_single_season_shows_videos(self): - mock = MagicMock() - mock.show_listing() - player = vrtplayer.VRTPlayer(None, mock, None, vrtapihelper.VRTApiHelper(mock)) + player = vrtplayer.VRTPlayer(None, self._kodi_wrapper, None, vrtapihelper.VRTApiHelper(self._kodi_wrapper)) player.show_episodes('/vrtnu/a-z/apocalyps--de-eerste-wereldoorlog/1/apocalyps--de-eerste-wereldoorlog-s1a3.relevant/') - self.assertTrue(mock.show_listing.called) + self.assertTrue(self._kodi_wrapper.show_listing.called) def test_show_videos_multiple_seasons_shows_videos(self): - mock = MagicMock() - mock.show_listing() - player = vrtplayer.VRTPlayer(None, mock, None, vrtapihelper.VRTApiHelper(mock)) + player = vrtplayer.VRTPlayer(None, self._kodi_wrapper, None, vrtapihelper.VRTApiHelper(self._kodi_wrapper)) player.show_episodes('vrtnu/a-z/animal-babies.relevant/') - self.assertTrue(mock.show_listing.called) + self.assertTrue(self._kodi_wrapper.show_listing.called) def test_show_videos_specific_seasons_shows_videos(self): - mock = MagicMock() - mock.show_listing() - player = vrtplayer.VRTPlayer(None, mock, None, vrtapihelper.VRTApiHelper(mock)) + player = vrtplayer.VRTPlayer(None, self._kodi_wrapper, None, vrtapihelper.VRTApiHelper(self._kodi_wrapper)) player.show_episodes('/vrtnu/a-z/thuis/24.lists.all-episodes.relevant/') - self.assertTrue(mock.show_listing.called) + self.assertTrue(self._kodi_wrapper.show_listing.called) if __name__ == '__main__':