commit 927195e28d765a3d0f618c78071634f70c2b20da Author: Xosé Date: Thu Nov 12 22:19:10 2026 +0100 Radios de Vigo: Vigo FM dial and UK/US/Portugal news radio for Kodi Co-Authored-By: Claude Opus 5.5 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e7c552a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +tools export-ignore +README.md export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..59e9a63 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Radios de Vigo (service.radios.vigo) + +Kodi add-on that adds the radio stations on Vigo's FM dial and news radio from +the UK, US and Portugal to Kodi's **Radio** section, with logos and groups: + +| Group | Channel numbers | +|---|---| +| Vigo (Spanish and the Portuguese ones from Valença) | the FM frequency, e.g. 100.6 = SER Radio Vigo | +| Noticias · Reino Unido | 201–205 (BBC Radio 4, World Service, 5 Live, Times Radio, LBC News) | +| Noticias · EUA | 301–305 (NPR, WNYC, WBUR, KQED, Bloomberg Radio) | +| Noticias · Portugal | 401–402 (TSF, Observador) + Antena 1 (98.2) | + +Religious stations (Radio María, Radio Vaticano, La Voz de Fátima, COPE, Rádio +Renascença, …) are left out on purpose. + +## How it works + +`service.py` runs when Kodi starts and after each update. It writes an +IPTV Simple instance (`instance-settings-95.xml`, or the next free number) +that reads `resources/radio.m3u` and `resources/logos/` from the add-on +folder, and restarts IPTV Simple (only when nothing is playing) if the +station list changed. On the first run it turns on *Use channel numbers from +backend* + *always*, so the frequencies show as channel numbers and KodiMov+ +keeps Movistar's TV numbers. + +Uninstalling the add-on leaves that instance behind: remove it in +IPTV Simple's settings (or delete the instance file). + +## Changing stations + +1. Edit `tools/stations.txt`. +2. `python3 -I tools/check.py` tests every stream with ffprobe. +3. `python3 -I tools/build.py` writes `resources/radio.m3u` and fetches missing logos. +4. Raise `version` in `addon.xml` (and add a `` line), commit, push to `main`. +5. Publish: dispatch the skin repo's `kodi-repo.yml` workflow; boxes update + from repository.xocko. + +`tools/` and this README aren't in the add-on zip (`.gitattributes` export-ignore). diff --git a/addon.xml b/addon.xml new file mode 100644 index 0000000..287cb7f --- /dev/null +++ b/addon.xml @@ -0,0 +1,26 @@ + + + + + + + + + As radios que se sintonizan en Vigo e informativos de Reino Unido, EUA e Portugal + Las radios que se sintonizan en Vigo e informativos de Reino Unido, EE. UU. y Portugal + The radio stations you can tune in Vigo, plus news radio from the UK, US and Portugal + Engade á sección Radio de Kodi as emisoras do dial FM de Vigo (co número de canal na súa frecuencia, incluídas as portuguesas de Valença) e emisoras de noticias de Reino Unido, Estados Unidos e Portugal. Crea a súa propia instancia de IPTV Simple e mantena ao día cando se actualiza o complemento. + Añade a la sección Radio de Kodi las emisoras del dial FM de Vigo (con el número de canal en su frecuencia, incluidas las portuguesas de Valença) y emisoras de noticias de Reino Unido, Estados Unidos y Portugal. Crea su propia instancia de IPTV Simple y la mantiene al día cuando se actualiza el complemento. + Adds the stations on Vigo's FM dial (numbered by frequency, including the Portuguese ones from Valença) and news stations from the UK, US and Portugal to Kodi's Radio section. It creates its own IPTV Simple instance and keeps it up to date when the add-on updates. + 1.0.0: first version, 38 Vigo stations and 12 news stations. + all + MIT + https://git.anosanube.ovh/xocko/radios-vigo + + resources/icon.png + + + diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 0000000..ca6a067 Binary files /dev/null and b/resources/icon.png differ diff --git a/resources/logos/antena-1.png b/resources/logos/antena-1.png new file mode 100644 index 0000000..f528ced Binary files /dev/null and b/resources/logos/antena-1.png differ diff --git a/resources/logos/antena-2.png b/resources/logos/antena-2.png new file mode 100644 index 0000000..ef422c7 Binary files /dev/null and b/resources/logos/antena-2.png differ diff --git a/resources/logos/antena-3.png b/resources/logos/antena-3.png new file mode 100644 index 0000000..08b65f5 Binary files /dev/null and b/resources/logos/antena-3.png differ diff --git a/resources/logos/bbc-radio-4.png b/resources/logos/bbc-radio-4.png new file mode 100644 index 0000000..c09493f Binary files /dev/null and b/resources/logos/bbc-radio-4.png differ diff --git a/resources/logos/bbc-radio-5-live.png b/resources/logos/bbc-radio-5-live.png new file mode 100644 index 0000000..340e5f1 Binary files /dev/null and b/resources/logos/bbc-radio-5-live.png differ diff --git a/resources/logos/bbc-world-service.png b/resources/logos/bbc-world-service.png new file mode 100644 index 0000000..f965405 Binary files /dev/null and b/resources/logos/bbc-world-service.png differ diff --git a/resources/logos/bloomberg-radio.png b/resources/logos/bloomberg-radio.png new file mode 100644 index 0000000..5828860 Binary files /dev/null and b/resources/logos/bloomberg-radio.png differ diff --git a/resources/logos/cadena-100.png b/resources/logos/cadena-100.png new file mode 100644 index 0000000..bb573d9 Binary files /dev/null and b/resources/logos/cadena-100.png differ diff --git a/resources/logos/cadena-dial.png b/resources/logos/cadena-dial.png new file mode 100644 index 0000000..42a3a2e Binary files /dev/null and b/resources/logos/cadena-dial.png differ diff --git a/resources/logos/esradio-galicia.png b/resources/logos/esradio-galicia.png new file mode 100644 index 0000000..8a1a4e2 Binary files /dev/null and b/resources/logos/esradio-galicia.png differ diff --git a/resources/logos/europa-fm.png b/resources/logos/europa-fm.png new file mode 100644 index 0000000..8bcfcc6 Binary files /dev/null and b/resources/logos/europa-fm.png differ diff --git a/resources/logos/flow-radio.png b/resources/logos/flow-radio.png new file mode 100644 index 0000000..114b722 Binary files /dev/null and b/resources/logos/flow-radio.png differ diff --git a/resources/logos/hit-fm.png b/resources/logos/hit-fm.png new file mode 100644 index 0000000..fa35adb Binary files /dev/null and b/resources/logos/hit-fm.png differ diff --git a/resources/logos/informa-radio.png b/resources/logos/informa-radio.png new file mode 100644 index 0000000..5d9a955 Binary files /dev/null and b/resources/logos/informa-radio.png differ diff --git a/resources/logos/inolvidable-fm.png b/resources/logos/inolvidable-fm.png new file mode 100644 index 0000000..117ba7d Binary files /dev/null and b/resources/logos/inolvidable-fm.png differ diff --git a/resources/logos/kiss-fm.png b/resources/logos/kiss-fm.png new file mode 100644 index 0000000..7fef272 Binary files /dev/null and b/resources/logos/kiss-fm.png differ diff --git a/resources/logos/kqed-san-francisco.png b/resources/logos/kqed-san-francisco.png new file mode 100644 index 0000000..163203a Binary files /dev/null and b/resources/logos/kqed-san-francisco.png differ diff --git a/resources/logos/lbc-news.png b/resources/logos/lbc-news.png new file mode 100644 index 0000000..212c812 Binary files /dev/null and b/resources/logos/lbc-news.png differ diff --git a/resources/logos/los40-classic.png b/resources/logos/los40-classic.png new file mode 100644 index 0000000..5822408 Binary files /dev/null and b/resources/logos/los40-classic.png differ diff --git a/resources/logos/los40-dance.png b/resources/logos/los40-dance.png new file mode 100644 index 0000000..bfe65e2 Binary files /dev/null and b/resources/logos/los40-dance.png differ diff --git a/resources/logos/los40-urban.png b/resources/logos/los40-urban.png new file mode 100644 index 0000000..9374c0c Binary files /dev/null and b/resources/logos/los40-urban.png differ diff --git a/resources/logos/los40-vigo.png b/resources/logos/los40-vigo.png new file mode 100644 index 0000000..9374c0c Binary files /dev/null and b/resources/logos/los40-vigo.png differ diff --git a/resources/logos/motiva-vigo.png b/resources/logos/motiva-vigo.png new file mode 100644 index 0000000..9ae5adb Binary files /dev/null and b/resources/logos/motiva-vigo.png differ diff --git a/resources/logos/npr.png b/resources/logos/npr.png new file mode 100644 index 0000000..c5bfd90 Binary files /dev/null and b/resources/logos/npr.png differ diff --git a/resources/logos/onda-cero-pontevedra.png b/resources/logos/onda-cero-pontevedra.png new file mode 100644 index 0000000..7a1ca31 Binary files /dev/null and b/resources/logos/onda-cero-pontevedra.png differ diff --git a/resources/logos/onda-cero-vigo.png b/resources/logos/onda-cero-vigo.png new file mode 100644 index 0000000..7a1ca31 Binary files /dev/null and b/resources/logos/onda-cero-vigo.png differ diff --git a/resources/logos/radio-8.png b/resources/logos/radio-8.png new file mode 100644 index 0000000..1241176 Binary files /dev/null and b/resources/logos/radio-8.png differ diff --git a/resources/logos/radio-comercial.png b/resources/logos/radio-comercial.png new file mode 100644 index 0000000..145a74a Binary files /dev/null and b/resources/logos/radio-comercial.png differ diff --git a/resources/logos/radio-galega-musica.png b/resources/logos/radio-galega-musica.png new file mode 100644 index 0000000..d33bc53 Binary files /dev/null and b/resources/logos/radio-galega-musica.png differ diff --git a/resources/logos/radio-galega.png b/resources/logos/radio-galega.png new file mode 100644 index 0000000..1391570 Binary files /dev/null and b/resources/logos/radio-galega.png differ diff --git a/resources/logos/radio-lider.png b/resources/logos/radio-lider.png new file mode 100644 index 0000000..041701b Binary files /dev/null and b/resources/logos/radio-lider.png differ diff --git a/resources/logos/radio-marca-vigo.png b/resources/logos/radio-marca-vigo.png new file mode 100644 index 0000000..179bdba Binary files /dev/null and b/resources/logos/radio-marca-vigo.png differ diff --git a/resources/logos/radio-observador.png b/resources/logos/radio-observador.png new file mode 100644 index 0000000..4eab4b2 Binary files /dev/null and b/resources/logos/radio-observador.png differ diff --git a/resources/logos/radio-piratona.png b/resources/logos/radio-piratona.png new file mode 100644 index 0000000..803a93a Binary files /dev/null and b/resources/logos/radio-piratona.png differ diff --git a/resources/logos/radio-voz-vigo.png b/resources/logos/radio-voz-vigo.png new file mode 100644 index 0000000..a124d71 Binary files /dev/null and b/resources/logos/radio-voz-vigo.png differ diff --git a/resources/logos/rfm.png b/resources/logos/rfm.png new file mode 100644 index 0000000..2fb350b Binary files /dev/null and b/resources/logos/rfm.png differ diff --git a/resources/logos/rne-radio-3.png b/resources/logos/rne-radio-3.png new file mode 100644 index 0000000..c9ac6e5 Binary files /dev/null and b/resources/logos/rne-radio-3.png differ diff --git a/resources/logos/rne-radio-5-galicia.png b/resources/logos/rne-radio-5-galicia.png new file mode 100644 index 0000000..40d1460 Binary files /dev/null and b/resources/logos/rne-radio-5-galicia.png differ diff --git a/resources/logos/rne-radio-clasica.png b/resources/logos/rne-radio-clasica.png new file mode 100644 index 0000000..5ba17bc Binary files /dev/null and b/resources/logos/rne-radio-clasica.png differ diff --git a/resources/logos/rne-radio-nacional-galicia.png b/resources/logos/rne-radio-nacional-galicia.png new file mode 100644 index 0000000..57d3da1 Binary files /dev/null and b/resources/logos/rne-radio-nacional-galicia.png differ diff --git a/resources/logos/rock-fm.png b/resources/logos/rock-fm.png new file mode 100644 index 0000000..794938d Binary files /dev/null and b/resources/logos/rock-fm.png differ diff --git a/resources/logos/ser-mas-radio-vigo.png b/resources/logos/ser-mas-radio-vigo.png new file mode 100644 index 0000000..91ab343 Binary files /dev/null and b/resources/logos/ser-mas-radio-vigo.png differ diff --git a/resources/logos/ser-radio-pontevedra.png b/resources/logos/ser-radio-pontevedra.png new file mode 100644 index 0000000..91ab343 Binary files /dev/null and b/resources/logos/ser-radio-pontevedra.png differ diff --git a/resources/logos/ser-radio-vigo.png b/resources/logos/ser-radio-vigo.png new file mode 100644 index 0000000..55d25cb Binary files /dev/null and b/resources/logos/ser-radio-vigo.png differ diff --git a/resources/logos/si-radio.png b/resources/logos/si-radio.png new file mode 100644 index 0000000..ce1f002 Binary files /dev/null and b/resources/logos/si-radio.png differ diff --git a/resources/logos/times-radio.png b/resources/logos/times-radio.png new file mode 100644 index 0000000..26badae Binary files /dev/null and b/resources/logos/times-radio.png differ diff --git a/resources/logos/tsf-radio-noticias.png b/resources/logos/tsf-radio-noticias.png new file mode 100644 index 0000000..c2cea5a Binary files /dev/null and b/resources/logos/tsf-radio-noticias.png differ diff --git a/resources/logos/ushuaia-radio.png b/resources/logos/ushuaia-radio.png new file mode 100644 index 0000000..68dd33c Binary files /dev/null and b/resources/logos/ushuaia-radio.png differ diff --git a/resources/logos/wbur-boston.png b/resources/logos/wbur-boston.png new file mode 100644 index 0000000..479444b Binary files /dev/null and b/resources/logos/wbur-boston.png differ diff --git a/resources/logos/wnyc-new-york.png b/resources/logos/wnyc-new-york.png new file mode 100644 index 0000000..29a94d3 Binary files /dev/null and b/resources/logos/wnyc-new-york.png differ diff --git a/resources/radio.m3u b/resources/radio.m3u new file mode 100644 index 0000000..004739b --- /dev/null +++ b/resources/radio.m3u @@ -0,0 +1,101 @@ +#EXTM3U +#EXTINF:-1 tvg-id="radio-8" tvg-chno="88.1" radio="true" group-title="Vigo" tvg-logo="radio-8.png",Radio 8 +https://relay.stream.enacast-cloud.com:30271/radio8HD.mp3 +#EXTINF:-1 tvg-id="rne-radio-clasica" tvg-chno="88.3" radio="true" group-title="Vigo" tvg-logo="rne-radio-clasica.png",RNE Radio Clásica +https://dispatcher.rndfnk.com/crtve/rnerc/main/mp3/high +#EXTINF:-1 tvg-id="europa-fm" tvg-chno="88.7" radio="true" group-title="Vigo" tvg-logo="europa-fm.png",Europa FM +https://atres-live.europafm.com/live/europafm/master.m3u8 +#EXTINF:-1 tvg-id="antena-2" tvg-chno="89.6" radio="true" group-title="Vigo" tvg-logo="antena-2.png",Antena 2 +https://streaming-live.rtp.pt/liveradio/antena280a/playlist.m3u8 +#EXTINF:-1 tvg-id="ushuaia-radio" tvg-chno="89.8" radio="true" group-title="Vigo" tvg-logo="ushuaia-radio.png",Ushuaïa Radio +https://s4.radio.co/sc6f29e098/listen +#EXTINF:-1 tvg-id="rne-radio-nacional-galicia" tvg-chno="90.1" radio="true" group-title="Vigo" tvg-logo="rne-radio-nacional-galicia.png",RNE Radio Nacional Galicia +https://dispatcher.rndfnk.com/crtve/rne1/gal/mp3/high +#EXTINF:-1 tvg-id="hit-fm" tvg-chno="90.5" radio="true" group-title="Vigo" tvg-logo="hit-fm.png",Hit FM +https://bbhitfm.kissfmradio.cires21.com/bbhitfm.mp3 +#EXTINF:-1 tvg-id="radio-galega-musica" tvg-chno="90.8" radio="true" group-title="Vigo" tvg-logo="radio-galega-musica.png",Radio Galega Música +https://crtvg-radiogalega-musica-hls.flumotion.cloud/playlist.m3u8 +#EXTINF:-1 tvg-id="radio-lider" tvg-chno="91.1" radio="true" group-title="Vigo" tvg-logo="radio-lider.png",Radio Líder +http://server9.20comunicacion.com:8200/stream +#EXTINF:-1 tvg-id="cadena-dial" tvg-chno="91.4" radio="true" group-title="Vigo" tvg-logo="cadena-dial.png",Cadena Dial +https://playerservices.streamtheworld.com/api/livestream-redirect/CADENADIAL.mp3 +#EXTINF:-1 tvg-id="los40-dance" tvg-chno="92.7" radio="true" group-title="Vigo" tvg-logo="los40-dance.png",LOS40 Dance +https://playerservices.streamtheworld.com/api/livestream-redirect/LOS40_DANCE.mp3 +#EXTINF:-1 tvg-id="ser-mas-radio-vigo" tvg-chno="93.7" radio="true" group-title="Vigo" tvg-logo="ser-mas-radio-vigo.png",SER+ Radio Vigo +https://playerservices.streamtheworld.com/api/livestream-redirect/SER_ASO_MAS_VIGO.mp3 +#EXTINF:-1 tvg-id="kiss-fm" tvg-chno="94.0" radio="true" group-title="Vigo" tvg-logo="kiss-fm.png",Kiss FM +https://kissfm.kissfmradio.cires21.com/kissfm.mp3 +#EXTINF:-1 tvg-id="rne-radio-3" tvg-chno="94.6" radio="true" group-title="Vigo" tvg-logo="rne-radio-3.png",RNE Radio 3 +https://dispatcher.rndfnk.com/crtve/rner3/main/mp3/high +#EXTINF:-1 tvg-id="flow-radio" tvg-chno="94.8" radio="true" group-title="Vigo" tvg-logo="flow-radio.png",Flow Radio +https://flow.urbanrevolution.es:8445/flow +#EXTINF:-1 tvg-id="radio-galega" tvg-chno="95.1" radio="true" group-title="Vigo" tvg-logo="radio-galega.png",Radio Galega +https://crtvg-radiogalega-hls.flumotion.cloud/playlist.m3u8 +#EXTINF:-1 tvg-id="rfm" tvg-chno="95.4" radio="true" group-title="Vigo" tvg-logo="rfm.png",RFM +https://playerservices.streamtheworld.com/api/livestream-redirect/RFMAAC.aac +#EXTINF:-1 tvg-id="rne-radio-5-galicia" tvg-chno="96.0" radio="true" group-title="Vigo" tvg-logo="rne-radio-5-galicia.png",RNE Radio 5 Galicia +https://dispatcher.rndfnk.com/crtve/rne5/vig/mp3/high +#EXTINF:-1 tvg-id="cadena-100" tvg-chno="96.5" radio="true" group-title="Vigo" tvg-logo="cadena-100.png",Cadena 100 +https://cadena100-streamers-mp3.flumotion.com/cope/cadena100.mp3 +#EXTINF:-1 tvg-id="antena-1" tvg-chno="98.2" radio="true" group-title="Vigo;Noticias · Portugal" tvg-logo="antena-1.png",Antena 1 +https://streaming-live.rtp.pt/liveradio/antena180a/playlist.m3u8 +#EXTINF:-1 tvg-id="radio-marca-vigo" tvg-chno="98.3" radio="true" group-title="Vigo" tvg-logo="radio-marca-vigo.png",Radio Marca Vigo +https://streaming.enacast.com/radiomarcavigoHD.mp3 +#EXTINF:-1 tvg-id="ser-radio-pontevedra" tvg-chno="98.7" radio="true" group-title="Vigo" tvg-logo="ser-radio-pontevedra.png",SER Radio Pontevedra +https://playerservices.streamtheworld.com/api/livestream-redirect/SER_ASO_PONTEVEDRA.mp3 +#EXTINF:-1 tvg-id="radio-comercial" tvg-chno="99.0" radio="true" group-title="Vigo" tvg-logo="radio-comercial.png",Rádio Comercial +https://stream-icy.bauermedia.pt/comercial.mp3 +#EXTINF:-1 tvg-id="motiva-vigo" tvg-chno="99.1" radio="true" group-title="Vigo" tvg-logo="motiva-vigo.png",motiva Vigo +https://stream.guadigo.es/listen/motiva/motivavigo.mp3 +#EXTINF:-1 tvg-id="los40-vigo" tvg-chno="99.4" radio="true" group-title="Vigo" tvg-logo="los40-vigo.png",LOS40 Vigo +https://playerservices.streamtheworld.com/api/livestream-redirect/LOS40_ASO_VIGO.mp3 +#EXTINF:-1 tvg-id="informa-radio" tvg-chno="99.7" radio="true" group-title="Vigo" tvg-logo="informa-radio.png",Informa Radio +https://24x7.red:42045/informa +#EXTINF:-1 tvg-id="ser-radio-vigo" tvg-chno="100.6" radio="true" group-title="Vigo" tvg-logo="ser-radio-vigo.png",SER Radio Vigo +https://playerservices.streamtheworld.com/api/livestream-redirect/SER_ASO_VIGO.mp3 +#EXTINF:-1 tvg-id="los40-classic" tvg-chno="101.2" radio="true" group-title="Vigo" tvg-logo="los40-classic.png",LOS40 Classic +https://playerservices.streamtheworld.com/api/livestream-redirect/LOS40_CLASSIC.mp3 +#EXTINF:-1 tvg-id="esradio-galicia" tvg-chno="101.9" radio="true" group-title="Vigo" tvg-logo="esradio-galicia.png",esRadio Galicia +https://libertaddigital-radio-live2.flumotion.com/libertaddigital/ld-live2-low.mp3 +#EXTINF:-1 tvg-id="onda-cero-pontevedra" tvg-chno="103.6" radio="true" group-title="Vigo" tvg-logo="onda-cero-pontevedra.png",Onda Cero Pontevedra +https://radio-atres-live.ondacero.es/api/livestream-redirect/OC_PONTEVEDRAAAC.m3u8 +#EXTINF:-1 tvg-id="radio-voz-vigo" tvg-chno="103.8" radio="true" group-title="Vigo" tvg-logo="radio-voz-vigo.png",Radio Voz Vigo +https://live.radiovoz.es/mp3/stream_vigo.mp3 +#EXTINF:-1 tvg-id="antena-3" tvg-chno="104.0" radio="true" group-title="Vigo" tvg-logo="antena-3.png",Antena 3 +https://streaming-live.rtp.pt/liveradio/antena380a/playlist.m3u8 +#EXTINF:-1 tvg-id="los40-urban" tvg-chno="104.9" radio="true" group-title="Vigo" tvg-logo="los40-urban.png",LOS40 Urban +https://playerservices.streamtheworld.com/api/livestream-redirect/LOS40_URBAN.mp3 +#EXTINF:-1 tvg-id="onda-cero-vigo" tvg-chno="105.4" radio="true" group-title="Vigo" tvg-logo="onda-cero-vigo.png",Onda Cero Vigo +https://radio-atres-live.ondacero.es/api/livestream-redirect/OC_VIGOAAC.m3u8 +#EXTINF:-1 tvg-id="inolvidable-fm" tvg-chno="105.8" radio="true" group-title="Vigo" tvg-logo="inolvidable-fm.png",Inolvidable FM +https://uk1.streamingpulse.com/ssl/inolvidablefm +#EXTINF:-1 tvg-id="radio-piratona" tvg-chno="106.1" radio="true" group-title="Vigo" tvg-logo="radio-piratona.png",Radio Piratona +https://zeppelin.streampunk.cc/_stream/radiopiratona.ogg +#EXTINF:-1 tvg-id="rock-fm" tvg-chno="106.7" radio="true" group-title="Vigo" tvg-logo="rock-fm.png",Rock FM +https://rockfm-cope-rrcast.flumotion.com/cope/rockfm-low.mp3 +#EXTINF:-1 tvg-id="si-radio" tvg-chno="107.7" radio="true" group-title="Vigo" tvg-logo="si-radio.png",Si Radio +http://82.223.29.65:8080/vcbm_siradio +#EXTINF:-1 tvg-id="bbc-radio-4" tvg-chno="201" radio="true" group-title="Noticias · Reino Unido" tvg-logo="bbc-radio-4.png",BBC Radio 4 +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 +#EXTINF:-1 tvg-id="bbc-world-service" tvg-chno="202" radio="true" group-title="Noticias · Reino Unido" tvg-logo="bbc-world-service.png",BBC World Service +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 +#EXTINF:-1 tvg-id="bbc-radio-5-live" tvg-chno="203" radio="true" group-title="Noticias · Reino Unido" tvg-logo="bbc-radio-5-live.png",BBC Radio 5 Live +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 +#EXTINF:-1 tvg-id="times-radio" tvg-chno="204" radio="true" group-title="Noticias · Reino Unido" tvg-logo="times-radio.png",Times Radio +https://timesradio.wireless.radio/stream +#EXTINF:-1 tvg-id="lbc-news" tvg-chno="205" radio="true" group-title="Noticias · Reino Unido" tvg-logo="lbc-news.png",LBC News +https://media-ssl.musicradio.com/LBCNewsUK +#EXTINF:-1 tvg-id="npr" tvg-chno="301" radio="true" group-title="Noticias · EUA" tvg-logo="npr.png",NPR +https://npr-ice.streamguys1.com/live.mp3 +#EXTINF:-1 tvg-id="wnyc-new-york" tvg-chno="302" radio="true" group-title="Noticias · EUA" tvg-logo="wnyc-new-york.png",WNYC New York +https://fm939.wnyc.org/wnycfm-web +#EXTINF:-1 tvg-id="wbur-boston" tvg-chno="303" radio="true" group-title="Noticias · EUA" tvg-logo="wbur-boston.png",WBUR Boston +https://icecast-stream.wbur.org/wbur_www +#EXTINF:-1 tvg-id="kqed-san-francisco" tvg-chno="304" radio="true" group-title="Noticias · EUA" tvg-logo="kqed-san-francisco.png",KQED San Francisco +https://hls.kqed.org/hls/kqed_app/playlist.m3u8 +#EXTINF:-1 tvg-id="bloomberg-radio" tvg-chno="305" radio="true" group-title="Noticias · EUA" tvg-logo="bloomberg-radio.png",Bloomberg Radio +https://playerservices.streamtheworld.com/api/livestream-redirect/WBBRAMAAC.aac +#EXTINF:-1 tvg-id="tsf-radio-noticias" tvg-chno="401" radio="true" group-title="Noticias · Portugal" tvg-logo="tsf-radio-noticias.png",TSF Rádio Notícias +https://directo.tsf.pt/tsfdirecto.mp3 +#EXTINF:-1 tvg-id="radio-observador" tvg-chno="402" radio="true" group-title="Noticias · Portugal" tvg-logo="radio-observador.png",Rádio Observador +https://playerservices.streamtheworld.com/api/livestream-redirect/OBSERVADORAAC.aac diff --git a/service.py b/service.py new file mode 100644 index 0000000..60c6a51 --- /dev/null +++ b/service.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +"""Keeps an IPTV Simple instance that plays this add-on's radio.m3u. + +Runs when Kodi starts and again after every add-on update. Writes the +instance settings, then restarts IPTV Simple (once nothing is playing) if the +settings or the station list changed since the last run. +""" +import glob +import hashlib +import io +import json +import os + +import xbmc +import xbmcaddon +import xbmcvfs + +ADDON = xbmcaddon.Addon() +ADDON_ID = ADDON.getAddonInfo('id') +HOME = 'special://home/addons/{}/resources/'.format(ADDON_ID) +FIRST_INSTANCE = 95 # KodiMov+ uses 91 (VAIO) and 93 (.49) + +SETTINGS = ''' + Radios de Vigo + true + 0 + {home}radio.m3u + + true + 1 + false + 0 + 0 + + + 0 + {home}logos/ + false + 0 + true + false + +'''.format(home=HOME) + + +def log(msg): + xbmc.log('[{}] {}'.format(ADDON_ID, msg), xbmc.LOGINFO) + + +def rpc(method, **params): + return json.loads(xbmc.executeJSONRPC(json.dumps( + {'jsonrpc': '2.0', 'id': 1, 'method': method, 'params': params}))) + + +def read(path): + try: + with io.open(path, 'r', encoding='utf-8') as f: + return f.read() + except (IOError, OSError): + return None + + +def write(path, content): + tmp = path + '.tmp' + with io.open(tmp, 'w', encoding='utf-8', newline='') as f: + f.write(content) + os.replace(tmp, path) + + +def instance_file(pvr_dir): + """Our instance file: the one that already reads radio.m3u, else the + first free number from FIRST_INSTANCE on.""" + for path in glob.glob(os.path.join(pvr_dir, 'instance-settings-*.xml')): + if HOME + 'radio.m3u' in (read(path) or ''): + return path + n = FIRST_INSTANCE + while os.path.exists(os.path.join(pvr_dir, 'instance-settings-{}.xml'.format(n))): + n += 1 + return os.path.join(pvr_dir, 'instance-settings-{}.xml'.format(n)) + + +def restart_iptv_simple(): + for enabled in (False, True): + rpc('Addons.SetAddonEnabled', addonid='pvr.iptvsimple', enabled=enabled) + xbmc.sleep(3000) + + +def main(): + monitor, player = xbmc.Monitor(), xbmc.Player() + profile = xbmcvfs.translatePath(ADDON.getAddonInfo('profile')) + pvr_dir = xbmcvfs.translatePath('special://profile/addon_data/pvr.iptvsimple/') + for d in (profile, pvr_dir): + if not os.path.isdir(d): + os.makedirs(d) + + path = instance_file(pvr_dir) + if read(path) != SETTINGS: + write(path, SETTINGS) + log('wrote ' + path) + + numbers_set = os.path.join(profile, 'numbers-set') + if not os.path.exists(numbers_set): + # Show the stations' own numbers (Vigo ones are their FM frequency). + # With more than one IPTV Simple instance Kodi ignores them unless + # "always" is on too; that also keeps KodiMov+'s TV numbers. + for setting in ('pvrmanager.usebackendchannelnumbers', 'pvrmanager.usebackendchannelnumbersalways'): + rpc('Settings.SetSettingValue', setting=setting, value=True) + write(numbers_set, '') + log('turned on backend channel numbers') + + m3u = read(xbmcvfs.translatePath(HOME + 'radio.m3u')) or '' + state = hashlib.sha1((SETTINGS + m3u).encode('utf-8')).hexdigest() + if read(os.path.join(profile, 'state')) == state: + return + + while player.isPlaying(): # restarting IPTV Simple stops the radio or TV playing + if monitor.waitForAbort(10): + return + if monitor.waitForAbort(5): + return + log('station list or settings changed, restarting IPTV Simple') + restart_iptv_simple() + write(os.path.join(profile, 'state'), state) + + +if __name__ == '__main__': + main() diff --git a/tools/build.py b/tools/build.py new file mode 100644 index 0000000..37826a4 --- /dev/null +++ b/tools/build.py @@ -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() diff --git a/tools/check.py b/tools/check.py new file mode 100644 index 0000000..d438183 --- /dev/null +++ b/tools/check.py @@ -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() diff --git a/tools/stations.txt b/tools/stations.txt new file mode 100644 index 0000000..efec2f4 --- /dev/null +++ b/tools/stations.txt @@ -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:" 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