Skip to content

Commit 4892bbd

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

File tree

7 files changed

+124
-108
lines changed

7 files changed

+124
-108
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: 8 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,14 @@ 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+
is_pending(local_dt, item=labels)
375+
376376
return labels
377377

378378

379379
@staticmethod
380380
def get_session():
381381
"""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
382+
return requests.Session()

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

Lines changed: 8 additions & 1 deletion
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
@@ -87,9 +89,14 @@ def get_guide_url(dttm, callsign=None):
8789
def call_guide_url(url, location=None):
8890
"""Call the guide URL and return the response body."""
8991
cookies = {}
92+
headers = {'User-Agent': USER_AGENT}
9093
if location is not None:
9194
cookies['pgTvLocation'] = location
92-
resp = requests.get(url, cookies=cookies)
95+
try:
96+
resp = requests.get(url, cookies=cookies, headers=headers, timeout=REQUEST_TIMEOUT)
97+
except requests.RequestException as ex:
98+
log(f'HTTP request failed for {url}: {ex}', True)
99+
return None
93100
if resp.status_code != 200:
94101
log('{} returns status of {}'.format(url, resp.status_code), True)
95102
return None

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 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

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

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
"""Module for live channels."""
2-
from concurrent import futures
32
import json
43
from urllib.parse import urlencode
54

65
import requests
76

8-
from resources.lib.utils import save_cookies, loadCookies, log, get_iptv_channels_file
7+
from resources.lib.utils import log, get_iptv_channels_file
98
from resources.lib.cbc import CBC
109
from resources.lib.gemv2 import GemV2
1110

12-
LIST_URL = 'https://services.radio-canada.ca/ott/catalog/v2/gem/home?device=web'
13-
LIST_ELEMENT = '2415871718'
11+
LIST_URL = 'https://gem.cbc.ca/_next/data/7ByKb_CElwT2xVJeTO43g/live.json'
1412

1513
class LiveChannels:
1614
"""Class for live channels."""
@@ -19,9 +17,6 @@ def __init__(self):
1917
"""Initialize the live channels class."""
2018
# Create requests session object
2119
self.session = requests.Session()
22-
session_cookies = loadCookies()
23-
if session_cookies is not None:
24-
self.session.cookies = session_cookies
2520

2621
def get_live_channels(self):
2722
"""Get the list of live channels."""
@@ -30,25 +25,48 @@ def get_live_channels(self):
3025
if not resp.status_code == 200:
3126
log('ERROR: {} returns status of {}'.format(LIST_URL, resp.status_code), True)
3227
return None
33-
save_cookies(self.session.cookies)
34-
35-
ret = None
36-
for result in json.loads(resp.content)['lineups']['results']:
37-
if result['key'] == LIST_ELEMENT:
38-
ret = result['items']
39-
40-
future_to_callsign = {}
41-
with futures.ThreadPoolExecutor(max_workers=20) as executor:
42-
for i, channel in enumerate(ret):
43-
callsign = CBC.get_callsign(channel)
44-
future = executor.submit(self.get_channel_metadata, callsign)
45-
future_to_callsign[future] = i
46-
47-
for future in futures.as_completed(future_to_callsign):
48-
i = future_to_callsign[future]
49-
metadata = future.result()
50-
ret[i]['image'] = metadata['Metas']['imageHR']
51-
return ret
28+
29+
data = json.loads(resp.content)
30+
page_data = data.get('pageProps', {}).get('data', {})
31+
streams = page_data.get('streams', [])
32+
free_tv_items = page_data.get('freeTv', {}).get('items', [])
33+
34+
channels = []
35+
for stream in streams:
36+
items = stream.get('items', [])
37+
if len(items) == 0:
38+
continue
39+
40+
for item in items:
41+
channel = dict(item)
42+
if 'title' not in channel or not channel['title']:
43+
channel['title'] = stream.get('title')
44+
if 'genericImage' in channel and 'image' not in channel:
45+
channel['image'] = channel['genericImage']
46+
channels.append(channel)
47+
48+
for item in free_tv_items:
49+
channel = dict(item)
50+
if 'genericImage' in channel and 'image' not in channel:
51+
channel['image'] = channel['genericImage']
52+
channels.append(channel)
53+
54+
unique_channels = []
55+
seen_ids = set()
56+
for channel in channels:
57+
id_media = channel.get('idMedia')
58+
59+
if id_media is None:
60+
unique_channels.append(channel)
61+
continue
62+
63+
if id_media in seen_ids:
64+
continue
65+
66+
seen_ids.add(id_media)
67+
unique_channels.append(channel)
68+
69+
return unique_channels
5270

5371
def get_iptv_channels(self):
5472
"""Get the channels in a IPTV Manager compatible list."""
@@ -99,7 +117,6 @@ def get_channel_metadata(self, id):
99117
if not resp.status_code == 200:
100118
log('ERROR: {} returns status of {}'.format(LIST_URL, resp.status_code), True)
101119
return None
102-
save_cookies(self.session.cookies)
103120
return json.loads(resp.content)
104121

105122
@staticmethod

0 commit comments

Comments
 (0)