diff --git a/bundles/org.openhab.binding.spotify/README.md b/bundles/org.openhab.binding.spotify/README.md index 50ee99678cb39..59f5fe197cb87 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 void play(String deviceId) { * @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 void setVolume(String deviceId, int volumePercent) { * 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 @@ private String optionalDeviceId(String deviceId, char prefix) { * @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 List getDevices() { /** * @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 List getPlaylists() { * 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 CurrentlyPlayingContext getPlayerInfo() { * 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 @@ private ContentResponse requestPlayer(HttpMethod method, String url) { * 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 @@ private ContentResponse request(HttpMethod method, String url, String requestDat 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 @@ private ContentResponse requestWithRetry(final Function cal 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 d84b6fad0c73f..b6f6872a5258e 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 Set getSupportedThingTypes() { @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 @@ protected void startScan() { 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 961c85d180213..08c126ca66fdd 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.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 SpotifyBridgeHandler(Bridge bridge, OAuthFactory oAuthFactory, HttpClient @Override public Collection> getServices() { - return Collections.singleton(SpotifyDeviceDiscoveryService.class); + return List.of(SpotifyActions.class, SpotifyDeviceDiscoveryService.class); } @Override @@ -172,7 +219,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { && 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 boolean isOnline() { } @Nullable - SpotifyApi getSpotifyApi() { + public SpotifyApi getSpotifyApi() { return spotifyApi; } @@ -240,7 +287,7 @@ public boolean equalsThingUID(String thingUID) { 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 String authorize(String redirectUri, String reqCode) { } 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 void initialize() { 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 void initialize() { 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 @@ private void scheduledPollingRestart() { if (pollStatus() && pollingNotRunning) { startPolling(); } - } catch (RuntimeException e) { + } catch (final RuntimeException e) { logger.debug("Restarting polling failed: ", e); } } @@ -363,7 +424,7 @@ private boolean pollStatus() { 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 @@ private boolean pollStatus() { } 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 @@ private void updateDevicesStatus(List spotifyDevices, boolean playing) { .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 @@ private class AlbumUpdater { * @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 @@ private void refreshAlbumAsynced(ChannelUID channelUID, String imageUrl) { 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 2de7f94ffd729..e5c80dc092628 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 @@ public boolean handleCommand(ChannelUID channelUID, Command command, boolean act 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 @@ public boolean handleCommand(ChannelUID channelUID, Command command, boolean act 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 0000000000000..6802e8d78309f --- /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 0000000000000..f3a26ce8c0d02 --- /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 949b5231eee66..b591562ad5ec2 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 + +