From: Hilbrand Bouwkamp Date: Sun, 24 Oct 2021 09:42:14 +0000 (+0200) Subject: [spotify] Various enhancements and fixes (#11370) X-Git-Url: https://git.basschouten.com/?a=commitdiff_plain;h=196e4e2210e47a00d010fdebae6c7e5f9e4f4a4f;p=openhab-addons.git [spotify] Various enhancements and fixes (#11370) Enhancements: - Added play actions to start a track or other via a rule. - Added albumImageUrl with the url to the album image to not have to send the whole image via the openHAB eventbus. - Added a parameter imageIndex to set the image size to use (0, 1 or 2) to channels albumImage and albumImageUrl. - Added offset and limit parameters to playlists channel (Closes #6843). Fixes: - Fixed invalid expire value set on ExpiringCache (Closes #10490). - Improved handling Spotify API error messages (Closes #11308). - Added TTL to discovery results to fix duplicated Spotify Connect devices reported in the inbox (Closes #7400). - Fixed device list update problem for devices. If no devices where configured as separate things (or the devices added where offline, but non added where not offline) the list was not updated. Signed-off-by: Hilbrand Bouwkamp --- diff --git a/bundles/org.openhab.binding.spotify/README.md b/bundles/org.openhab.binding.spotify/README.md index 50ee99678c..59f5fe197c 100644 --- a/bundles/org.openhab.binding.spotify/README.md +++ b/bundles/org.openhab.binding.spotify/README.md @@ -109,8 +109,22 @@ __Common Channels:__ | playlistName | String | Read-write | The currently playing playlist. Or empty if no playing list is playing. | | albumName | String | Read-only | Album Name of the currently playing track. | | albumImage | RawType | Read-only | Album Image of the currently playing track. | +| albumImageUrl | String | Read-only | Url to the album Image of the currently playing track. | | artistName | String | Read-only | Artist Name of the currently playing track. | +The `playlists` channel has 2 parameters: + +| Parameter | Description | +|-----------|----------------------------------------------------------------------------| +| offset | The index of the first playlist to return. Default `0`, max `100.000` | +| limit | The maximum number of playlists to return. Default `20`, min `1`, max `50` | + +The `albumImage` and `albumImageUrl` channels has 1 parameter: + +| Parameter | Description | +|------------|--------------------------------------------------------------------------------------------| +| imageIndex | Index in list of to select size of the image to show. 0:large (default), 1:medium, 2:small | + Note: The `deviceName` and `playlist` channels are Selection channels. They are dynamically populated by the binding with the user specific devices and playlists. @@ -163,6 +177,19 @@ __Advanced Channels:__ | deviceActive | Switch | Read-only | Indicates if the device is active or not. Should be the same as Thing status ONLINE/OFFLINE. | | deviceRestricted | Switch | Read-only | Indicates if this device allows to be controlled by the API or not. If restricted it cannot be controlled. | +### Actions + +The bridge supports an action to play a track or other context uri. +The following actions are supported: + +``` +play(String context_uri) +play(String context_uri, int offset, int position_ms) +play(String context_uri, String device_id) +play(String context_uri, String device_id, int offset, int position_ms) +``` + + ## Full Example In this example there is a bridge configured with Thing ID __user1__ and illustrating that the bridge is authorized to play in the context of the Spotify user account __user1__. @@ -174,24 +201,27 @@ Bridge spotify:player:user1 "Me" [clientId="", clientSecret=" '"' + t + '"') - .collect(Collectors.joining(","))); + play = String.format(PLAY_TRACK_URIS, + Arrays.asList(trackId.split(",")).stream().map(t -> '"' + t + '"').collect(Collectors.joining(",")), + offset, positionMs); } else { - play = String.format(PLAY_TRACK_CONTEXT_URI, trackId); + play = String.format(PLAY_TRACK_CONTEXT_URI, trackId, offset, positionMs); } - requestPlayer(PUT, url, play); + requestPlayer(PUT, url, play, String.class); } /** @@ -127,7 +134,7 @@ public class SpotifyApi { * @param play if true transfers and starts to play, else transfers but pauses. */ public void transferPlay(String deviceId, boolean play) { - requestPlayer(PUT, "", String.format(TRANSFER_PLAY, deviceId, play)); + requestPlayer(PUT, "", String.format(TRANSFER_PLAY, deviceId, play), String.class); } /** @@ -174,7 +181,7 @@ public class SpotifyApi { * active device. * * @param deviceId device to set repeat state on or empty if set repeat on the active device - * @param repeateState set the spotify repeat state + * @param repeateState set the Spotify repeat state */ public void setRepeatState(String deviceId, String repeateState) { requestPlayer(PUT, String.format("repeat?state=%s", repeateState) + optionalDeviceId(deviceId, AMP)); @@ -208,8 +215,7 @@ public class SpotifyApi { * @return Calls Spotify Api and returns the list of device or an empty list if nothing was returned */ public List getDevices() { - final ContentResponse response = requestPlayer(GET, "devices"); - final Devices deviceList = ModelUtil.gsonInstance().fromJson(response.getContentAsString(), Devices.class); + final Devices deviceList = requestPlayer(GET, "devices", "", Devices.class); return deviceList == null || deviceList.getDevices() == null ? Collections.emptyList() : deviceList.getDevices(); @@ -218,9 +224,9 @@ public class SpotifyApi { /** * @return Returns the playlists of the user. */ - public List getPlaylists() { - final ContentResponse response = request(GET, SPOTIFY_API_URL + "/playlists", ""); - final Playlists playlists = ModelUtil.gsonInstance().fromJson(response.getContentAsString(), Playlists.class); + public List getPlaylists(int offset, int limit) { + final Playlists playlists = request(GET, SPOTIFY_API_URL + "/playlists?offset" + offset + "&limit=" + limit, "", + Playlists.class); return playlists == null || playlists.getItems() == null ? Collections.emptyList() : playlists.getItems(); } @@ -230,9 +236,7 @@ public class SpotifyApi { * returned by Spotify */ public CurrentlyPlayingContext getPlayerInfo() { - final ContentResponse response = requestPlayer(GET, ""); - final CurrentlyPlayingContext context = ModelUtil.gsonInstance().fromJson(response.getContentAsString(), - CurrentlyPlayingContext.class); + final CurrentlyPlayingContext context = requestPlayer(GET, "", "", CurrentlyPlayingContext.class); return context == null ? EMPTY_CURRENTLYPLAYINGCONTEXT : context; } @@ -242,11 +246,10 @@ public class SpotifyApi { * Spotify. * * @param method Http method to perform - * @param url url path to call to spotify - * @return the response give by Spotify + * @param url url path to call to Spotify */ - private ContentResponse requestPlayer(HttpMethod method, String url) { - return requestPlayer(method, url, ""); + private void requestPlayer(HttpMethod method, String url) { + requestPlayer(method, url, "", String.class); } /** @@ -254,23 +257,42 @@ public class SpotifyApi { * Spotify. * * @param method Http method to perform - * @param url url path to call to spotify + * @param url url path to call to Spotify * @param requestData data to pass along with the call as content + * @param clazz data type of return data, if null no data is expected to be returned. * @return the response give by Spotify */ - private ContentResponse requestPlayer(HttpMethod method, String url, String requestData) { - return request(method, SPOTIFY_API_PLAYER_URL + (url.isEmpty() ? "" : ('/' + url)), requestData); + private @Nullable T requestPlayer(HttpMethod method, String url, String requestData, Class clazz) { + return request(method, SPOTIFY_API_PLAYER_URL + (url.isEmpty() ? "" : ('/' + url)), requestData, clazz); + } + + /** + * Parses the Spotify returned json. + * + * @param z data type to return + * @param content json content to parse + * @param clazz data type to return + * @throws SpotifyException throws a {@link SpotifyException} in case the json could not be parsed. + * @return parsed json. + */ + private static @Nullable T fromJson(String content, Class clazz) { + try { + return (T) ModelUtil.gsonInstance().fromJson(content, clazz); + } catch (final JsonSyntaxException e) { + throw new SpotifyException("Unknown Spotify response:" + content, e); + } } /** * Calls the Spotify Web Api with the given method and given url as parameters of the call to Spotify. * * @param method Http method to perform - * @param url url path to call to spotify + * @param url url path to call to Spotify * @param requestData data to pass along with the call as content + * @param clazz data type of return data, if null no data is expected to be returned. * @return the response give by Spotify */ - private ContentResponse request(HttpMethod method, String url, String requestData) { + private @Nullable T request(HttpMethod method, String url, String requestData, Class clazz) { logger.debug("Request: ({}) {} - {}", method, url, requestData); final Function call = httpClient -> httpClient.newRequest(url).method(method) .header("Accept", CONTENT_TYPE).content(new StringContentProvider(requestData), CONTENT_TYPE); @@ -280,11 +302,13 @@ public class SpotifyApi { if (accessToken == null || accessToken.isEmpty()) { throw new SpotifyAuthorizationException( - "No spotify accesstoken. Did you authorize spotify via /connectspotify ?"); + "No Spotify accesstoken. Did you authorize Spotify via /connectspotify ?"); } else { - return requestWithRetry(call, accessToken); + final String response = requestWithRetry(call, accessToken).getContentAsString(); + + return clazz == String.class ? (@Nullable T) response : fromJson(response, clazz); } - } catch (IOException e) { + } catch (final IOException e) { throw new SpotifyException(e.getMessage(), e); } catch (OAuthException | OAuthResponseException e) { throw new SpotifyAuthorizationException(e.getMessage(), e); @@ -295,7 +319,7 @@ public class SpotifyApi { throws OAuthException, IOException, OAuthResponseException { try { return connector.request(call, BEARER + accessToken); - } catch (SpotifyTokenExpiredException e) { + } catch (final SpotifyTokenExpiredException e) { // Retry with new access token return connector.request(call, BEARER + oAuthClientService.refreshToken().getAccessToken()); } diff --git a/bundles/org.openhab.binding.spotify/src/main/java/org/openhab/binding/spotify/internal/discovery/SpotifyDeviceDiscoveryService.java b/bundles/org.openhab.binding.spotify/src/main/java/org/openhab/binding/spotify/internal/discovery/SpotifyDeviceDiscoveryService.java index d84b6fad0c..b6f6872a52 100644 --- a/bundles/org.openhab.binding.spotify/src/main/java/org/openhab/binding/spotify/internal/discovery/SpotifyDeviceDiscoveryService.java +++ b/bundles/org.openhab.binding.spotify/src/main/java/org/openhab/binding/spotify/internal/discovery/SpotifyDeviceDiscoveryService.java @@ -12,8 +12,10 @@ */ package org.openhab.binding.spotify.internal.discovery; -import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.*; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.PROPERTY_SPOTIFY_DEVICE_NAME; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.THING_TYPE_DEVICE; +import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -55,6 +57,8 @@ public class SpotifyDeviceDiscoveryService extends AbstractDiscoveryService private static final int DISCOVERY_TIME_SECONDS = 10; // Check every minute for new devices private static final long BACKGROUND_SCAN_REFRESH_MINUTES = 1; + // Time to life for discovered things. + private static final long TTL_SECONDS = Duration.ofHours(1).toSeconds(); private final Logger logger = LoggerFactory.getLogger(SpotifyDeviceDiscoveryService.class); @@ -74,7 +78,7 @@ public class SpotifyDeviceDiscoveryService extends AbstractDiscoveryService @Override public void activate() { - Map properties = new HashMap<>(); + final Map properties = new HashMap<>(); properties.put(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY, Boolean.TRUE); super.activate(properties); } @@ -120,22 +124,22 @@ public class SpotifyDeviceDiscoveryService extends AbstractDiscoveryService logger.debug("Starting Spotify Device discovery for bridge {}", bridgeUID); try { bridgeHandler.listDevices().forEach(this::thingDiscovered); - } catch (RuntimeException e) { + } catch (final RuntimeException e) { logger.debug("Finding devices failed with message: {}", e.getMessage(), e); } } } private void thingDiscovered(Device device) { - Map properties = new HashMap<>(); + final Map properties = new HashMap<>(); properties.put(PROPERTY_SPOTIFY_DEVICE_NAME, device.getName()); - ThingUID thing = new ThingUID(SpotifyBindingConstants.THING_TYPE_DEVICE, bridgeUID, + final ThingUID thing = new ThingUID(SpotifyBindingConstants.THING_TYPE_DEVICE, bridgeUID, device.getId().substring(0, PLAYER_ID_LENGTH)); - DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thing).withBridge(bridgeUID) + final DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thing).withBridge(bridgeUID) .withProperties(properties).withRepresentationProperty(PROPERTY_SPOTIFY_DEVICE_NAME) - .withLabel(device.getName()).build(); + .withTTL(TTL_SECONDS).withLabel(device.getName()).build(); thingDiscovered(discoveryResult); } diff --git a/bundles/org.openhab.binding.spotify/src/main/java/org/openhab/binding/spotify/internal/handler/SpotifyBridgeHandler.java b/bundles/org.openhab.binding.spotify/src/main/java/org/openhab/binding/spotify/internal/handler/SpotifyBridgeHandler.java index 961c85d180..08c126ca66 100644 --- a/bundles/org.openhab.binding.spotify/src/main/java/org/openhab/binding/spotify/internal/handler/SpotifyBridgeHandler.java +++ b/bundles/org.openhab.binding.spotify/src/main/java/org/openhab/binding/spotify/internal/handler/SpotifyBridgeHandler.java @@ -12,9 +12,53 @@ */ package org.openhab.binding.spotify.internal.handler; -import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.*; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_ACCESSTOKEN; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_CONFIG_IMAGE_INDEX; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_DEVICEACTIVE; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_DEVICEID; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_DEVICENAME; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_DEVICES; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_DEVICESHUFFLE; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_DEVICETYPE; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_DEVICEVOLUME; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_ALBUMHREF; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_ALBUMID; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_ALBUMIMAGE; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_ALBUMIMAGEURL; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_ALBUMNAME; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_ALBUMTYPE; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_ALBUMURI; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_ARTISTHREF; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_ARTISTID; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_ARTISTNAME; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_ARTISTTYPE; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_ARTISTURI; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_TRACKDISCNUMBER; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_TRACKDURATION_FMT; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_TRACKDURATION_MS; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_TRACKEXPLICIT; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_TRACKHREF; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_TRACKID; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_TRACKNAME; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_TRACKNUMBER; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_TRACKPOPULARITY; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_TRACKPROGRESS_FMT; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_TRACKPROGRESS_MS; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_TRACKTYPE; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYED_TRACKURI; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYLISTNAME; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYLISTS; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYLISTS_LIMIT; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_PLAYLISTS_OFFSET; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_TRACKPLAYER; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.CHANNEL_TRACKREPEAT; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.PROPERTY_SPOTIFY_USER; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.SPOTIFY_API_TOKEN_URL; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.SPOTIFY_AUTHORIZE_URL; +import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.SPOTIFY_SCOPES; import java.io.IOException; +import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.time.Duration; import java.util.Collection; @@ -31,6 +75,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.spotify.internal.SpotifyAccountHandler; import org.openhab.binding.spotify.internal.SpotifyBridgeConfiguration; +import org.openhab.binding.spotify.internal.actions.SpotifyActions; import org.openhab.binding.spotify.internal.api.SpotifyApi; import org.openhab.binding.spotify.internal.api.exception.SpotifyAuthorizationException; import org.openhab.binding.spotify.internal.api.exception.SpotifyException; @@ -133,6 +178,8 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler private volatile State lastTrackId = StringType.EMPTY; private volatile String lastKnownDeviceId = ""; private volatile boolean lastKnownDeviceActive; + private int imageChannelAlbumImageIndex; + private int imageChannelAlbumImageUrlIndex; public SpotifyBridgeHandler(Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient, SpotifyDynamicStateDescriptionProvider spotifyDynamicStateDescriptionProvider) { @@ -146,7 +193,7 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler @Override public Collection> getServices() { - return Collections.singleton(SpotifyDeviceDiscoveryService.class); + return List.of(SpotifyActions.class, SpotifyDeviceDiscoveryService.class); } @Override @@ -172,7 +219,7 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler && handleCommand.handleCommand(channelUID, command, lastKnownDeviceActive, lastKnownDeviceId)) { scheduler.schedule(this::scheduledPollingRestart, POLL_DELAY_AFTER_COMMAND_S, TimeUnit.SECONDS); } - } catch (SpotifyException e) { + } catch (final SpotifyException e) { logger.debug("Handle Spotify command failed: ", e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage()); } @@ -227,7 +274,7 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler } @Nullable - SpotifyApi getSpotifyApi() { + public SpotifyApi getSpotifyApi() { return spotifyApi; } @@ -240,7 +287,7 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler public String formatAuthorizationUrl(String redirectUri) { try { return oAuthService.getAuthorizationUrl(redirectUri, null, thing.getUID().getAsString()); - } catch (OAuthException e) { + } catch (final OAuthException e) { logger.debug("Error constructing AuthorizationUrl: ", e); return ""; } @@ -259,7 +306,7 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler } catch (RuntimeException | OAuthException | IOException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); throw new SpotifyException(e.getMessage(), e); - } catch (OAuthResponseException e) { + } catch (final OAuthResponseException e) { throw new SpotifyAuthorizationException(e.getMessage(), e); } } @@ -287,9 +334,13 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler oAuthService.addAccessTokenRefreshListener(SpotifyBridgeHandler.this); spotifyApi = new SpotifyApi(oAuthService, scheduler, httpClient); handleCommand = new SpotifyHandleCommands(spotifyApi); - playingContextCache = new ExpiringCache<>(configuration.refreshPeriod, spotifyApi::getPlayerInfo); - playlistCache = new ExpiringCache<>(POLL_PLAY_LIST_HOURS, spotifyApi::getPlaylists); - devicesCache = new ExpiringCache<>(configuration.refreshPeriod, spotifyApi::getDevices); + final Duration expiringPeriod = Duration.ofSeconds(configuration.refreshPeriod); + + playingContextCache = new ExpiringCache<>(expiringPeriod, spotifyApi::getPlayerInfo); + final int offset = getIntChannelParameter(CHANNEL_PLAYLISTS, CHANNEL_PLAYLISTS_OFFSET, 0); + final int limit = getIntChannelParameter(CHANNEL_PLAYLISTS, CHANNEL_PLAYLISTS_LIMIT, 20); + playlistCache = new ExpiringCache<>(POLL_PLAY_LIST_HOURS, () -> spotifyApi.getPlaylists(offset, limit)); + devicesCache = new ExpiringCache<>(expiringPeriod, spotifyApi::getDevices); // Start with update status by calling Spotify. If no credentials available no polling should be started. scheduler.execute(() -> { @@ -297,6 +348,16 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler startPolling(); } }); + imageChannelAlbumImageIndex = getIntChannelParameter(CHANNEL_PLAYED_ALBUMIMAGE, CHANNEL_CONFIG_IMAGE_INDEX, 0); + imageChannelAlbumImageUrlIndex = getIntChannelParameter(CHANNEL_PLAYED_ALBUMIMAGEURL, + CHANNEL_CONFIG_IMAGE_INDEX, 0); + } + + private int getIntChannelParameter(String channelName, String parameter, int _default) { + final Channel channel = thing.getChannel(channelName); + final BigDecimal index = channel == null ? null : (BigDecimal) channel.getConfiguration().get(parameter); + + return index == null ? _default : index.intValue(); } @Override @@ -318,7 +379,7 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler if (pollStatus() && pollingNotRunning) { startPolling(); } - } catch (RuntimeException e) { + } catch (final RuntimeException e) { logger.debug("Restarting polling failed: ", e); } } @@ -363,7 +424,7 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler final CurrentlyPlayingContext playingContext = pc == null ? EMPTY_CURRENTLY_PLAYING_CONTEXT : pc; // Collect devices and populate selection with available devices. - if (hasPlayData || hasAnyDeviceStatusUnknown()) { + if (hasPlayData) { final List ld = devicesCache.getValue(); final List devices = ld == null ? Collections.emptyList() : ld; spotifyDynamicStateDescriptionProvider.setDevices(devicesChannelUID, devices); @@ -381,17 +442,17 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler } updateStatus(ThingStatus.ONLINE); return true; - } catch (SpotifyAuthorizationException e) { + } catch (final SpotifyAuthorizationException e) { logger.debug("Authorization error during polling: ", e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); cancelSchedulers(); devicesCache.invalidateValue(); - } catch (SpotifyException e) { + } catch (final SpotifyException e) { logger.info("Spotify returned an error during polling: {}", e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); - } catch (RuntimeException e) { + } catch (final RuntimeException e) { // This only should catch RuntimeException as the apiCall don't throw other exceptions. logger.info("Unexpected error during polling status, please report if this keeps occurring: ", e); @@ -431,12 +492,6 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler .forEach(thing -> ((SpotifyDeviceHandler) thing.getHandler()).setStatusGone()); } - private boolean hasAnyDeviceStatusUnknown() { - return getThing().getThings().stream() // - .filter(thing -> thing.getHandler() instanceof SpotifyDeviceHandler) // - .anyMatch(sd -> ((SpotifyDeviceHandler) sd.getHandler()).getThing().getStatus() == ThingStatus.UNKNOWN); - } - /** * Update the player data. * @@ -651,22 +706,32 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler * @param album album data */ public void updateAlbumImage(Album album) { - final Channel channel = thing.getChannel(CHANNEL_PLAYED_ALBUMIMAGE); + final Channel imageChannel = thing.getChannel(CHANNEL_PLAYED_ALBUMIMAGE); final List images = album.getImages(); - if (channel != null && images != null && !images.isEmpty()) { - final String imageUrl = images.get(0).getUrl(); + // Update album image url channel + final String albumImageUrlUrl = albumUrl(images, imageChannelAlbumImageUrlIndex); + updateChannelState(CHANNEL_PLAYED_ALBUMIMAGEURL, + albumImageUrlUrl == null ? UnDefType.UNDEF : StringType.valueOf(albumImageUrlUrl)); - if (!lastAlbumImageUrl.equals(imageUrl)) { + // Trigger image refresh of album image channel + final String albumImageUrl = albumUrl(images, imageChannelAlbumImageIndex); + if (imageChannel != null && albumImageUrl != null) { + if (!lastAlbumImageUrl.equals(albumImageUrl)) { // Download the cover art in a different thread to not delay the other operations - lastAlbumImageUrl = imageUrl == null ? "" : imageUrl; - refreshAlbumImage(channel.getUID()); - } + lastAlbumImageUrl = albumImageUrl; + refreshAlbumImage(imageChannel.getUID()); + } // else album image still the same so nothing to do } else { + lastAlbumImageUrl = ""; updateChannelState(CHANNEL_PLAYED_ALBUMIMAGE, UnDefType.UNDEF); } } + private @Nullable String albumUrl(@Nullable List images, int index) { + return images == null || index >= images.size() || images.isEmpty() ? null : images.get(index).getUrl(); + } + /** * Refreshes the image asynchronously, but only downloads the image if the channel is linked to avoid * unnecessary downloading of the image. @@ -686,7 +751,7 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler final RawType image = HttpUtil.downloadImage(imageUrl, true, MAX_IMAGE_SIZE); updateChannelState(CHANNEL_PLAYED_ALBUMIMAGE, image == null ? UnDefType.UNDEF : image); } - } catch (RuntimeException e) { + } catch (final RuntimeException e) { logger.debug("Async call to refresh Album image failed: ", e); } } diff --git a/bundles/org.openhab.binding.spotify/src/main/java/org/openhab/binding/spotify/internal/handler/SpotifyHandleCommands.java b/bundles/org.openhab.binding.spotify/src/main/java/org/openhab/binding/spotify/internal/handler/SpotifyHandleCommands.java index 2de7f94ffd..e5c80dc092 100644 --- a/bundles/org.openhab.binding.spotify/src/main/java/org/openhab/binding/spotify/internal/handler/SpotifyHandleCommands.java +++ b/bundles/org.openhab.binding.spotify/src/main/java/org/openhab/binding/spotify/internal/handler/SpotifyHandleCommands.java @@ -123,7 +123,7 @@ class SpotifyHandleCommands { case CHANNEL_TRACKPLAY: case CHANNEL_PLAYLISTS: if (command instanceof StringType) { - spotifyApi.playTrack(deviceId, command.toString()); + spotifyApi.playTrack(deviceId, command.toString(), 0, 0); commandRun = true; } break; @@ -132,7 +132,7 @@ class SpotifyHandleCommands { final String newName = command.toString(); playlists.stream().filter(pl -> pl.getName().equals(newName)).findFirst() - .ifPresent(pl -> spotifyApi.playTrack(deviceId, pl.getUri())); + .ifPresent(pl -> spotifyApi.playTrack(deviceId, pl.getUri(), 0, 0)); commandRun = true; } break; diff --git a/bundles/org.openhab.binding.spotify/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.spotify/src/main/resources/OH-INF/config/config.xml new file mode 100644 index 0000000000..6802e8d783 --- /dev/null +++ b/bundles/org.openhab.binding.spotify/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,25 @@ + + + + + + The maximum number of playlists to return + 20 + + + + The index of the first playlist to return + 0 + + + + + + Index in list of to select image to show + 0 + + + diff --git a/bundles/org.openhab.binding.spotify/src/main/resources/OH-INF/i18n/spotify_en.properties b/bundles/org.openhab.binding.spotify/src/main/resources/OH-INF/i18n/spotify_en.properties new file mode 100644 index 0000000000..f3a26ce8c0 --- /dev/null +++ b/bundles/org.openhab.binding.spotify/src/main/resources/OH-INF/i18n/spotify_en.properties @@ -0,0 +1,10 @@ +actions.play.label=Play +actions.play.description=Play the given Spotify uri +actions.play.context_uri.label=Context URI +actions.play.context_uri.description=The context uri or a comma separated list of uris +actions.play.device_id.label=Device Id +actions.play.device_id.description=Id of the device to play. If omitted will play on the current active device (Optional) +actions.play.offset.label=offset +actions.play.offset.description=Offset to start (Optional). +actions.play.positions_ms.label=Position ms +actions.play.positions_ms.description=Position in milliseconds to start (Optional) diff --git a/bundles/org.openhab.binding.spotify/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.spotify/src/main/resources/OH-INF/thing/thing-types.xml index 949b5231ee..b591562ad5 100644 --- a/bundles/org.openhab.binding.spotify/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.spotify/src/main/resources/OH-INF/thing/thing-types.xml @@ -48,6 +48,7 @@ + @@ -78,10 +79,8 @@ 10 This is the frequency of the polling requests to the Spotify Connect Web API. There are limits to the - number of requests - that can be sent to the Web API. The more often you poll, the better status updates - at the risk - of running out of - your request quota. + number of requests that can be sent to the Web API. The more often you poll, the better status updates - at the + risk of running out of your request quota. @@ -159,6 +158,7 @@ String List of the users playlists + @@ -293,6 +293,14 @@ The cover art for the album in widest size + + + + String + + The URL to the cover art for the album in widest size + +