Skip to content

Commit 344f3d4

Browse files
committed
[plugin.video.cbc] v4.0.23
1 parent 32731dc commit 344f3d4

File tree

7 files changed

+195
-112
lines changed

7 files changed

+195
-112
lines changed

plugin.video.cbc/addon.xml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
22
<addon id="plugin.video.cbc"
33
name="Canadian Broadcasting Corp (CBC)"
4-
version="4.0.22+matrix.1"
4+
version="4.0.23+matrix.1"
55
provider-name="micahg,t1m,smf007,oshanrube,jgaudet">
66
<requires>
77
<import addon="xbmc.python" version="3.0.0"/>
@@ -29,8 +29,10 @@
2929
<website>https://watch.cbc.ca/</website>
3030
<source>https://github.com/micahg/plugin.video.cbc</source>
3131
<news>
32-
- Updates for new Auth API
33-
- Fixes for live events
32+
- Add back missing live channels
33+
- Better live event handling
34+
- Better EPG error handling
35+
- Remove dead code and fix match bug
3436
</news>
3537
</extension>
3638
</addon>

plugin.video.cbc/default.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import routing
1313

1414
from resources.lib.cbc import CBC
15-
from resources.lib.utils import log, getAuthorizationFile, get_cookie_file, get_iptv_channels_file
15+
from resources.lib.utils import log, getAuthorizationFile, get_iptv_channels_file, is_pending, iso8601_to_local
1616
from resources.lib.livechannels import LiveChannels
1717
from resources.lib.gemv2 import GemV2
1818
from resources.lib.iptvmanager import IPTVManager
@@ -119,7 +119,6 @@ def logout():
119119
"""Remove authorization stuff."""
120120
log('Logging out...', True)
121121
os.remove(getAuthorizationFile())
122-
os.remove(get_cookie_file())
123122

124123

125124
@plugin.route('/iptv/channels')
@@ -174,6 +173,7 @@ def play_live_channel():
174173
def live_channels_menu():
175174
"""Populate the menu with live channels."""
176175
xbmcplugin.setContent(plugin.handle, 'videos')
176+
xbmcplugin.addSortMethod(plugin.handle, xbmcplugin.SORT_METHOD_LABEL)
177177
chans = LiveChannels()
178178
chan_list = chans.get_live_channels()
179179
cbc = CBC()
@@ -184,7 +184,10 @@ def live_channels_menu():
184184
item = xbmcgui.ListItem(labels['title'])
185185
item.setArt({'thumb': image, 'poster': image})
186186
item.setInfo(type="Video", infoLabels=labels)
187-
item.setProperty('IsPlayable', 'true')
187+
air_date = channel.get('airDate')
188+
local_dt = iso8601_to_local(air_date) if air_date else None
189+
if local_dt is None or not is_pending(local_dt):
190+
item.setProperty('IsPlayable', 'true')
188191
item.addContextMenuItems([
189192
(getString(30014), 'RunPlugin({})'.format(plugin.url_for(live_channels_add_all))),
190193
(getString(30015), 'RunPlugin({})'.format(plugin.url_for(live_channels_add, callsign))),
@@ -222,16 +225,16 @@ def layout_menu(path):
222225
for f in items:
223226
n = GemV2.normalized_format_item(f)
224227
p = GemV2.normalized_format_path(f, path)
225-
item = xbmcgui.ListItem(n['label'])
228+
item = xbmcgui.ListItem(n['title'])
226229
if 'art' in n:
227230
item.setArt(n['art'])
228231
item.setInfo(type="Video", infoLabels=n['info_labels'])
229-
if n['playable']:
230-
item.setProperty('IsPlayable', 'true')
232+
if 'app_code' in n and n['app_code']:
233+
item.setProperty('IsPlayable', 'true' if 'playable' in n and n['playable'] else 'false')
231234
url = plugin.url_for(play_live_channel, id=p, app_code=n['app_code'])
232235
else:
233236
url = plugin.url_for(layout_menu, p)
234-
xbmcplugin.addDirectoryItem(handle, url, item, not n['playable'])
237+
xbmcplugin.addDirectoryItem(handle, url, item, not 'app_code' in n)
235238
xbmcplugin.endOfDirectory(handle)
236239

237240

plugin.video.cbc/resources/lib/cbc.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
import json
77
# import http.client as http_client
88
from urllib.parse import urlparse, parse_qs, quote
9-
from xml.dom.minidom import parseString
109

1110
import requests
1211

13-
from .utils import save_cookies, loadCookies, saveAuthorization, log
12+
from .utils import saveAuthorization, log, iso8601_to_local, is_pending
1413

1514
# http_client.HTTPConnection.debuglevel = 1
1615

@@ -65,9 +64,6 @@ def __init__(self):
6564
"""Initialize the CBC class."""
6665
# Create requests session object
6766
self.session = requests.Session()
68-
session_cookies = loadCookies()
69-
if session_cookies is not None:
70-
self.session.cookies = session_cookies
7167

7268
@staticmethod
7369
def azure_authorize_authorize(sess: requests.Session):
@@ -336,8 +332,8 @@ def get_labels(item):
336332
'studio': 'Canadian Broadcasting Corporation',
337333
'country': 'Canada'
338334
}
339-
if 'cbc$callSign' in item:
340-
labels['title'] = '{} {}'.format(item['cbc$callSign'], item['title'])
335+
if 'streamTitle' in item:
336+
labels['title'] = item['streamTitle'].encode('utf-8')
341337
else:
342338
labels['title'] = item['title'].encode('utf-8')
343339

@@ -373,14 +369,15 @@ def get_labels(item):
373369
if item['cbc$audioVideo'].lower() == 'video':
374370
labels['mediatype'] = 'video'
375371

372+
if 'airDate' in item:
373+
local_dt = iso8601_to_local(item['airDate'])
374+
if local_dt is not None:
375+
is_pending(local_dt, item=labels)
376+
376377
return labels
377378

378379

379380
@staticmethod
380381
def get_session():
381382
"""Get a requests session object with CBC cookies."""
382-
sess = requests.Session()
383-
cookies = loadCookies()
384-
if cookies is not None:
385-
sess.cookies = cookies
386-
return sess
383+
return requests.Session()

plugin.video.cbc/resources/lib/epg.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
# Y/M/D
1414
GUIDE_URL_FMT = 'https://www.cbc.ca/programguide/daily/{}/cbc_television'
15+
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'
16+
REQUEST_TIMEOUT = (10, 20)
1517

1618
# This is a combination of actual callsigns (NN for newsnet) and guid values
1719
# for over-the-top-only services. No doubt, someone will come back here some
@@ -63,8 +65,16 @@ def map_channel_ids(unblocked):
6365
"""Map channel IDs to guide names."""
6466
url = get_guide_url(datetime.now())
6567
data = call_guide_url(url)
68+
if not data:
69+
log('Unable to map channel IDs: no data returned from {}'.format(url), True)
70+
return { sign: None for sign in SPECIAL_GUIDES.keys()}
71+
6672
soup = BeautifulSoup(data, features="html.parser")
6773
select = soup.find('select', id="selectlocation-tv")
74+
if select is None:
75+
log('Unable to map channel IDs: missing location selector at {}'.format(url), True)
76+
return { sign: None for sign in SPECIAL_GUIDES.keys()}
77+
6878
options = select.find_all('option')
6979
channel_map = { sign: None for sign in SPECIAL_GUIDES.keys()}
7080
for option in options:
@@ -87,9 +97,14 @@ def get_guide_url(dttm, callsign=None):
8797
def call_guide_url(url, location=None):
8898
"""Call the guide URL and return the response body."""
8999
cookies = {}
100+
headers = {'User-Agent': USER_AGENT}
90101
if location is not None:
91102
cookies['pgTvLocation'] = location
92-
resp = requests.get(url, cookies=cookies)
103+
try:
104+
resp = requests.get(url, cookies=cookies, headers=headers, timeout=REQUEST_TIMEOUT)
105+
except requests.RequestException as ex:
106+
log(f'HTTP request failed for {url}: {ex}', True)
107+
return None
93108
if resp.status_code != 200:
94109
log('{} returns status of {}'.format(url, resp.status_code), True)
95110
return None
@@ -101,9 +116,21 @@ def get_channel_data(dttm, channel, callsign):
101116
epg_data = []
102117
url = get_guide_url(dttm, callsign)
103118
data = call_guide_url(url, channel)
119+
if not data:
120+
return epg_data
121+
104122
soup = BeautifulSoup(data, features="html.parser")
105123

106-
select = soup.find('table', id="sched-table").find('tbody')
124+
table = soup.find('table', id="sched-table")
125+
if table is None:
126+
log('Missing schedule table at "{}"'.format(url), True)
127+
return epg_data
128+
129+
select = table.find('tbody')
130+
if select is None:
131+
log('Missing schedule body at "{}"'.format(url), True)
132+
return epg_data
133+
107134
progs = select.find_all('tr')
108135
for prog in progs:
109136
prog_js = {}
@@ -122,7 +149,9 @@ def get_channel_data(dttm, channel, callsign):
122149
prog_js = {}
123150
break
124151
prog_js['title'] = title_cell.get_text()
125-
prog_js['description'] = cell.find('dd').get_text()
152+
description_cell = cell.find('dd')
153+
if description_cell is not None:
154+
prog_js['description'] = description_cell.get_text()
126155

127156
# skip the header row
128157
if len(prog_js.items()) == 0:

plugin.video.cbc/resources/lib/gemv2.py

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
"""Module for the V2 Gem API."""
22
import json
3-
from datetime import datetime
43

54
import requests
65

76
from resources.lib.cbc import CBC
8-
from resources.lib.utils import loadAuthorization, log
7+
from resources.lib.utils import loadAuthorization, log, iso8601_to_local, is_pending
98

109

1110
# api CONFIG IS AT https://services.radio-canada.ca/ott/catalog/v1/gem/settings?device=web
@@ -18,15 +17,6 @@
1817
class GemV2:
1918
"""V2 Gem API class."""
2019

21-
@staticmethod
22-
def iso8601_to_local(dttm):
23-
"""Convert an ISO 8601 timestamp (UTC or offset-aware) to local time string."""
24-
try:
25-
local_dt = datetime.fromisoformat(dttm.replace('Z', '+00:00')).astimezone()
26-
return local_dt.strftime('%Y-%m-%d %H:%M:%S')
27-
except (ValueError, TypeError, AttributeError):
28-
return dttm
29-
3020
@staticmethod
3121
def scrape_json(uri, headers=None, params=None):
3222
if headers is None:
@@ -198,7 +188,7 @@ def normalized_format_item(item):
198188
images = item['images'] if 'images' in item else None
199189
title = item['label'] if 'label' in item else item['title']
200190
retval = {
201-
'label': title,
191+
'title': title,
202192
'playable': 'idMedia' in item,
203193
'info_labels': {
204194
'tvshowtitle': title,
@@ -225,22 +215,23 @@ def normalized_format_item(item):
225215
if 'credits' in meta:
226216
retval['info_labels']['cast'] = meta['credits'][0]['peoples'].split(',')
227217
if 'live' in meta and 'startDate' in meta['live']:
228-
dttm = GemV2.iso8601_to_local(meta['live']['startDate'])
229-
retval['label'] += f' [Live: {dttm}]'
218+
local_dt = iso8601_to_local(meta['live']['startDate'])
219+
if local_dt is not None and is_pending(local_dt, item=retval):
220+
retval['playable'] = False
230221

231222
if 'idMedia' in item:
232223
# logic in https://services.radio-canada.ca/ott/catalog/v1/gem/settings?device=web
233224
if 'type' in item:
234-
match item['type'].lower():
235-
case 'media':
236-
retval['app_code'] = 'gem'
237-
case 'quickturn':
238-
retval['app_code'] = 'medianet'
239-
case 'liveevent' | 'replay':
240-
retval['app_code'] = 'medianetlive'
241-
case _:
242-
log(f'Unknown type {item["type"]} for item with idMedia {item["idMedia"]}, defaulting to app_code "gem"')
243-
retval['app_code'] = 'gem'
225+
item_type = item['type'].lower()
226+
if item_type == 'media':
227+
retval['app_code'] = 'gem'
228+
elif item_type == 'quickturn':
229+
retval['app_code'] = 'medianet'
230+
elif item_type == 'liveevent' or item_type == 'replay':
231+
retval['app_code'] = 'medianetlive'
232+
else:
233+
log(f'Unknown type {item["type"]} for item with idMedia {item["idMedia"]}, defaulting to app_code "gem"')
234+
retval['app_code'] = 'gem'
244235

245236

246237
return retval

0 commit comments

Comments
 (0)