Skip to content

Commit 49a3240

Browse files
authored
Merge pull request #4595 from dimkroon/matrix_viwx_1-5-1
[plugin.video.viwx] v1.5.1
2 parents f433709 + 34ac0ea commit 49a3240

File tree

8 files changed

+84
-45
lines changed

8 files changed

+84
-45
lines changed

plugin.video.viwx/addon.xml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<addon id="plugin.video.viwx" name="viwX" version="1.5.0" provider-name="Dimitri Kroon">
2+
<addon id="plugin.video.viwx" name="viwX" version="1.5.1" provider-name="Dimitri Kroon">
33
<requires>
44
<import addon="xbmc.python" version="3.0.0"/>
55
<import addon="inputstream.adaptive" version="19.0.5"/>
@@ -30,13 +30,18 @@
3030
<fanart>resources/fanart.png</fanart>
3131
</assets>
3232
<news>
33-
[B]v1.4.1[/B]
33+
[B]v1.5.1[/B]
3434
[B]Fixes:[/B]
35-
* 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.
35+
* News clips failed to play with KeyError 'Brand'.
36+
* Error messages when opening an empty 'Continue Watching' list.
37+
* Stream-only FAST channels stalled in advert breaks.
38+
* Programmes are now reported as having been fully played when the user has skipped to the end while playing.
39+
* Some programmes were missing en IPTV EPG.
40+
* Search now requests the same number of items as a generic web browser does.
3641

3742
[B]New Features:[/B]
38-
* Episodes in 'Continue Watching' now have context menu item 'Show all episodes', which opens the programme folder with all series and episodes.
39-
* Trending now shows programmes with episodes as folder, rather than playing the first episode of series 1.
43+
* Paid items are excluded from search results based on the setting 'Hide premium content'.
44+
* On most live channels it's now possible to seek back up to 1 hour from the moment the channel is started.
4045
</news>
4146
<reuselanguageinvoker>true</reuselanguageinvoker>
4247
</extension>

plugin.video.viwx/changelog.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
v 1.5.1
2+
Fixes:
3+
- News clips failed to play with KeyError 'Brand'.
4+
- Error messages when opening an empty 'Continue Watching' list.
5+
- Stream-only FAST channels stalled in advert breaks.
6+
- Programmes are now reported as having been fully played when the user has skipped to the end while playing.
7+
- Some programmes were missing en IPTV EPG.
8+
- Search now requests the same number of items as a generic web browser does.
9+
10+
New Features:
11+
- Paid items are excluded from search results based on the setting 'Hide premium content'.
12+
- On most live channels it's now possible to seek back up to 1 hour from the moment the channel is started.
13+
114
v1.5.0
215
Fixes:
316
- 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.

plugin.video.viwx/resources/lib/fetch.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ def _create_cookiejar():
165165

166166
except (FileNotFoundError, pickle.UnpicklingError):
167167
cj = set_default_cookies(PersistentCookieJar(cookie_file))
168+
cj.cassie_converted = True
168169
logger.info("Created new cookiejar")
169170
return cj
170171

@@ -268,7 +269,7 @@ def web_request(method, url, headers=None, data=None, **kwargs):
268269
if 400 <= e.response.status_code < 500:
269270
# noinspection PyBroadException
270271
try:
271-
resp_data = resp.json()
272+
resp_data = json.loads(resp.content.decode('utf8'))
272273
except:
273274
# Intentional broad exception as requests can raise various types of errors
274275
# depending on python, etc.
@@ -302,23 +303,23 @@ def post_json(url, data, headers=None, **kwargs):
302303
dflt_headers.update(headers)
303304
resp = web_request('POST', url, dflt_headers, data, **kwargs)
304305
try:
305-
return resp.json()
306+
return json.loads(resp.content.decode('utf8'))
306307
except json.JSONDecodeError:
307-
raise FetchError(Script.localize(30920))
308+
raise ParseError(Script.localize(30920))
308309

309310

310311
def get_json(url, headers=None, **kwargs):
311-
"""Make a GET reguest and expect JSON data back."""
312+
"""Make a GET request and expect JSON data back."""
312313
dflt_headers = {'Accept': 'application/json'}
313314
if headers:
314315
dflt_headers.update(headers)
315316
resp = web_request('GET', url, dflt_headers, **kwargs)
316317
if resp.status_code == 204: # No Content
317318
return None
318319
try:
319-
return resp.json()
320+
return json.loads(resp.content.decode('utf8'))
320321
except json.JSONDecodeError:
321-
raise FetchError(Script.localize(30920))
322+
raise ParseError(Script.localize(30920))
322323

323324

324325
def put_json(url, data, headers=None, **kwargs):
@@ -337,9 +338,9 @@ def delete_json(url, data, headers=None, **kwargs):
337338
if resp.status_code == 204: # No Content
338339
return None
339340
try:
340-
return resp.json()
341+
return json.loads(resp.content.decode('utf8'))
341342
except json.JSONDecodeError:
342-
raise FetchError(Script.localize(30920))
343+
raise ParseError(Script.localize(30920))
343344

344345

345346
def get_document(url, headers=None, **kwargs):

plugin.video.viwx/resources/lib/itv.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# ----------------------------------------------------------------------------------------------------------------------
2-
# Copyright (c) 2022-2023 Dimitri Kroon.
2+
# Copyright (c) 2022-2024 Dimitri Kroon.
33
# This file is part of plugin.video.viwx.
44
# SPDX-License-Identifier: GPL-2.0-or-later
55
# See LICENSE.txt
@@ -8,7 +8,7 @@
88
import os
99
import logging
1010

11-
from datetime import datetime, timedelta
11+
from datetime import datetime, timedelta, timezone
1212
import pytz
1313
import xbmc
1414

@@ -135,10 +135,10 @@ def get_live_urls(url=None, title=None, start_time=None, play_from_start=False):
135135
if start_time and (play_from_start or kodi_utils.ask_play_from_start(title)):
136136
dash_url = start_again_url.format(START_TIME=start_time)
137137
logger.debug('get_live_urls - selected play from start at %s', start_time)
138-
# Fast channels play only for about 5 minutes on the time shift stream
139-
elif not channel.startswith('FAST'):
140-
# Go 30 sec back to ensure we get the timeshift stream
141-
start_time = datetime.utcnow() - timedelta(seconds=30)
138+
elif video_locations.get('IsDar'):
139+
# Go 1 hour back to ensure we get the timeshift stream with adverts embedded
140+
# and can skip back a bit in the stream.
141+
start_time = datetime.now(timezone.utc) - timedelta(seconds=3600)
142142
dash_url = start_again_url.format(START_TIME=start_time.strftime('%Y-%m-%dT%H:%M:%S'))
143143

144144
key_service = video_locations['KeyServiceUrl']
@@ -152,12 +152,13 @@ def get_catchup_urls(episode_url):
152152
"""
153153
playlist = _request_stream_data(episode_url, 'catchup')['Playlist']
154154
stream_data = playlist['Video']
155-
url_base = stream_data['Base']
155+
url_base = stream_data.get('Base', '')
156156
video_locations = stream_data['MediaFiles'][0]
157157
dash_url = url_base + video_locations['Href']
158158
key_service = video_locations.get('KeyServiceUrl')
159159
try:
160-
# Usually stream_data['Subtitles'] is just None when subtitles are not available.
160+
# Usually stream_data['Subtitles'] is just None when subtitles are not available,
161+
# but on shortform items it's completely absent.
161162
subtitles = stream_data['Subtitles'][0]['Href']
162163
except (TypeError, KeyError, IndexError):
163164
subtitles = None

plugin.video.viwx/resources/lib/itvx.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ def get_now_next_schedule(local_tz=None):
9797
programs_list.append({
9898
'programme_details': details,
9999
'programmeTitle': displ_title,
100-
'orig_start': None, # fast channels do not support play from start
100+
# Not all fast channels support play from start and at this stage there's
101+
# no to determine which do.
102+
'orig_start': None,
101103
'startTime': utc_start.astimezone(local_tz).strftime(time_format)
102104
})
103105
channel['slot'] = programs_list
@@ -431,9 +433,9 @@ def search(search_term, hide_paid=False):
431433
432434
"""
433435
from urllib.parse import quote
434-
url = 'https://textsearch.prd.oasvc.itv.com/search?broadcaster=itv&featureSet=clearkey,outband-webvtt,hls,aes,' \
435-
'playready,widevine,fairplay,bbts,progressive,hd,rtmpe&onlyFree={}&platform=ctv&query={}'.format(
436-
str(hide_paid).lower(), quote(search_term))
436+
url = ('https://textsearch.prd.oasvc.itv.com/search?broadcaster=itv&channelType=simulcast&'
437+
'featureSet=clearkey,outband-webvtt,hls,aes,playready,widevine,fairplay,bbts,progressive,hd,rtmpe&'
438+
'platform=dotcom&query={}&size=24').format(quote(search_term.lower()))
437439
headers = {
438440
'User-Agent': fetch.USER_AGENT,
439441
'accept': 'application/json',
@@ -461,7 +463,7 @@ def search(search_term, hide_paid=False):
461463
results = data.get('results')
462464
if not results:
463465
logger.debug("Search for '%s' returned an empty list of results. (hide_paid=%s)", search_term, hide_paid)
464-
return (parsex.parse_search_result(result) for result in results)
466+
return (parsex.parse_search_result(result, hide_paid) for result in results)
465467

466468

467469
def my_list(user_id, programme_id=None, operation=None, offer_login=True, use_cache=True):
@@ -525,8 +527,16 @@ def get_last_watched():
525527
user_id, FEATURE_SET)
526528
header = {'accept': 'application/vnd.user.content.v1+json'}
527529
utc_now = datetime.now(tz=timezone.utc).replace(tzinfo=None)
528-
data = itv_account.fetch_authenticated(fetch.get_json, url, headers=header)
529-
watched_list = [parsex.parse_last_watched_item(item, utc_now) for item in data]
530+
try:
531+
data = itv_account.fetch_authenticated(fetch.get_json, url, headers=header)
532+
except (errors.HttpError, errors.ParseError):
533+
# A wide variety of responses have been observed when the watch list has no items.
534+
# Just regard any HTTP, or JSON decoding error as an empty list.
535+
data = None
536+
if data:
537+
watched_list = [parsex.parse_last_watched_item(item, utc_now) for item in data]
538+
else:
539+
watched_list = []
530540
cache.set_item(cache_key, watched_list, 600)
531541
return watched_list
532542

plugin.video.viwx/resources/lib/main.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -556,10 +556,8 @@ def play_stream_live(addon, channel, url=None, title=None, start_time=None, play
556556
play_from_start)
557557
list_item = create_dash_stream_item(channel, manifest_url, key_service_url)
558558
if list_item:
559-
# list_item.setProperty('inputstream.adaptive.manifest_update_parameter', 'full')
560-
if '?t=' in manifest_url or '&t=' in manifest_url:
559+
if start_time and ('?t=' in manifest_url or '&t=' in manifest_url):
561560
list_item.setProperty('inputstream.adaptive.play_timeshift_buffer', 'true')
562-
# list_item.property['inputstream.adaptive.live_delay'] = '2'
563561
logger.debug("play live stream - timeshift_buffer enabled")
564562
else:
565563
logger.debug("play live stream timeshift_buffer disabled")

plugin.video.viwx/resources/lib/parsex.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -486,14 +486,17 @@ def parse_episode_title(title_data, brand_fanart=None, prefer_bsl=False):
486486
return title_obj
487487

488488

489-
def parse_search_result(search_data):
489+
def parse_search_result(search_data, hide_paid=False):
490490
entity_type = search_data['entityType']
491491
result_data = search_data['data']
492492
api_episode_id = ''
493-
if 'FREE' in result_data['tier']:
494-
plot = result_data['synopsis']
495-
else:
493+
494+
if 'PAID' in result_data['tier']:
495+
if hide_paid:
496+
return
496497
plot = premium_plot(result_data['synopsis'])
498+
else:
499+
plot = result_data['synopsis']
497500

498501
if entity_type == 'programme':
499502
prog_name = result_data['programmeTitle']
@@ -668,14 +671,14 @@ def parse_schedule_item(data):
668671
from urllib.parse import quote
669672

670673
plugin_id = utils.addon_info.id
671-
674+
genres = data.get('genres')
672675
try:
673676
item = {
674677
'start': data['start'],
675678
'stop': data['end'],
676679
'title': data['title'],
677680
'description': '\n\n'.join(t for t in (data.get('description'), data.get('guidance')) if t),
678-
'genre': data.get('genres', [{}])[0].get('name'),
681+
'genre': genres[0].get('name') if genres else None,
679682
}
680683

681684
episode_nr = data.get('episodeNumber')

plugin.video.viwx/resources/lib/xprogress.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# ----------------------------------------------------------------------------------------------------------------------
2-
# Copyright (c) 2023 Dimitri Kroon.
2+
# Copyright (c) 2023-2024 Dimitri Kroon.
33
# This file is part of plugin.video.viwx.
44
# SPDX-License-Identifier: GPL-2.0-or-later
55
# See LICENSE.txt
@@ -41,6 +41,7 @@ def __init__(self, production_id):
4141
self._production_id = production_id
4242
self._event_seq_nr = 0
4343
self._playtime = 0
44+
self._totaltime = 0
4445
self._user_id = itv_session().user_id
4546
self.monitor = Monitor()
4647
self._status = PlayState.UNDEFINED
@@ -62,8 +63,9 @@ def onAVStarted(self) -> None:
6263
try:
6364
self._cur_file = self.getPlayingFile()
6465
self._playtime = self.getTime()
66+
self._totaltime = self.getTotalTime()
6567
self._status = PlayState.PLAYING
66-
logger.debug("PlayTimeMonitor: total play time = %s", self.playtime/60)
68+
logger.debug("PlayTimeMonitor: total play time = %s", self._totaltime/60)
6769
self._post_event_startup_complete()
6870
except:
6971
logger.error("PlayTimeMonitor.onAVStarted:\n", exc_info=True)
@@ -88,6 +90,13 @@ def onPlayBackEnded(self) -> None:
8890
def onPlayBackError(self) -> None:
8991
self.onPlayBackStopped()
9092

93+
# noinspection PyShadowingNames,PyPep8Naming
94+
def onPlayBackSeek(self, time: int, seekOffset: int) -> None:
95+
if time/1000 > self._totaltime - 2:
96+
# skipped beyond end of stream
97+
self._playtime = self._totaltime
98+
self.onPlayBackStopped()
99+
91100
def wait_until_playing(self, timeout) -> bool:
92101
"""Wait and return `True` when the player has started playing.
93102
Return `False` when `timeout` expires, or when playing has been aborted before
@@ -101,7 +110,7 @@ def wait_until_playing(self, timeout) -> bool:
101110
if self.monitor.waitForAbort(0.2):
102111
logger.debug("wait_until_playing ended: abort requested")
103112
return False
104-
return not self._status is PlayState.STOPPED
113+
return self._status is not PlayState.STOPPED
105114

106115
def monitor_progress(self) -> None:
107116
"""Wait while the player is playing and return when playing the file has stopped.
@@ -126,8 +135,8 @@ def monitor_progress(self) -> None:
126135
def initialise(self):
127136
"""Initialise play state reports.
128137
129-
Create an instance ID and post a 'open' event. Subsequent events are to use the
130-
same instance ID. So if posting fails it's no use going on monitoring and post
138+
Create an instance ID and post an 'open' event. Subsequent events are to use the
139+
same instance ID. So, if posting fails it's no use going on monitoring and post
131140
other events.
132141
133142
"""
@@ -204,7 +213,6 @@ def _post_event_seek(self, from_position: float):
204213
'seekButtonInteract': 0}
205214
self._handle_event(data, 'seek')
206215

207-
208216
def _post_event_stop(self):
209217
"""Stop event. Only seen on mobile app. Currently not used."""
210218
self._event_seq_nr += 1
@@ -219,7 +227,7 @@ def _post_event_stop(self):
219227
}
220228
fetch.web_request('post', EVT_URL, data=data)
221229

222-
def _handle_event(self, data:dict, evt_type:str):
230+
def _handle_event(self, data: dict, evt_type: str):
223231
self._event_seq_nr += 1
224232
post_data = {
225233
'_v': '1.2.2',
@@ -250,4 +258,4 @@ def playtime_monitor(production_id):
250258
player.wait_until_playing(15)
251259
player.monitor_progress()
252260
except Exception as e:
253-
logger.error("Playtime monitoring aborted due to unhandled exception: %r", e)
261+
logger.error("Playtime monitoring aborted due to unhandled exception: %r", e)

0 commit comments

Comments
 (0)