Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[plugin.video.viwx] v1.5.1 #4595

Merged
merged 1 commit into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions plugin.video.viwx/addon.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="plugin.video.viwx" name="viwX" version="1.5.0" provider-name="Dimitri Kroon">
<addon id="plugin.video.viwx" name="viwX" version="1.5.1" provider-name="Dimitri Kroon">
<requires>
<import addon="xbmc.python" version="3.0.0"/>
<import addon="inputstream.adaptive" version="19.0.5"/>
Expand Down Expand Up @@ -30,13 +30,18 @@
<fanart>resources/fanart.png</fanart>
</assets>
<news>
[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.
</news>
<reuselanguageinvoker>true</reuselanguageinvoker>
</extension>
Expand Down
13 changes: 13 additions & 0 deletions plugin.video.viwx/changelog.txt
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
17 changes: 9 additions & 8 deletions plugin.video.viwx/resources/lib/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -302,23 +303,23 @@ 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)
resp = web_request('GET', url, dflt_headers, **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):
Expand All @@ -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):
Expand Down
17 changes: 9 additions & 8 deletions plugin.video.viwx/resources/lib/itv.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -8,7 +8,7 @@
import os
import logging

from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
import pytz
import xbmc

Expand Down Expand Up @@ -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']
Expand All @@ -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
Expand Down
24 changes: 17 additions & 7 deletions plugin.video.viwx/resources/lib/itvx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down
4 changes: 1 addition & 3 deletions plugin.video.viwx/resources/lib/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
15 changes: 9 additions & 6 deletions plugin.video.viwx/resources/lib/parsex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -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')
Expand Down
24 changes: 16 additions & 8 deletions plugin.video.viwx/resources/lib/xprogress.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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.

"""
Expand Down Expand Up @@ -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
Expand All @@ -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',
Expand Down Expand Up @@ -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)
logger.error("Playtime monitoring aborted due to unhandled exception: %r", e)
Loading