1.0.0+kodimov.2: programme guide for every channel, TeleVigo on 15

- The amazon.tv guide that upstream 1.0.0 switched to is a flat list of
  programmes; group it by channel (the guide export crashed with KeyError)
- Add TVG2/TeleVigo listings from the EPG_dobleM guide to epg.xml
- Export channels + programme guide every 6 hours, by default, without
  on-screen notifications
- TeleVigo moves to 15, near the generalist channels

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xockoandClaude Opus 5.5 committed 2026-11-07 11:01:37 +01:00
1 parent 235f7a6cab
commit 119002fbbf
11 files changed
+70 -26

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
+35 -2
View File
@@ -33,14 +33,17 @@ WATERMARKED_BUT_PLAYABLE = {
# Free channels that Movistar+ doesn't carry. 'chno' must not clash with a
# Movistar dial number.
EXTRA_CHANNELS = [
{'id': 'TVG2', 'name': 'TVG2', 'chno': 10, 'group': 'Galicia',
{'id': 'TVG2', 'name': 'TVG2', 'chno': 10, 'group': 'Galicia', 'epg_id': 'TVG2',
'logo': 'https://i.imgur.com/yFy6gxg.png',
'url': 'https://crtvg-tvg2.flumotion.cloud/playlist.m3u8'},
{'id': 'TeleVigo', 'name': 'TeleVigo', 'chno': 155, 'group': 'Galicia',
{'id': 'TeleVigo', 'name': 'TeleVigo', 'chno': 15, 'group': 'Galicia', 'epg_id': 'TELEVIGO',
'logo': 'https://i.imgur.com/4iVmlFa.png',
'url': 'https://stream-us-east-1.getpublica.com/playlist.m3u8?network_id=12687'},
]
# Programme listings for the extra channels (Movistar's guide covers the rest).
EXTRA_EPG_URL = 'https://raw.githubusercontent.com/davidmuma/EPG_dobleM/master/guiafanart_color.xml.gz'
AUTOLOGIN_URL = 'https://soter-pf.svp.video.telefonicaservices.com/service/login/autologin?deviceId={device}&deviceType=webplayer&_={ts}'
SOTER_INITDATA_URL = 'https://soter-pf.sve.video.telefonicaservices.com/service/login/initData?legacy=true'
@@ -167,3 +170,33 @@ def write_if_changed(filename, content):
f.write(content)
os.replace(tmp, filename) if hasattr(os, 'replace') else shutil.move(tmp, filename)
return True
def extra_channels_xmltv(session):
"""Returns the <channel> and <programme> elements for the extra channels,
taken from a public XMLTV guide and renamed to their tvg-id."""
import gzip
import xml.etree.ElementTree as ET
wanted = dict((c['epg_id'], c) for c in EXTRA_CHANNELS if c.get('epg_id'))
out = []
for c in EXTRA_CHANNELS:
out.append('<channel id="{id}">\n <display-name>{name}</display-name>\n <icon src="{logo}"/>\n</channel>\n'.format(**c))
try:
r = session.get(EXTRA_EPG_URL, timeout=(10, 120))
r.raise_for_status()
found = 0
for _, el in ET.iterparse(gzip.GzipFile(fileobj=io.BytesIO(r.content))):
if el.tag == 'programme':
c = wanted.get(el.get('channel'))
if c:
el.set('channel', c['id'])
for icon in el.findall('icon'): el.remove(icon) # large fan art
out.append(ET.tostring(el, encoding='unicode') + '\n')
found += 1
el.clear()
elif el.tag == 'channel':
el.clear()
LOG('extra_channels_xmltv: {} programmes'.format(found))
except Exception as e:
LOG('extra_channels_xmltv: failed: {}'.format(e))
return out
+12 -2
View File
@@ -540,7 +540,16 @@ class Movistar(object):
epg = {}
if 'error' in data: return epg
# The amazon.tv guide is one flat list of programmes; webplayer's is
# grouped by channel. Group the flat one the same way.
if data and isinstance(data[0], dict):
groups = {}
for p in data:
groups.setdefault(p['Canal']['CodCadenaTv'], []).append(p)
data = list(groups.values())
for ch in data:
if not ch: continue
id = ch[0]['Canal']['CodCadenaTv']
if not id in epg: epg[id] = []
for p in ch:
@@ -1537,9 +1546,10 @@ class Movistar(object):
res.append(' <actor>{}</actor>\n'.format(c['name']))
res.append(' </credits>\n');
res.append('</programme>\n')
if self.add_extra_channels:
res.extend(kodimov.extra_channels_xmltv(self.net.session))
res.append('</tv>\n')
with io.open(filename, 'w', encoding='utf-8', newline='') as handle:
handle.write(''.join(res))
kodimov.write_if_changed(filename, ''.join(res))
def save_credentials(self, username, password):
from .b64 import encode_base64
+9 -6
View File
@@ -776,23 +776,26 @@ def export_channels_now():
if m.export_channels_to_m3u8(channels_filename, addon.getSettingBool('only_subscribed')):
LOG('export_channels_now: channel list changed')
def export_epg_now():
def export_epg_now(quiet=False):
# quiet: background refresh from the service, no pop-ups while watching TV
def notify(message):
if not quiet: show_notification(message, xbmcgui.NOTIFICATION_INFO)
def report_progress(message):
show_notification(addon.getLocalizedString(30314).format(message), xbmcgui.NOTIFICATION_INFO)
notify(addon.getLocalizedString(30314).format(message))
if not m.logged: return
only_subscribed = addon.getSettingBool('only_subscribed')
channels_filename = os.path.join(profile_dir, 'channels.m3u8')
epg_filename = os.path.join(profile_dir, 'epg.xml')
show_notification(addon.getLocalizedString(30310), xbmcgui.NOTIFICATION_INFO)
notify(addon.getLocalizedString(30310))
m.export_channels_to_m3u8(channels_filename, only_subscribed)
if True: #not addon.getSettingBool('use_external_epg'):
show_notification(addon.getLocalizedString(30311), xbmcgui.NOTIFICATION_INFO)
notify(addon.getLocalizedString(30311))
m.export_epg_to_xml(epg_filename, addon.getSettingInt('export_days'), report_progress, only_subscribed)
show_notification(addon.getLocalizedString(30313), xbmcgui.NOTIFICATION_INFO)
notify(addon.getLocalizedString(30313))
def to_wishlist(params):
@@ -886,7 +889,7 @@ def router(paramstring):
elif params['action'] == 'create_iptv_settings':
create_iptv_settings()
elif params['action'] == 'export_epg_now':
export_epg_now()
export_epg_now(quiet=params.get('quiet') == '1')
elif params['action'] == 'to_wishlist':
to_wishlist(params)
elif 'iptv' in params['action']: