Radios de Vigo: Vigo FM dial and UK/US/Portugal news radio for Kodi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
927195e28d
60 files changed
+488
No files matched your search
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Writes resources/radio.m3u from tools/stations.txt and downloads the logos
|
||||
that are missing from resources/logos (512 px PNG, needs macOS sips).
|
||||
|
||||
Run with: python3 -I tools/build.py [--logos] (--logos downloads every logo again)
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import unicodedata
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
LOGOS = os.path.join(ROOT, 'resources', 'logos')
|
||||
UA = {'User-Agent': 'Mozilla/5.0'}
|
||||
|
||||
|
||||
def get(url):
|
||||
return urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=20).read()
|
||||
|
||||
|
||||
def channel_id(name):
|
||||
name = unicodedata.normalize('NFKD', name.replace('+', ' mas')).encode('ascii', 'ignore').decode()
|
||||
return re.sub(r'[^a-z0-9]+', '-', name.lower()).strip('-')
|
||||
|
||||
|
||||
def stations():
|
||||
with open(os.path.join(ROOT, 'tools', 'stations.txt'), encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
yield line.split('|', 4)
|
||||
|
||||
|
||||
def logo_url(source):
|
||||
if source.startswith('rb:'):
|
||||
query = urllib.parse.urlencode({'name': source[3:], 'limit': 5, 'order': 'clickcount', 'reverse': 'true'})
|
||||
for s in json.loads(get('https://de1.api.radio-browser.info/json/stations/search?' + query)):
|
||||
if s['favicon']:
|
||||
return s['favicon']
|
||||
return None
|
||||
country, slug = source.split('.', 1)
|
||||
page = get('https://onlineradiobox.com/{}/{}/'.format(country, slug)).decode('utf-8', 'replace')
|
||||
m = re.search(r'og:image" content="https://cdn\.onlineradiobox\.com/img/fblogo/([^"]+)"', page)
|
||||
return 'https://cdn.onlineradiobox.com/img/l/' + m.group(1) if m else None # the logo, not the share banner
|
||||
|
||||
|
||||
def fetch_logo(source, dest):
|
||||
url = logo_url(source)
|
||||
if not url:
|
||||
return False
|
||||
tmp = dest + '.tmp'
|
||||
with open(tmp, 'wb') as f:
|
||||
f.write(get(url))
|
||||
try:
|
||||
subprocess.run(['sips', '-s', 'format', 'png', '-Z', '512', tmp, '--out', dest], check=True, capture_output=True)
|
||||
finally:
|
||||
os.remove(tmp)
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
refresh = '--logos' in sys.argv
|
||||
os.makedirs(LOGOS, exist_ok=True)
|
||||
lines, used = ['#EXTM3U'], set()
|
||||
for chno, name, groups, source, url in stations():
|
||||
cid = channel_id(name)
|
||||
logo = cid + '.png'
|
||||
path = os.path.join(LOGOS, logo)
|
||||
if source != '-' and (refresh or not os.path.exists(path)):
|
||||
try:
|
||||
if not fetch_logo(source, path):
|
||||
print('no logo found:', name)
|
||||
except Exception as e:
|
||||
print('logo failed:', name, e)
|
||||
attrs = 'tvg-id="{}" tvg-chno="{}" radio="true" group-title="{}"'.format(cid, chno, groups)
|
||||
if os.path.exists(path):
|
||||
attrs += ' tvg-logo="{}"'.format(logo)
|
||||
used.add(logo)
|
||||
else:
|
||||
print('no logo:', name)
|
||||
lines += ['#EXTINF:-1 {},{}'.format(attrs, name), url]
|
||||
for f in sorted(set(os.listdir(LOGOS)) - used):
|
||||
print('unused logo removed:', f)
|
||||
os.remove(os.path.join(LOGOS, f))
|
||||
with open(os.path.join(ROOT, 'resources', 'radio.m3u'), 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(lines) + '\n')
|
||||
print(len(lines) // 2, 'stations')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests every stream in tools/stations.txt with ffprobe and lists the ones
|
||||
that don't play. Exit status 1 if any failed.
|
||||
|
||||
Run with: python3 -I tools/check.py
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from build import stations # noqa: E402
|
||||
|
||||
|
||||
def probe(station):
|
||||
chno, name, _, _, url = station
|
||||
cmd = ['ffprobe', '-v', 'error', '-rw_timeout', '15000000', '-user_agent', 'Kodi/21.2',
|
||||
'-show_entries', 'stream=codec_name,bit_rate', '-of', 'csv=p=0', url]
|
||||
try:
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
out = (r.stdout + r.stderr).strip().splitlines()
|
||||
except subprocess.TimeoutExpired:
|
||||
out = ['timeout']
|
||||
ok = bool(out) and out[0].split(',')[0] in ('aac', 'mp3', 'vorbis', 'opus', 'flac')
|
||||
return ok, '{:>6} {:<28} {}'.format(chno, name, out[0] if out else 'no output')
|
||||
|
||||
|
||||
def main():
|
||||
with ThreadPoolExecutor(16) as pool:
|
||||
results = list(pool.map(probe, stations()))
|
||||
failed = [line for ok, line in results if not ok]
|
||||
for ok, line in results:
|
||||
print(('ok ' if ok else 'FAIL ') + line)
|
||||
print('{} of {} streams play'.format(len(results) - len(failed), len(results)))
|
||||
sys.exit(1 if failed else 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,57 @@
|
||||
# Channel list for service.radios.vigo. One station per line:
|
||||
# channel number|name|groups (; between them)|logo source|stream URL
|
||||
# Vigo stations use their FM frequency as channel number. Logo source is an
|
||||
# onlineradiobox.com id (cc.slug), "rb:<name>" for radio-browser.info, or "-"
|
||||
# to keep the logo already in resources/logos. After editing run
|
||||
# tools/check.py (tests every stream) and tools/build.py (writes
|
||||
# resources/radio.m3u and missing logos), then raise the version in addon.xml.
|
||||
88.1|Radio 8|Vigo|es.turadio|https://relay.stream.enacast-cloud.com:30271/radio8HD.mp3
|
||||
88.3|RNE Radio Clásica|Vigo|-|https://dispatcher.rndfnk.com/crtve/rnerc/main/mp3/high
|
||||
88.7|Europa FM|Vigo|es.europa|https://atres-live.europafm.com/live/europafm/master.m3u8
|
||||
89.6|Antena 2|Vigo|pt.rtpantena2|https://streaming-live.rtp.pt/liveradio/antena280a/playlist.m3u8
|
||||
89.8|Ushuaïa Radio|Vigo|es.ushuaia|https://s4.radio.co/sc6f29e098/listen
|
||||
90.1|RNE Radio Nacional Galicia|Vigo|es.rneradionacional|https://dispatcher.rndfnk.com/crtve/rne1/gal/mp3/high
|
||||
90.5|Hit FM|Vigo|es.hitfm899|https://bbhitfm.kissfmradio.cires21.com/bbhitfm.mp3
|
||||
90.8|Radio Galega Música|Vigo|es.galegamusica|https://crtvg-radiogalega-musica-hls.flumotion.cloud/playlist.m3u8
|
||||
91.1|Radio Líder|Vigo|es.lider|http://server9.20comunicacion.com:8200/stream
|
||||
91.4|Cadena Dial|Vigo|es.cadenadial|https://playerservices.streamtheworld.com/api/livestream-redirect/CADENADIAL.mp3
|
||||
92.7|LOS40 Dance|Vigo|es.maximafm|https://playerservices.streamtheworld.com/api/livestream-redirect/LOS40_DANCE.mp3
|
||||
93.7|SER+ Radio Vigo|Vigo|es.cadenaser|https://playerservices.streamtheworld.com/api/livestream-redirect/SER_ASO_MAS_VIGO.mp3
|
||||
94.0|Kiss FM|Vigo|es.kissfm1027|https://kissfm.kissfmradio.cires21.com/kissfm.mp3
|
||||
94.6|RNE Radio 3|Vigo|es.rneradio3|https://dispatcher.rndfnk.com/crtve/rner3/main/mp3/high
|
||||
94.8|Flow Radio|Vigo|es.flow|https://flow.urbanrevolution.es:8445/flow
|
||||
95.1|Radio Galega|Vigo|es.galega|https://crtvg-radiogalega-hls.flumotion.cloud/playlist.m3u8
|
||||
95.4|RFM|Vigo|pt.rfmlisbon|https://playerservices.streamtheworld.com/api/livestream-redirect/RFMAAC.aac
|
||||
96.0|RNE Radio 5 Galicia|Vigo|es.rner5|https://dispatcher.rndfnk.com/crtve/rne5/vig/mp3/high
|
||||
96.5|Cadena 100|Vigo|es.cadena100|https://cadena100-streamers-mp3.flumotion.com/cope/cadena100.mp3
|
||||
98.2|Antena 1|Vigo;Noticias · Portugal|pt.rtpantena1|https://streaming-live.rtp.pt/liveradio/antena180a/playlist.m3u8
|
||||
98.3|Radio Marca Vigo|Vigo|es.marca|https://streaming.enacast.com/radiomarcavigoHD.mp3
|
||||
98.7|SER Radio Pontevedra|Vigo|es.cadenaser|https://playerservices.streamtheworld.com/api/livestream-redirect/SER_ASO_PONTEVEDRA.mp3
|
||||
99.0|Rádio Comercial|Vigo|pt.comercial|https://stream-icy.bauermedia.pt/comercial.mp3
|
||||
99.1|motiva Vigo|Vigo|es.motivavigo|https://stream.guadigo.es/listen/motiva/motivavigo.mp3
|
||||
99.4|LOS40 Vigo|Vigo|es.los40vigo|https://playerservices.streamtheworld.com/api/livestream-redirect/LOS40_ASO_VIGO.mp3
|
||||
99.7|Informa Radio|Vigo|es.informa|https://24x7.red:42045/informa
|
||||
100.6|SER Radio Vigo|Vigo|es.radiovigo|https://playerservices.streamtheworld.com/api/livestream-redirect/SER_ASO_VIGO.mp3
|
||||
101.2|LOS40 Classic|Vigo|es.m80|https://playerservices.streamtheworld.com/api/livestream-redirect/LOS40_CLASSIC.mp3
|
||||
101.9|esRadio Galicia|Vigo|-|https://libertaddigital-radio-live2.flumotion.com/libertaddigital/ld-live2-low.mp3
|
||||
103.6|Onda Cero Pontevedra|Vigo|es.ondacero1036|https://radio-atres-live.ondacero.es/api/livestream-redirect/OC_PONTEVEDRAAAC.m3u8
|
||||
103.8|Radio Voz Vigo|Vigo|es.radiovoz|https://live.radiovoz.es/mp3/stream_vigo.mp3
|
||||
104.0|Antena 3|Vigo|pt.rtpantena3|https://streaming-live.rtp.pt/liveradio/antena380a/playlist.m3u8
|
||||
104.9|LOS40 Urban|Vigo|es.los40|https://playerservices.streamtheworld.com/api/livestream-redirect/LOS40_URBAN.mp3
|
||||
105.4|Onda Cero Vigo|Vigo|es.ondacero1054|https://radio-atres-live.ondacero.es/api/livestream-redirect/OC_VIGOAAC.m3u8
|
||||
105.8|Inolvidable FM|Vigo|es.inolvidable|https://uk1.streamingpulse.com/ssl/inolvidablefm
|
||||
106.1|Radio Piratona|Vigo|es.piratona|https://zeppelin.streampunk.cc/_stream/radiopiratona.ogg
|
||||
106.7|Rock FM|Vigo|es.rockfm|https://rockfm-cope-rrcast.flumotion.com/cope/rockfm-low.mp3
|
||||
107.7|Si Radio|Vigo|es.siradio|http://82.223.29.65:8080/vcbm_siradio
|
||||
201|BBC Radio 4|Noticias · Reino Unido|uk.bbcradio4|http://as-hls-ww-live.akamaized.net/pool_55057080/live/ww/bbc_radio_fourfm/bbc_radio_fourfm.isml/bbc_radio_fourfm-audio%3d128000.norewind.m3u8
|
||||
202|BBC World Service|Noticias · Reino Unido|uk.bbcworld|http://as-hls-ww-live.akamaized.net/pool_87948813/live/ww/bbc_world_service/bbc_world_service.isml/bbc_world_service-audio%3d128000.norewind.m3u8
|
||||
203|BBC Radio 5 Live|Noticias · Reino Unido|uk.bbcradio5|http://as-hls-ww-live.akamaized.net/pool_89021708/live/ww/bbc_radio_five_live/bbc_radio_five_live.isml/bbc_radio_five_live-audio%3d128000.norewind.m3u8
|
||||
204|Times Radio|Noticias · Reino Unido|uk.times|https://timesradio.wireless.radio/stream
|
||||
205|LBC News|Noticias · Reino Unido|uk.lbcnews|https://media-ssl.musicradio.com/LBCNewsUK
|
||||
301|NPR|Noticias · EUA|us.npr|https://npr-ice.streamguys1.com/live.mp3
|
||||
302|WNYC New York|Noticias · EUA|us.wnycfm|https://fm939.wnyc.org/wnycfm-web
|
||||
303|WBUR Boston|Noticias · EUA|us.wbur|https://icecast-stream.wbur.org/wbur_www
|
||||
304|KQED San Francisco|Noticias · EUA|us.kqedfm|https://hls.kqed.org/hls/kqed_app/playlist.m3u8
|
||||
305|Bloomberg Radio|Noticias · EUA|us.wbbr|https://playerservices.streamtheworld.com/api/livestream-redirect/WBBRAMAAC.aac
|
||||
401|TSF Rádio Notícias|Noticias · Portugal|pt.tsfnoticias|https://directo.tsf.pt/tsfdirecto.mp3
|
||||
402|Rádio Observador|Noticias · Portugal|pt.observador|https://playerservices.streamtheworld.com/api/livestream-redirect/OBSERVADORAAC.aac
|
||||
Reference in new issue
Block a user