diff --git a/script.radioparadise/CHANGELOG.md b/script.radioparadise/CHANGELOG.md index 325139a816..dc70373d60 100644 --- a/script.radioparadise/CHANGELOG.md +++ b/script.radioparadise/CHANGELOG.md @@ -1,3 +1,11 @@ +## v2.1.1 + +- Improve metadata handling + +## v2.1.0 + +- Add Serenity channel + ## v2.0.1 - Use new metadata API diff --git a/script.radioparadise/README.md b/script.radioparadise/README.md index 1a3c8b3528..f4db8d3ff2 100644 --- a/script.radioparadise/README.md +++ b/script.radioparadise/README.md @@ -1,12 +1,12 @@ # Radio Paradise addon for Kodi -Plays [Radio Paradise][] music mixes, accompanied by the HD slideshow. +Plays [Radio Paradise][] music streams, accompanied by the HD slideshow. [radio paradise]: https://radioparadise.com/ ## Features -- Radio Paradise music mixes in AAC or FLAC +- Radio Paradise music streams in AAC or FLAC - HD slideshow (optional) - Auto Play (optional) @@ -16,24 +16,23 @@ Plays [Radio Paradise][] music mixes, accompanied by the HD slideshow. [release]: https://kodi.wiki/view/Releases -## Mix Selection by Script Parameter +## Channel Selection by Script Parameter In addition to the Auto Play feature, the addon script can be called with a -parameter to start a particular RP mix: +parameter to start a particular RP channel: ```python RunScript('script.radioparadise', 1) ``` -Mix parameter: - -| Value | Mix | +| Value | Channel | | --- | --- | -| 0 | RP Main Mix | -| 1 | RP Mellow Mix | -| 2 | RP Rock Mix | -| 3 | RP Global Mix | +| 0 | Main Mix | +| 1 | Mellow Mix | +| 2 | Rock Mix | +| 3 | Global Mix | +| 4 | Serenity | -This can be used to add shortcuts for RP mixes in [favourites.xml][]. +This can be used to add shortcuts for RP channels in [favourites.xml][]. [favourites.xml]: https://kodi.wiki/view/Favourites.xml diff --git a/script.radioparadise/addon.xml b/script.radioparadise/addon.xml index 27f1c05fc2..3935417934 100644 --- a/script.radioparadise/addon.xml +++ b/script.radioparadise/addon.xml @@ -1,5 +1,5 @@ - + @@ -9,7 +9,7 @@ - Radio Paradise addon for Kodi + Radio Paradise music and slideshows An eclectic DJ-mixed blend of rock, indie, electronica, world music, and more. Listener supported & always 100% commercial free. https://radioparadise.com/ https://codeberg.org/alxndr42/script.radioparadise diff --git a/script.radioparadise/resources/channels.json b/script.radioparadise/resources/channels.json new file mode 100644 index 0000000000..fae041510b --- /dev/null +++ b/script.radioparadise/resources/channels.json @@ -0,0 +1,32 @@ +[ + { + "channel_id": "0", + "title": "Main Mix", + "url_aac": "http://stream.radioparadise.com/aac-128", + "url_flac": "http://stream.radioparadise.com/flacm" + }, + { + "channel_id": "1", + "title": "Mellow Mix", + "url_aac": "http://stream.radioparadise.com/mellow-128", + "url_flac": "http://stream.radioparadise.com/mellow-flacm" + }, + { + "channel_id": "2", + "title": "Rock Mix", + "url_aac": "http://stream.radioparadise.com/rock-128", + "url_flac": "http://stream.radioparadise.com/rock-flacm" + }, + { + "channel_id": "3", + "title": "Global Mix", + "url_aac": "http://stream.radioparadise.com/global-128", + "url_flac": "http://stream.radioparadise.com/global-flacm" + }, + { + "channel_id": "42", + "title": "Serenity", + "url_aac": "http://stream.radioparadise.com/serenity", + "url_flac": "http://stream.radioparadise.com/serenity" + } +] diff --git a/script.radioparadise/resources/language/resource.language.en_gb/strings.po b/script.radioparadise/resources/language/resource.language.en_gb/strings.po index ca9085e184..ed01d5beaf 100644 --- a/script.radioparadise/resources/language/resource.language.en_gb/strings.po +++ b/script.radioparadise/resources/language/resource.language.en_gb/strings.po @@ -26,21 +26,25 @@ msgid "FLAC" msgstr "" msgctxt "#30203" -msgid "RP Main Mix" +msgid "Main Mix" msgstr "" msgctxt "#30204" -msgid "RP Mellow Mix" +msgid "Mellow Mix" msgstr "" msgctxt "#30205" -msgid "RP Rock Mix" +msgid "Rock Mix" msgstr "" msgctxt "#30206" -msgid "RP Global Mix" +msgid "Global Mix" msgstr "" msgctxt "#30207" msgid "RP Slideshow" msgstr "" + +msgctxt "#30208" +msgid "Serenity" +msgstr "" diff --git a/script.radioparadise/resources/lib/logger.py b/script.radioparadise/resources/lib/logger.py new file mode 100644 index 0000000000..6c82a23f7f --- /dev/null +++ b/script.radioparadise/resources/lib/logger.py @@ -0,0 +1,27 @@ +import traceback + +import xbmc + + +DEVELOPMENT = False + + +class Logger(): + def __init__(self, name): + self.name = name + + def log(self, message, level=None): + """Log the message.""" + if level is not None: + xbmc.log(f'{self.name}: {message}', level) + elif DEVELOPMENT: + xbmc.log(f'{self.name}: {message}', xbmc.LOGINFO) + else: + xbmc.log(f'{self.name}: {message}', xbmc.LOGDEBUG) + + def exception(self, exc): + """Log the exception.""" + if DEVELOPMENT: + self.log(traceback.format_exc(), xbmc.LOGERROR) + else: + self.log(repr(exc), xbmc.LOGERROR) diff --git a/script.radioparadise/resources/lib/radioparadise.py b/script.radioparadise/resources/lib/radioparadise.py index bac8883b6a..f84c2bb68e 100644 --- a/script.radioparadise/resources/lib/radioparadise.py +++ b/script.radioparadise/resources/lib/radioparadise.py @@ -1,8 +1,11 @@ from collections import OrderedDict +import json +from pathlib import Path import re import time import requests +import xbmcaddon NOWPLAYING_URL = 'https://api.radioparadise.com/api/nowplaying_list_v2022?chan={}&list_num=10' @@ -21,34 +24,10 @@ # Number of seconds to wait before retrying API updates UPDATE_WAIT = 5 -STREAMS = [ - { - 'channel': 0, - 'title': 'RP Main Mix', - 'url_aac': 'http://stream.radioparadise.com/aac-128', - 'url_flac': 'http://stream.radioparadise.com/flacm', - }, - { - 'channel': 1, - 'title': 'RP Mellow Mix', - 'url_aac': 'http://stream.radioparadise.com/mellow-128', - 'url_flac': 'http://stream.radioparadise.com/mellow-flacm', - }, - { - 'channel': 2, - 'title': 'RP Rock Mix', - 'url_aac': 'http://stream.radioparadise.com/rock-128', - 'url_flac': 'http://stream.radioparadise.com/rock-flacm', - }, - { - 'channel': 3, - 'title': 'RP Global Mix', - 'url_aac': 'http://stream.radioparadise.com/global-128', - 'url_flac': 'http://stream.radioparadise.com/global-flacm', - }, -] -STREAM_INFO = {s['url_aac']: s for s in STREAMS} -STREAM_INFO.update({s['url_flac']: s for s in STREAMS}) +# List of channel objects from channels.json +CHANNELS = None +# Map of stream URL to channel object +CHANNEL_INFO = None class NowPlaying(): @@ -74,10 +53,10 @@ def get_next_song(self, song_key): next_key = self.songs.get(song_key, {}).get('next_key') return self.songs.get(next_key) - def set_channel(self, channel): - """Set the RP channel number, or None.""" - if channel is not None: - self.url = NOWPLAYING_URL.format(channel) + def set_channel(self, channel_id): + """Set the RP channel ID, or None.""" + if channel_id is not None: + self.url = NOWPLAYING_URL.format(channel_id) else: self.url = None self.current = None @@ -146,3 +125,16 @@ def build_key(strings): words = KEY_FILTER_RE.sub(' ', s).casefold().split() result.extend(words) return tuple(sorted(result)) + + +def init(): + global CHANNELS, CHANNEL_INFO + addon = xbmcaddon.Addon() + addon_path = addon.getAddonInfo('path') + channels_json = Path(addon_path, 'resources', 'channels.json') + CHANNELS = json.loads(channels_json.read_text()) + CHANNEL_INFO = {s['url_aac']: s for s in CHANNELS} + CHANNEL_INFO.update({s['url_flac']: s for s in CHANNELS}) + + +init() diff --git a/script.radioparadise/resources/lib/script.py b/script.radioparadise/resources/lib/script.py index a7206833fb..2d91468594 100644 --- a/script.radioparadise/resources/lib/script.py +++ b/script.radioparadise/resources/lib/script.py @@ -4,16 +4,16 @@ import xbmcaddon import xbmcgui -from .radioparadise import STREAMS +from .radioparadise import CHANNELS class Window(xbmcgui.WindowXML): def onInit(self): xbmc.executebuiltin('Container.SetViewMode(100)') listitems = [] - for s in STREAMS: - item = xbmcgui.ListItem(s['title']) - item.setProperty('channel', str(s['channel'])) + for idx, channel in enumerate(CHANNELS): + item = xbmcgui.ListItem(channel['title']) + item.setProperty('channel_index', str(idx)) listitems.append(item) self.clearList() self.addItems(listitems) @@ -23,20 +23,20 @@ def onInit(self): def onClick(self, controlId): if controlId == 100: item = self.getListItem(self.getCurrentListPosition()) - channel = int(item.getProperty('channel')) - play_channel(channel) + channel_index = int(item.getProperty('channel_index')) + play_channel(channel_index) self.close() -def play_channel(channel_number): +def play_channel(channel_index): """Play the channel, unless it's already playing.""" - stream = STREAMS[channel_number] + channel = CHANNELS[channel_index] addon = xbmcaddon.Addon() audio_format = addon.getSetting('audio_format') if audio_format == 'flac': - url = stream['url_flac'] + url = channel['url_flac'] else: - url = stream['url_aac'] + url = channel['url_aac'] player = xbmc.Player() if not player.isPlayingAudio() or player.getPlayingFile() != url: player.stop() diff --git a/script.radioparadise/resources/lib/service.py b/script.radioparadise/resources/lib/service.py index b456d6e502..8e3a33f825 100644 --- a/script.radioparadise/resources/lib/service.py +++ b/script.radioparadise/resources/lib/service.py @@ -1,21 +1,21 @@ import time -import traceback import requests import xbmc import xbmcaddon import xbmcgui -from .radioparadise import STREAM_INFO, NowPlaying, build_key +from .logger import Logger +from .radioparadise import CHANNEL_INFO, NowPlaying, build_key -DEVELOPMENT = False - EXPIRATION_DELAY = 10 RESTART_DELAY = 1.0 RESTART_TIMEOUT = 1.0 +LOG = Logger('rp_service') + class Song(): """Current song information.""" @@ -149,9 +149,10 @@ def update_slideshow(self): def update_song(self): """Update song metadata, if necessary.""" player_key = self.get_song_key() - song = self.song if player_key is None: return + + song = self.song if song and not (song.key != player_key or song.expired()): return @@ -163,22 +164,26 @@ def update_song(self): start_time = None song_data = None - # Try to match API metadata on song changes + if song is None: start_time = 0 song_data = self.now_playing.get_song_data(player_key) elif song.key != player_key and not song.expired(): start_time = self.tracked_time song_data = self.now_playing.get_song_data(player_key) - # Show "next" song if the song change was missed - elif song.expired(): + self.slideshow.set_slides(None) + if song.start_time == 0: + song.start_time = self.tracked_time - song.duration + else: start_time = song.start_time + song.duration song_data = self.now_playing.get_next_song(player_key) - # Without API metadata, show the stream metadata - if song_data is None and song.start_time: - song.start_time = 0 + # Fall back to stream metadata + if song_data is None: + self.song = None + self.tracked_time = 0 self.slideshow.set_slides(None) self.clear_player() + # API metadata may not be available yet if song_data is None: return @@ -196,13 +201,13 @@ def update_song(self): song_key = build_key((song_data['artist'], song_data['title'])) self.song = Song(song_key, song_data, fanart, start_time) - log(f'Song: {self.song}') + LOG.log(f'Song: {self.song}') self.update_player() def onAVStarted(self): - if self.isPlaying() and self.getPlayingFile() in STREAM_INFO: + if self.isPlaying() and self.getPlayingFile() in CHANNEL_INFO: url = self.getPlayingFile() - info = STREAM_INFO[url] + info = CHANNEL_INFO[url] # Kodi switches to fullscreen for FLAC, but not AAC if url == info['url_aac']: xbmc.executebuiltin('Action(FullScreen)') @@ -219,12 +224,12 @@ def onPlayBackError(self): self.reset() def onPlayBackStarted(self): - if self.isPlaying() and self.getPlayingFile() in STREAM_INFO: + if self.isPlaying() and self.getPlayingFile() in CHANNEL_INFO: url = self.getPlayingFile() self.stream_url = url self.restart_time = 0 - info = STREAM_INFO[url] - self.now_playing.set_channel(info['channel']) + info = CHANNEL_INFO[url] + self.now_playing.set_channel(info['channel_id']) else: self.reset() @@ -259,18 +264,8 @@ def next_slide(self): return result -def log(message, level=None): - """Write to the Kodi log.""" - if level is not None: - xbmc.log(f'rp_service: {message}', level) - elif DEVELOPMENT: - xbmc.log(f'rp_service: {message}', xbmc.LOGINFO) - else: - xbmc.log(f'rp_service: {message}', xbmc.LOGDEBUG) - - def run_service(): - log('Service started.') + LOG.log('Service started.') player = Player() monitor = xbmc.Monitor() while not monitor.abortRequested(): @@ -279,8 +274,5 @@ def run_service(): try: player.update() except Exception as e: - if DEVELOPMENT: - log(traceback.format_exc(), xbmc.LOGERROR) - else: - log(repr(e), xbmc.LOGERROR) - log('Service exiting.') + LOG.exception(e) + LOG.log('Service exiting.') diff --git a/script.radioparadise/resources/skins/Default/720p/script-radioparadise.xml b/script.radioparadise/resources/skins/Default/720p/script-radioparadise.xml index 0da40d58a6..96cbe1c2ae 100644 --- a/script.radioparadise/resources/skins/Default/720p/script-radioparadise.xml +++ b/script.radioparadise/resources/skins/Default/720p/script-radioparadise.xml @@ -5,8 +5,8 @@ 50% 50% - 180 - 180 + 300 + 300 keep 10 logo.png @@ -14,8 +14,8 @@ 50% 50% - 180 - 120 + 150 + 150 list