From 34ac0eaaa84d5af172bc51010c640e77e4924ed7 Mon Sep 17 00:00:00 2001 From: dimkroon <111366411+dimkroon@users.noreply.github.com> Date: Mon, 4 Nov 2024 02:27:29 +0100 Subject: [PATCH] [plugin.video.viwx] v1.5.1 --- plugin.video.viwx/addon.xml | 15 ++++++++---- plugin.video.viwx/changelog.txt | 13 +++++++++++ plugin.video.viwx/resources/lib/fetch.py | 17 +++++++------- plugin.video.viwx/resources/lib/itv.py | 17 +++++++------- plugin.video.viwx/resources/lib/itvx.py | 24 ++++++++++++++------ plugin.video.viwx/resources/lib/main.py | 4 +--- plugin.video.viwx/resources/lib/parsex.py | 15 +++++++----- plugin.video.viwx/resources/lib/xprogress.py | 24 +++++++++++++------- 8 files changed, 84 insertions(+), 45 deletions(-) diff --git a/plugin.video.viwx/addon.xml b/plugin.video.viwx/addon.xml index f6ec59f8e..9ef9f42b3 100644 --- a/plugin.video.viwx/addon.xml +++ b/plugin.video.viwx/addon.xml @@ -1,5 +1,5 @@ - + @@ -30,13 +30,18 @@ resources/fanart.png -[B]v1.4.1[/B] +[B]v1.5.1[/B] [B]Fixes:[/B] -* ViwX failed to start with 'FetchError: Forbidden'. And issue only experienced by users of OSMC and possibly some other systems that still use OpenSSL v1.1.1. +* News clips failed to play with KeyError 'Brand'. +* Error messages when opening an empty 'Continue Watching' list. +* Stream-only FAST channels stalled in advert breaks. +* Programmes are now reported as having been fully played when the user has skipped to the end while playing. +* Some programmes were missing en IPTV EPG. +* Search now requests the same number of items as a generic web browser does. [B]New Features:[/B] -* Episodes in 'Continue Watching' now have context menu item 'Show all episodes', which opens the programme folder with all series and episodes. -* Trending now shows programmes with episodes as folder, rather than playing the first episode of series 1. +* Paid items are excluded from search results based on the setting 'Hide premium content'. +* On most live channels it's now possible to seek back up to 1 hour from the moment the channel is started. true diff --git a/plugin.video.viwx/changelog.txt b/plugin.video.viwx/changelog.txt index ac7c0c54b..c9df6cb6c 100644 --- a/plugin.video.viwx/changelog.txt +++ b/plugin.video.viwx/changelog.txt @@ -1,3 +1,16 @@ +v 1.5.1 +Fixes: +- News clips failed to play with KeyError 'Brand'. +- Error messages when opening an empty 'Continue Watching' list. +- Stream-only FAST channels stalled in advert breaks. +- Programmes are now reported as having been fully played when the user has skipped to the end while playing. +- Some programmes were missing en IPTV EPG. +- Search now requests the same number of items as a generic web browser does. + +New Features: +- Paid items are excluded from search results based on the setting 'Hide premium content'. +- On most live channels it's now possible to seek back up to 1 hour from the moment the channel is started. + v1.5.0 Fixes: - ViwX failed to start with 'FetchError: Forbidden'. And issue only experienced by users of OSMC and possibly some other systems that still use OpenSSL v1.1.1. diff --git a/plugin.video.viwx/resources/lib/fetch.py b/plugin.video.viwx/resources/lib/fetch.py index 97fc13510..171c58d01 100644 --- a/plugin.video.viwx/resources/lib/fetch.py +++ b/plugin.video.viwx/resources/lib/fetch.py @@ -165,6 +165,7 @@ def _create_cookiejar(): except (FileNotFoundError, pickle.UnpicklingError): cj = set_default_cookies(PersistentCookieJar(cookie_file)) + cj.cassie_converted = True logger.info("Created new cookiejar") return cj @@ -268,7 +269,7 @@ def web_request(method, url, headers=None, data=None, **kwargs): if 400 <= e.response.status_code < 500: # noinspection PyBroadException try: - resp_data = resp.json() + resp_data = json.loads(resp.content.decode('utf8')) except: # Intentional broad exception as requests can raise various types of errors # depending on python, etc. @@ -302,13 +303,13 @@ def post_json(url, data, headers=None, **kwargs): dflt_headers.update(headers) resp = web_request('POST', url, dflt_headers, data, **kwargs) try: - return resp.json() + return json.loads(resp.content.decode('utf8')) except json.JSONDecodeError: - raise FetchError(Script.localize(30920)) + raise ParseError(Script.localize(30920)) def get_json(url, headers=None, **kwargs): - """Make a GET reguest and expect JSON data back.""" + """Make a GET request and expect JSON data back.""" dflt_headers = {'Accept': 'application/json'} if headers: dflt_headers.update(headers) @@ -316,9 +317,9 @@ def get_json(url, headers=None, **kwargs): if resp.status_code == 204: # No Content return None try: - return resp.json() + return json.loads(resp.content.decode('utf8')) except json.JSONDecodeError: - raise FetchError(Script.localize(30920)) + raise ParseError(Script.localize(30920)) def put_json(url, data, headers=None, **kwargs): @@ -337,9 +338,9 @@ def delete_json(url, data, headers=None, **kwargs): if resp.status_code == 204: # No Content return None try: - return resp.json() + return json.loads(resp.content.decode('utf8')) except json.JSONDecodeError: - raise FetchError(Script.localize(30920)) + raise ParseError(Script.localize(30920)) def get_document(url, headers=None, **kwargs): diff --git a/plugin.video.viwx/resources/lib/itv.py b/plugin.video.viwx/resources/lib/itv.py index c0a72be82..6ca23704d 100644 --- a/plugin.video.viwx/resources/lib/itv.py +++ b/plugin.video.viwx/resources/lib/itv.py @@ -1,5 +1,5 @@ # ---------------------------------------------------------------------------------------------------------------------- -# Copyright (c) 2022-2023 Dimitri Kroon. +# Copyright (c) 2022-2024 Dimitri Kroon. # This file is part of plugin.video.viwx. # SPDX-License-Identifier: GPL-2.0-or-later # See LICENSE.txt @@ -8,7 +8,7 @@ import os import logging -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pytz import xbmc @@ -135,10 +135,10 @@ def get_live_urls(url=None, title=None, start_time=None, play_from_start=False): if start_time and (play_from_start or kodi_utils.ask_play_from_start(title)): dash_url = start_again_url.format(START_TIME=start_time) logger.debug('get_live_urls - selected play from start at %s', start_time) - # Fast channels play only for about 5 minutes on the time shift stream - elif not channel.startswith('FAST'): - # Go 30 sec back to ensure we get the timeshift stream - start_time = datetime.utcnow() - timedelta(seconds=30) + elif video_locations.get('IsDar'): + # Go 1 hour back to ensure we get the timeshift stream with adverts embedded + # and can skip back a bit in the stream. + start_time = datetime.now(timezone.utc) - timedelta(seconds=3600) dash_url = start_again_url.format(START_TIME=start_time.strftime('%Y-%m-%dT%H:%M:%S')) key_service = video_locations['KeyServiceUrl'] @@ -152,12 +152,13 @@ def get_catchup_urls(episode_url): """ playlist = _request_stream_data(episode_url, 'catchup')['Playlist'] stream_data = playlist['Video'] - url_base = stream_data['Base'] + url_base = stream_data.get('Base', '') video_locations = stream_data['MediaFiles'][0] dash_url = url_base + video_locations['Href'] key_service = video_locations.get('KeyServiceUrl') try: - # Usually stream_data['Subtitles'] is just None when subtitles are not available. + # Usually stream_data['Subtitles'] is just None when subtitles are not available, + # but on shortform items it's completely absent. subtitles = stream_data['Subtitles'][0]['Href'] except (TypeError, KeyError, IndexError): subtitles = None diff --git a/plugin.video.viwx/resources/lib/itvx.py b/plugin.video.viwx/resources/lib/itvx.py index 3985ac3f0..a38844dec 100644 --- a/plugin.video.viwx/resources/lib/itvx.py +++ b/plugin.video.viwx/resources/lib/itvx.py @@ -97,7 +97,9 @@ def get_now_next_schedule(local_tz=None): programs_list.append({ 'programme_details': details, 'programmeTitle': displ_title, - 'orig_start': None, # fast channels do not support play from start + # Not all fast channels support play from start and at this stage there's + # no to determine which do. + 'orig_start': None, 'startTime': utc_start.astimezone(local_tz).strftime(time_format) }) channel['slot'] = programs_list @@ -431,9 +433,9 @@ def search(search_term, hide_paid=False): """ from urllib.parse import quote - url = 'https://textsearch.prd.oasvc.itv.com/search?broadcaster=itv&featureSet=clearkey,outband-webvtt,hls,aes,' \ - 'playready,widevine,fairplay,bbts,progressive,hd,rtmpe&onlyFree={}&platform=ctv&query={}'.format( - str(hide_paid).lower(), quote(search_term)) + url = ('https://textsearch.prd.oasvc.itv.com/search?broadcaster=itv&channelType=simulcast&' + 'featureSet=clearkey,outband-webvtt,hls,aes,playready,widevine,fairplay,bbts,progressive,hd,rtmpe&' + 'platform=dotcom&query={}&size=24').format(quote(search_term.lower())) headers = { 'User-Agent': fetch.USER_AGENT, 'accept': 'application/json', @@ -461,7 +463,7 @@ def search(search_term, hide_paid=False): results = data.get('results') if not results: logger.debug("Search for '%s' returned an empty list of results. (hide_paid=%s)", search_term, hide_paid) - return (parsex.parse_search_result(result) for result in results) + return (parsex.parse_search_result(result, hide_paid) for result in results) def my_list(user_id, programme_id=None, operation=None, offer_login=True, use_cache=True): @@ -525,8 +527,16 @@ def get_last_watched(): user_id, FEATURE_SET) header = {'accept': 'application/vnd.user.content.v1+json'} utc_now = datetime.now(tz=timezone.utc).replace(tzinfo=None) - data = itv_account.fetch_authenticated(fetch.get_json, url, headers=header) - watched_list = [parsex.parse_last_watched_item(item, utc_now) for item in data] + try: + data = itv_account.fetch_authenticated(fetch.get_json, url, headers=header) + except (errors.HttpError, errors.ParseError): + # A wide variety of responses have been observed when the watch list has no items. + # Just regard any HTTP, or JSON decoding error as an empty list. + data = None + if data: + watched_list = [parsex.parse_last_watched_item(item, utc_now) for item in data] + else: + watched_list = [] cache.set_item(cache_key, watched_list, 600) return watched_list diff --git a/plugin.video.viwx/resources/lib/main.py b/plugin.video.viwx/resources/lib/main.py index 02854e491..5fe5ef5e8 100644 --- a/plugin.video.viwx/resources/lib/main.py +++ b/plugin.video.viwx/resources/lib/main.py @@ -556,10 +556,8 @@ def play_stream_live(addon, channel, url=None, title=None, start_time=None, play play_from_start) list_item = create_dash_stream_item(channel, manifest_url, key_service_url) if list_item: - # list_item.setProperty('inputstream.adaptive.manifest_update_parameter', 'full') - if '?t=' in manifest_url or '&t=' in manifest_url: + if start_time and ('?t=' in manifest_url or '&t=' in manifest_url): list_item.setProperty('inputstream.adaptive.play_timeshift_buffer', 'true') - # list_item.property['inputstream.adaptive.live_delay'] = '2' logger.debug("play live stream - timeshift_buffer enabled") else: logger.debug("play live stream timeshift_buffer disabled") diff --git a/plugin.video.viwx/resources/lib/parsex.py b/plugin.video.viwx/resources/lib/parsex.py index fbe1bb0e9..50cf156d7 100644 --- a/plugin.video.viwx/resources/lib/parsex.py +++ b/plugin.video.viwx/resources/lib/parsex.py @@ -486,14 +486,17 @@ def parse_episode_title(title_data, brand_fanart=None, prefer_bsl=False): return title_obj -def parse_search_result(search_data): +def parse_search_result(search_data, hide_paid=False): entity_type = search_data['entityType'] result_data = search_data['data'] api_episode_id = '' - if 'FREE' in result_data['tier']: - plot = result_data['synopsis'] - else: + + if 'PAID' in result_data['tier']: + if hide_paid: + return plot = premium_plot(result_data['synopsis']) + else: + plot = result_data['synopsis'] if entity_type == 'programme': prog_name = result_data['programmeTitle'] @@ -668,14 +671,14 @@ def parse_schedule_item(data): from urllib.parse import quote plugin_id = utils.addon_info.id - + genres = data.get('genres') try: item = { 'start': data['start'], 'stop': data['end'], 'title': data['title'], 'description': '\n\n'.join(t for t in (data.get('description'), data.get('guidance')) if t), - 'genre': data.get('genres', [{}])[0].get('name'), + 'genre': genres[0].get('name') if genres else None, } episode_nr = data.get('episodeNumber') diff --git a/plugin.video.viwx/resources/lib/xprogress.py b/plugin.video.viwx/resources/lib/xprogress.py index 14d7af851..cb5fc58bf 100644 --- a/plugin.video.viwx/resources/lib/xprogress.py +++ b/plugin.video.viwx/resources/lib/xprogress.py @@ -1,5 +1,5 @@ # ---------------------------------------------------------------------------------------------------------------------- -# Copyright (c) 2023 Dimitri Kroon. +# Copyright (c) 2023-2024 Dimitri Kroon. # This file is part of plugin.video.viwx. # SPDX-License-Identifier: GPL-2.0-or-later # See LICENSE.txt @@ -41,6 +41,7 @@ def __init__(self, production_id): self._production_id = production_id self._event_seq_nr = 0 self._playtime = 0 + self._totaltime = 0 self._user_id = itv_session().user_id self.monitor = Monitor() self._status = PlayState.UNDEFINED @@ -62,8 +63,9 @@ def onAVStarted(self) -> None: try: self._cur_file = self.getPlayingFile() self._playtime = self.getTime() + self._totaltime = self.getTotalTime() self._status = PlayState.PLAYING - logger.debug("PlayTimeMonitor: total play time = %s", self.playtime/60) + logger.debug("PlayTimeMonitor: total play time = %s", self._totaltime/60) self._post_event_startup_complete() except: logger.error("PlayTimeMonitor.onAVStarted:\n", exc_info=True) @@ -88,6 +90,13 @@ def onPlayBackEnded(self) -> None: def onPlayBackError(self) -> None: self.onPlayBackStopped() + # noinspection PyShadowingNames,PyPep8Naming + def onPlayBackSeek(self, time: int, seekOffset: int) -> None: + if time/1000 > self._totaltime - 2: + # skipped beyond end of stream + self._playtime = self._totaltime + self.onPlayBackStopped() + def wait_until_playing(self, timeout) -> bool: """Wait and return `True` when the player has started playing. Return `False` when `timeout` expires, or when playing has been aborted before @@ -101,7 +110,7 @@ def wait_until_playing(self, timeout) -> bool: if self.monitor.waitForAbort(0.2): logger.debug("wait_until_playing ended: abort requested") return False - return not self._status is PlayState.STOPPED + return self._status is not PlayState.STOPPED def monitor_progress(self) -> None: """Wait while the player is playing and return when playing the file has stopped. @@ -126,8 +135,8 @@ def monitor_progress(self) -> None: def initialise(self): """Initialise play state reports. - Create an instance ID and post a 'open' event. Subsequent events are to use the - same instance ID. So if posting fails it's no use going on monitoring and post + Create an instance ID and post an 'open' event. Subsequent events are to use the + same instance ID. So, if posting fails it's no use going on monitoring and post other events. """ @@ -204,7 +213,6 @@ def _post_event_seek(self, from_position: float): 'seekButtonInteract': 0} self._handle_event(data, 'seek') - def _post_event_stop(self): """Stop event. Only seen on mobile app. Currently not used.""" self._event_seq_nr += 1 @@ -219,7 +227,7 @@ def _post_event_stop(self): } fetch.web_request('post', EVT_URL, data=data) - def _handle_event(self, data:dict, evt_type:str): + def _handle_event(self, data: dict, evt_type: str): self._event_seq_nr += 1 post_data = { '_v': '1.2.2', @@ -250,4 +258,4 @@ def playtime_monitor(production_id): player.wait_until_playing(15) player.monitor_progress() except Exception as e: - logger.error("Playtime monitoring aborted due to unhandled exception: %r", e) \ No newline at end of file + logger.error("Playtime monitoring aborted due to unhandled exception: %r", e)