[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 <hilbrand@h72.nl>
This commit is contained in:
Hilbrand Bouwkamp 2021-10-24 11:42:14 +02:00 committed by GitHub
parent 3fc968a980
commit 196e4e2210
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 381 additions and 89 deletions

View File

@ -109,8 +109,22 @@ __Common Channels:__
| playlistName | String | Read-write | The currently playing playlist. Or empty if no playing list is playing. | | 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. | | albumName | String | Read-only | Album Name of the currently playing track. |
| albumImage | RawType | Read-only | Album Image 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. | | 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. Note: The `deviceName` and `playlist` channels are Selection channels.
They are dynamically populated by the binding with the user specific devices and playlists. 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. | | 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. | | 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 ## 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__. 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,6 +201,9 @@ Bridge spotify:player:user1 "Me" [clientId="<your client id>", clientSecret="<yo
Things: Things:
device device1 "Device 1" [deviceName="<spotify device name>"] device device1 "Device 1" [deviceName="<spotify device name>"]
device device2 "Device 2" [deviceName="<spotify device name>"] device device2 "Device 2" [deviceName="<spotify device name>"]
Channels:
String : playlists [limit=50]
String : albumImageUrl [imageIndex=1]
} }
``` ```
@ -189,7 +219,7 @@ String spotifyTrackDuration "Track duration: [%s]" {channel="spotify:player:user
String spotifyTrackName "Track Name: [%s]" {channel="spotify:player:user1:trackName"} String spotifyTrackName "Track Name: [%s]" {channel="spotify:player:user1:trackName"}
String spotifyAlbumName "Album Name: [%s]" {channel="spotify:player:user1:albumName"} String spotifyAlbumName "Album Name: [%s]" {channel="spotify:player:user1:albumName"}
String spotifyArtistName "Artist Name: [%s]" {channel="spotify:player:user1:artistName"} String spotifyArtistName "Artist Name: [%s]" {channel="spotify:player:user1:artistName"}
Image spotifyAlbumImage "Album Art" {channel="spotify:player:user1:albumImage"} String spotifyAlbumImageUrl "Album Art" {channel="spotify:player:user1:albumImageUrl"}
String spotifyPlaylists "Playlists [%s]" {channel="spotify:player:user1:playlists"} String spotifyPlaylists "Playlists [%s]" {channel="spotify:player:user1:playlists"}
String spotifyPlayName "Playlist [%s]" {channel="spotify:player:user1:playlistName"} String spotifyPlayName "Playlist [%s]" {channel="spotify:player:user1:playlistName"}
@ -217,7 +247,7 @@ sitemap spotify label="Spotify Sitemap" {
Text item=spotifyTrackProgress label="Track progress: [%s]" Text item=spotifyTrackProgress label="Track progress: [%s]"
Text item=spotifyTrackDuration label="Track duration: [%s]" Text item=spotifyTrackDuration label="Track duration: [%s]"
Text item=spotifyTrackName label="Track Name: [%s]" Text item=spotifyTrackName label="Track Name: [%s]"
Image item=spotifyAlbumImage label="Album Art" Image item=spotifyAlbumImageUrl label="Album Art"
Text item=spotifyAlbumName label="Currently Played Album Name: [%s]" Text item=spotifyAlbumName label="Currently Played Album Name: [%s]"
Text item=spotifyArtistName label="Currently Played Artist Name: [%s]" Text item=spotifyArtistName label="Currently Played Artist Name: [%s]"
Selection item=spotifyPlaylists label="Playlist" icon="music" Selection item=spotifyPlaylists label="Playlist" icon="music"
@ -239,6 +269,14 @@ sitemap spotify label="Spotify Sitemap" {
} }
``` ```
spotify.rules
```
val spotifyActions = getActions("spotify", "spotify:player:user1")
// play the song
spotifyActions.play("spotify:track:4cOdK2wGLETKBW3PvgPWqT")
```
## Binding model and Spotify Web API ## Binding model and Spotify Web API
The model of the binding is such that the bridge acts as a player in the context of a specific user. The model of the binding is such that the bridge acts as a player in the context of a specific user.

View File

@ -55,6 +55,8 @@ public class SpotifyBindingConstants {
public static final String CHANNEL_PLAYLISTS = "playlists"; public static final String CHANNEL_PLAYLISTS = "playlists";
public static final String CHANNEL_PLAYLISTNAME = "playlistName"; public static final String CHANNEL_PLAYLISTNAME = "playlistName";
public static final String CHANNEL_PLAYLISTS_LIMIT = "limit";
public static final String CHANNEL_PLAYLISTS_OFFSET = "offset";
public static final String CHANNEL_PLAYED_TRACKID = "trackId"; public static final String CHANNEL_PLAYED_TRACKID = "trackId";
public static final String CHANNEL_PLAYED_TRACKURI = "trackUri"; public static final String CHANNEL_PLAYED_TRACKURI = "trackUri";
@ -76,6 +78,8 @@ public class SpotifyBindingConstants {
public static final String CHANNEL_PLAYED_ALBUMNAME = "albumName"; public static final String CHANNEL_PLAYED_ALBUMNAME = "albumName";
public static final String CHANNEL_PLAYED_ALBUMTYPE = "albumType"; public static final String CHANNEL_PLAYED_ALBUMTYPE = "albumType";
public static final String CHANNEL_PLAYED_ALBUMIMAGE = "albumImage"; public static final String CHANNEL_PLAYED_ALBUMIMAGE = "albumImage";
public static final String CHANNEL_PLAYED_ALBUMIMAGEURL = "albumImageUrl";
public static final String CHANNEL_CONFIG_IMAGE_INDEX = "imageIndex";
public static final String CHANNEL_PLAYED_ARTISTID = "artistId"; public static final String CHANNEL_PLAYED_ARTISTID = "artistId";
public static final String CHANNEL_PLAYED_ARTISTURI = "artistUri"; public static final String CHANNEL_PLAYED_ARTISTURI = "artistUri";

View File

@ -0,0 +1,114 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.spotify.internal.actions;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.spotify.internal.handler.SpotifyBridgeHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
/**
* Spotify Rule Actions.
*
* @author Hilbrand Bouwkamp - Initial contribution
*/
@ThingActionsScope(name = "spotify")
@NonNullByDefault
public class SpotifyActions implements ThingActions, ThingHandlerService {
private @Nullable ThingHandler handler;
/**
* Play a context uri (track or other) on the current active device (if null is passed for deviceID) or the given
* device at the given offset and/or position in milliseconds.
*
* @param actions Spotify Actions object.
* @param contextUri context uri (track or other)
* @param deviceId Id of the device to play on, or current device if given null
* @param offset Offset in the list, default 0.
* @param positionMs position in the track in milliseconds, default 0,
*/
@RuleAction(label = "@text/actions.play.label", description = "@text/actions.play.description")
public void play(
@ActionInput(name = "contextUri", label = "@text/actions.play.context_uri.label", description = "@text/actions.play.context_uri.description", type = "java.lang.String", required = true) String contextUri,
@ActionInput(name = "deviceId", label = "@text/actions.play.device_id.label", description = "@text/actions.play.device_id.description", type = "java.lang.String", defaultValue = "") @Nullable String deviceId,
@ActionInput(name = "offset", label = "@text/actions.play.offset.label", description = "@text/actions.play.offset.description", type = "java.lang.Integer", defaultValue = "0") final int offset,
@ActionInput(name = "positionMs", label = "@text/actions.play.positions_ms.label", description = "@text/actions.play.positions_ms.description", type = "java.lang.Integer", defaultValue = "0") final int positionMs) {
((SpotifyBridgeHandler) getThingHandler()).getSpotifyApi().playTrack(deviceId == null ? "" : deviceId,
contextUri, offset, positionMs);
}
/**
* Play a context uri (track or other) on the current active device.
*
* @param actions Spotify Actions object.
* @param contextUri context uri (track or other)
*/
public static void play(ThingActions actions, String contextUri) {
((SpotifyActions) actions).play(contextUri, null, 0, 0);
}
/**
* Play a context uri (track or other) on the current active device at the given offset and/or position in
* milliseconds.
*
* @param actions Spotify Actions object.
* @param contextUri context uri (track or other)
* @param offset Offset in the list, default 0.
* @param positionMs position in the track in milliseconds, default 0,
*/
public static void play(ThingActions actions, String contextUri, final int offset, final int positionMs) {
((SpotifyActions) actions).play(contextUri, null, positionMs, positionMs);
}
/**
* Play a context uri (track or other) on the given device.
*
* @param actions Spotify Actions object.
* @param contextUri context uri (track or other)
* @param deviceId Id of the device to play on, or current device if given null
*/
public static void play(ThingActions actions, String contextUri, @Nullable String deviceId) {
((SpotifyActions) actions).play(contextUri, deviceId, 0, 0);
}
/**
* Play a context uri (track or other) on the current active device (if null is passed for deviceID) or the given
* device at the given offset and/or position in milliseconds.
*
* @param actions Spotify Actions object.
* @param contextUri context uri (track or other)
* @param deviceId Id of the device to play on, or current device if given null
* @param offset Offset in the list, default 0.
* @param positionMs position in the track in milliseconds, default 0,
*/
public static void play(ThingActions actions, String contextUri, @Nullable String deviceId, final int offset,
final int positionMs) {
((SpotifyActions) actions).play(contextUri, deviceId, positionMs, positionMs);
}
@Override
public void setThingHandler(ThingHandler handler) {
this.handler = handler;
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
}

View File

@ -12,8 +12,11 @@
*/ */
package org.openhab.binding.spotify.internal.api; package org.openhab.binding.spotify.internal.api;
import static org.eclipse.jetty.http.HttpMethod.*; import static org.eclipse.jetty.http.HttpMethod.GET;
import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.*; import static org.eclipse.jetty.http.HttpMethod.POST;
import static org.eclipse.jetty.http.HttpMethod.PUT;
import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.SPOTIFY_API_PLAYER_URL;
import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.SPOTIFY_API_URL;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
@ -25,6 +28,7 @@ import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Request;
@ -48,6 +52,8 @@ import org.openhab.core.library.types.OnOffType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.gson.JsonSyntaxException;
/** /**
* Class to handle Spotify Web Api calls. * Class to handle Spotify Web Api calls.
* *
@ -62,8 +68,8 @@ public class SpotifyApi {
private static final char AMP = '&'; private static final char AMP = '&';
private static final char QSM = '?'; private static final char QSM = '?';
private static final CurrentlyPlayingContext EMPTY_CURRENTLYPLAYINGCONTEXT = new CurrentlyPlayingContext(); private static final CurrentlyPlayingContext EMPTY_CURRENTLYPLAYINGCONTEXT = new CurrentlyPlayingContext();
private static final String PLAY_TRACK_URIS = "{\"uris\":[%s],\"offset\":{\"position\":0}}"; private static final String PLAY_TRACK_URIS = "{\"uris\":[%s],\"offset\":{\"position\":%d},\"position_ms\":%d}";
private static final String PLAY_TRACK_CONTEXT_URI = "{\"context_uri\":\"%s\",\"offset\":{\"position\":0}}"; private static final String PLAY_TRACK_CONTEXT_URI = "{\"context_uri\":\"%s\",\"offset\":{\"position\":%d},\"position_ms\":%d}}";
private static final String TRANSFER_PLAY = "{\"device_ids\":[\"%s\"],\"play\":%b}"; private static final String TRANSFER_PLAY = "{\"device_ids\":[\"%s\"],\"play\":%b}";
private final Logger logger = LoggerFactory.getLogger(SpotifyApi.class); private final Logger logger = LoggerFactory.getLogger(SpotifyApi.class);
@ -87,9 +93,7 @@ public class SpotifyApi {
* @return Returns the Spotify user information * @return Returns the Spotify user information
*/ */
public Me getMe() { public Me getMe() {
final ContentResponse response = request(GET, SPOTIFY_API_URL, ""); return Objects.requireNonNull(request(GET, SPOTIFY_API_URL, "", Me.class));
return Objects.requireNonNull(ModelUtil.gsonInstance().fromJson(response.getContentAsString(), Me.class));
} }
/** /**
@ -98,17 +102,20 @@ public class SpotifyApi {
* *
* @param deviceId device to play on or empty if play on the active device * @param deviceId device to play on or empty if play on the active device
* @param trackId id of the track to play * @param trackId id of the track to play
* @param offset offset
* @param positionMs position in ms
*/ */
public void playTrack(String deviceId, String trackId) { public void playTrack(String deviceId, String trackId, int offset, int positionMs) {
final String url = "play" + optionalDeviceId(deviceId, QSM); final String url = "play" + optionalDeviceId(deviceId, QSM);
final String play; final String play;
if (trackId.contains(":track:")) { if (trackId.contains(":track:")) {
play = String.format(PLAY_TRACK_URIS, Arrays.asList(trackId.split(",")).stream().map(t -> '"' + t + '"') play = String.format(PLAY_TRACK_URIS,
.collect(Collectors.joining(","))); Arrays.asList(trackId.split(",")).stream().map(t -> '"' + t + '"').collect(Collectors.joining(",")),
offset, positionMs);
} else { } 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. * @param play if true transfers and starts to play, else transfers but pauses.
*/ */
public void transferPlay(String deviceId, boolean play) { 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. * active device.
* *
* @param deviceId device to set repeat state on or empty if set repeat on the 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) { public void setRepeatState(String deviceId, String repeateState) {
requestPlayer(PUT, String.format("repeat?state=%s", repeateState) + optionalDeviceId(deviceId, AMP)); 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 * @return Calls Spotify Api and returns the list of device or an empty list if nothing was returned
*/ */
public List<Device> getDevices() { public List<Device> getDevices() {
final ContentResponse response = requestPlayer(GET, "devices"); final Devices deviceList = requestPlayer(GET, "devices", "", Devices.class);
final Devices deviceList = ModelUtil.gsonInstance().fromJson(response.getContentAsString(), Devices.class);
return deviceList == null || deviceList.getDevices() == null ? Collections.emptyList() return deviceList == null || deviceList.getDevices() == null ? Collections.emptyList()
: deviceList.getDevices(); : deviceList.getDevices();
@ -218,9 +224,9 @@ public class SpotifyApi {
/** /**
* @return Returns the playlists of the user. * @return Returns the playlists of the user.
*/ */
public List<Playlist> getPlaylists() { public List<Playlist> getPlaylists(int offset, int limit) {
final ContentResponse response = request(GET, SPOTIFY_API_URL + "/playlists", ""); final Playlists playlists = request(GET, SPOTIFY_API_URL + "/playlists?offset" + offset + "&limit=" + limit, "",
final Playlists playlists = ModelUtil.gsonInstance().fromJson(response.getContentAsString(), Playlists.class); Playlists.class);
return playlists == null || playlists.getItems() == null ? Collections.emptyList() : playlists.getItems(); return playlists == null || playlists.getItems() == null ? Collections.emptyList() : playlists.getItems();
} }
@ -230,9 +236,7 @@ public class SpotifyApi {
* returned by Spotify * returned by Spotify
*/ */
public CurrentlyPlayingContext getPlayerInfo() { public CurrentlyPlayingContext getPlayerInfo() {
final ContentResponse response = requestPlayer(GET, ""); final CurrentlyPlayingContext context = requestPlayer(GET, "", "", CurrentlyPlayingContext.class);
final CurrentlyPlayingContext context = ModelUtil.gsonInstance().fromJson(response.getContentAsString(),
CurrentlyPlayingContext.class);
return context == null ? EMPTY_CURRENTLYPLAYINGCONTEXT : context; return context == null ? EMPTY_CURRENTLYPLAYINGCONTEXT : context;
} }
@ -242,11 +246,10 @@ public class SpotifyApi {
* Spotify. * Spotify.
* *
* @param method Http method to perform * @param method Http method to perform
* @param url url path to call to spotify * @param url url path to call to Spotify
* @return the response give by Spotify
*/ */
private ContentResponse requestPlayer(HttpMethod method, String url) { private void requestPlayer(HttpMethod method, String url) {
return requestPlayer(method, url, ""); requestPlayer(method, url, "", String.class);
} }
/** /**
@ -254,23 +257,42 @@ public class SpotifyApi {
* Spotify. * Spotify.
* *
* @param method Http method to perform * @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 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 * @return the response give by Spotify
*/ */
private ContentResponse requestPlayer(HttpMethod method, String url, String requestData) { private <T> @Nullable T requestPlayer(HttpMethod method, String url, String requestData, Class<T> clazz) {
return request(method, SPOTIFY_API_PLAYER_URL + (url.isEmpty() ? "" : ('/' + url)), requestData); return request(method, SPOTIFY_API_PLAYER_URL + (url.isEmpty() ? "" : ('/' + url)), requestData, clazz);
}
/**
* Parses the Spotify returned json.
*
* @param <T> 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 <T> @Nullable T fromJson(String content, Class<T> 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. * 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 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 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 * @return the response give by Spotify
*/ */
private ContentResponse request(HttpMethod method, String url, String requestData) { private <T> @Nullable T request(HttpMethod method, String url, String requestData, Class<T> clazz) {
logger.debug("Request: ({}) {} - {}", method, url, requestData); logger.debug("Request: ({}) {} - {}", method, url, requestData);
final Function<HttpClient, Request> call = httpClient -> httpClient.newRequest(url).method(method) final Function<HttpClient, Request> call = httpClient -> httpClient.newRequest(url).method(method)
.header("Accept", CONTENT_TYPE).content(new StringContentProvider(requestData), CONTENT_TYPE); .header("Accept", CONTENT_TYPE).content(new StringContentProvider(requestData), CONTENT_TYPE);
@ -280,11 +302,13 @@ public class SpotifyApi {
if (accessToken == null || accessToken.isEmpty()) { if (accessToken == null || accessToken.isEmpty()) {
throw new SpotifyAuthorizationException( throw new SpotifyAuthorizationException(
"No spotify accesstoken. Did you authorize spotify via /connectspotify ?"); "No Spotify accesstoken. Did you authorize Spotify via /connectspotify ?");
} else { } 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); throw new SpotifyException(e.getMessage(), e);
} catch (OAuthException | OAuthResponseException e) { } catch (OAuthException | OAuthResponseException e) {
throw new SpotifyAuthorizationException(e.getMessage(), e); throw new SpotifyAuthorizationException(e.getMessage(), e);
@ -295,7 +319,7 @@ public class SpotifyApi {
throws OAuthException, IOException, OAuthResponseException { throws OAuthException, IOException, OAuthResponseException {
try { try {
return connector.request(call, BEARER + accessToken); return connector.request(call, BEARER + accessToken);
} catch (SpotifyTokenExpiredException e) { } catch (final SpotifyTokenExpiredException e) {
// Retry with new access token // Retry with new access token
return connector.request(call, BEARER + oAuthClientService.refreshToken().getAccessToken()); return connector.request(call, BEARER + oAuthClientService.refreshToken().getAccessToken());
} }

View File

@ -12,8 +12,10 @@
*/ */
package org.openhab.binding.spotify.internal.discovery; 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.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -55,6 +57,8 @@ public class SpotifyDeviceDiscoveryService extends AbstractDiscoveryService
private static final int DISCOVERY_TIME_SECONDS = 10; private static final int DISCOVERY_TIME_SECONDS = 10;
// Check every minute for new devices // Check every minute for new devices
private static final long BACKGROUND_SCAN_REFRESH_MINUTES = 1; 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); private final Logger logger = LoggerFactory.getLogger(SpotifyDeviceDiscoveryService.class);
@ -74,7 +78,7 @@ public class SpotifyDeviceDiscoveryService extends AbstractDiscoveryService
@Override @Override
public void activate() { public void activate() {
Map<String, Object> properties = new HashMap<>(); final Map<String, Object> properties = new HashMap<>();
properties.put(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY, Boolean.TRUE); properties.put(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY, Boolean.TRUE);
super.activate(properties); super.activate(properties);
} }
@ -120,22 +124,22 @@ public class SpotifyDeviceDiscoveryService extends AbstractDiscoveryService
logger.debug("Starting Spotify Device discovery for bridge {}", bridgeUID); logger.debug("Starting Spotify Device discovery for bridge {}", bridgeUID);
try { try {
bridgeHandler.listDevices().forEach(this::thingDiscovered); bridgeHandler.listDevices().forEach(this::thingDiscovered);
} catch (RuntimeException e) { } catch (final RuntimeException e) {
logger.debug("Finding devices failed with message: {}", e.getMessage(), e); logger.debug("Finding devices failed with message: {}", e.getMessage(), e);
} }
} }
} }
private void thingDiscovered(Device device) { private void thingDiscovered(Device device) {
Map<String, Object> properties = new HashMap<>(); final Map<String, Object> properties = new HashMap<>();
properties.put(PROPERTY_SPOTIFY_DEVICE_NAME, device.getName()); 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)); 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) .withProperties(properties).withRepresentationProperty(PROPERTY_SPOTIFY_DEVICE_NAME)
.withLabel(device.getName()).build(); .withTTL(TTL_SECONDS).withLabel(device.getName()).build();
thingDiscovered(discoveryResult); thingDiscovered(discoveryResult);
} }

View File

@ -12,9 +12,53 @@
*/ */
package org.openhab.binding.spotify.internal.handler; 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.io.IOException;
import java.math.BigDecimal;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.Duration; import java.time.Duration;
import java.util.Collection; import java.util.Collection;
@ -31,6 +75,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.spotify.internal.SpotifyAccountHandler; import org.openhab.binding.spotify.internal.SpotifyAccountHandler;
import org.openhab.binding.spotify.internal.SpotifyBridgeConfiguration; 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.SpotifyApi;
import org.openhab.binding.spotify.internal.api.exception.SpotifyAuthorizationException; import org.openhab.binding.spotify.internal.api.exception.SpotifyAuthorizationException;
import org.openhab.binding.spotify.internal.api.exception.SpotifyException; 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 State lastTrackId = StringType.EMPTY;
private volatile String lastKnownDeviceId = ""; private volatile String lastKnownDeviceId = "";
private volatile boolean lastKnownDeviceActive; private volatile boolean lastKnownDeviceActive;
private int imageChannelAlbumImageIndex;
private int imageChannelAlbumImageUrlIndex;
public SpotifyBridgeHandler(Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient, public SpotifyBridgeHandler(Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient,
SpotifyDynamicStateDescriptionProvider spotifyDynamicStateDescriptionProvider) { SpotifyDynamicStateDescriptionProvider spotifyDynamicStateDescriptionProvider) {
@ -146,7 +193,7 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler
@Override @Override
public Collection<Class<? extends ThingHandlerService>> getServices() { public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(SpotifyDeviceDiscoveryService.class); return List.of(SpotifyActions.class, SpotifyDeviceDiscoveryService.class);
} }
@Override @Override
@ -172,7 +219,7 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler
&& handleCommand.handleCommand(channelUID, command, lastKnownDeviceActive, lastKnownDeviceId)) { && handleCommand.handleCommand(channelUID, command, lastKnownDeviceActive, lastKnownDeviceId)) {
scheduler.schedule(this::scheduledPollingRestart, POLL_DELAY_AFTER_COMMAND_S, TimeUnit.SECONDS); 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); logger.debug("Handle Spotify command failed: ", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
} }
@ -227,7 +274,7 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler
} }
@Nullable @Nullable
SpotifyApi getSpotifyApi() { public SpotifyApi getSpotifyApi() {
return spotifyApi; return spotifyApi;
} }
@ -240,7 +287,7 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler
public String formatAuthorizationUrl(String redirectUri) { public String formatAuthorizationUrl(String redirectUri) {
try { try {
return oAuthService.getAuthorizationUrl(redirectUri, null, thing.getUID().getAsString()); return oAuthService.getAuthorizationUrl(redirectUri, null, thing.getUID().getAsString());
} catch (OAuthException e) { } catch (final OAuthException e) {
logger.debug("Error constructing AuthorizationUrl: ", e); logger.debug("Error constructing AuthorizationUrl: ", e);
return ""; return "";
} }
@ -259,7 +306,7 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler
} catch (RuntimeException | OAuthException | IOException e) { } catch (RuntimeException | OAuthException | IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
throw new SpotifyException(e.getMessage(), e); throw new SpotifyException(e.getMessage(), e);
} catch (OAuthResponseException e) { } catch (final OAuthResponseException e) {
throw new SpotifyAuthorizationException(e.getMessage(), e); throw new SpotifyAuthorizationException(e.getMessage(), e);
} }
} }
@ -287,9 +334,13 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler
oAuthService.addAccessTokenRefreshListener(SpotifyBridgeHandler.this); oAuthService.addAccessTokenRefreshListener(SpotifyBridgeHandler.this);
spotifyApi = new SpotifyApi(oAuthService, scheduler, httpClient); spotifyApi = new SpotifyApi(oAuthService, scheduler, httpClient);
handleCommand = new SpotifyHandleCommands(spotifyApi); handleCommand = new SpotifyHandleCommands(spotifyApi);
playingContextCache = new ExpiringCache<>(configuration.refreshPeriod, spotifyApi::getPlayerInfo); final Duration expiringPeriod = Duration.ofSeconds(configuration.refreshPeriod);
playlistCache = new ExpiringCache<>(POLL_PLAY_LIST_HOURS, spotifyApi::getPlaylists);
devicesCache = new ExpiringCache<>(configuration.refreshPeriod, spotifyApi::getDevices); 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. // Start with update status by calling Spotify. If no credentials available no polling should be started.
scheduler.execute(() -> { scheduler.execute(() -> {
@ -297,6 +348,16 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler
startPolling(); 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 @Override
@ -318,7 +379,7 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler
if (pollStatus() && pollingNotRunning) { if (pollStatus() && pollingNotRunning) {
startPolling(); startPolling();
} }
} catch (RuntimeException e) { } catch (final RuntimeException e) {
logger.debug("Restarting polling failed: ", 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; final CurrentlyPlayingContext playingContext = pc == null ? EMPTY_CURRENTLY_PLAYING_CONTEXT : pc;
// Collect devices and populate selection with available devices. // Collect devices and populate selection with available devices.
if (hasPlayData || hasAnyDeviceStatusUnknown()) { if (hasPlayData) {
final List<Device> ld = devicesCache.getValue(); final List<Device> ld = devicesCache.getValue();
final List<Device> devices = ld == null ? Collections.emptyList() : ld; final List<Device> devices = ld == null ? Collections.emptyList() : ld;
spotifyDynamicStateDescriptionProvider.setDevices(devicesChannelUID, devices); spotifyDynamicStateDescriptionProvider.setDevices(devicesChannelUID, devices);
@ -381,17 +442,17 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler
} }
updateStatus(ThingStatus.ONLINE); updateStatus(ThingStatus.ONLINE);
return true; return true;
} catch (SpotifyAuthorizationException e) { } catch (final SpotifyAuthorizationException e) {
logger.debug("Authorization error during polling: ", e); logger.debug("Authorization error during polling: ", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
cancelSchedulers(); cancelSchedulers();
devicesCache.invalidateValue(); devicesCache.invalidateValue();
} catch (SpotifyException e) { } catch (final SpotifyException e) {
logger.info("Spotify returned an error during polling: {}", e.getMessage()); logger.info("Spotify returned an error during polling: {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, 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. // 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); 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()); .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. * Update the player data.
* *
@ -651,22 +706,32 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler
* @param album album data * @param album album data
*/ */
public void updateAlbumImage(Album album) { public void updateAlbumImage(Album album) {
final Channel channel = thing.getChannel(CHANNEL_PLAYED_ALBUMIMAGE); final Channel imageChannel = thing.getChannel(CHANNEL_PLAYED_ALBUMIMAGE);
final List<Image> images = album.getImages(); final List<Image> images = album.getImages();
if (channel != null && images != null && !images.isEmpty()) { // Update album image url channel
final String imageUrl = images.get(0).getUrl(); 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 // Download the cover art in a different thread to not delay the other operations
lastAlbumImageUrl = imageUrl == null ? "" : imageUrl; lastAlbumImageUrl = albumImageUrl;
refreshAlbumImage(channel.getUID()); refreshAlbumImage(imageChannel.getUID());
} } // else album image still the same so nothing to do
} else { } else {
lastAlbumImageUrl = "";
updateChannelState(CHANNEL_PLAYED_ALBUMIMAGE, UnDefType.UNDEF); updateChannelState(CHANNEL_PLAYED_ALBUMIMAGE, UnDefType.UNDEF);
} }
} }
private @Nullable String albumUrl(@Nullable List<Image> 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 * Refreshes the image asynchronously, but only downloads the image if the channel is linked to avoid
* unnecessary downloading of the image. * unnecessary downloading of the image.
@ -686,7 +751,7 @@ public class SpotifyBridgeHandler extends BaseBridgeHandler
final RawType image = HttpUtil.downloadImage(imageUrl, true, MAX_IMAGE_SIZE); final RawType image = HttpUtil.downloadImage(imageUrl, true, MAX_IMAGE_SIZE);
updateChannelState(CHANNEL_PLAYED_ALBUMIMAGE, image == null ? UnDefType.UNDEF : image); 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); logger.debug("Async call to refresh Album image failed: ", e);
} }
} }

View File

@ -123,7 +123,7 @@ class SpotifyHandleCommands {
case CHANNEL_TRACKPLAY: case CHANNEL_TRACKPLAY:
case CHANNEL_PLAYLISTS: case CHANNEL_PLAYLISTS:
if (command instanceof StringType) { if (command instanceof StringType) {
spotifyApi.playTrack(deviceId, command.toString()); spotifyApi.playTrack(deviceId, command.toString(), 0, 0);
commandRun = true; commandRun = true;
} }
break; break;
@ -132,7 +132,7 @@ class SpotifyHandleCommands {
final String newName = command.toString(); final String newName = command.toString();
playlists.stream().filter(pl -> pl.getName().equals(newName)).findFirst() 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; commandRun = true;
} }
break; break;

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="spotify:config:playlists">
<parameter name="limit" type="integer" min="1" max="50">
<label>Limit</label>
<description>The maximum number of playlists to return</description>
<default>20</default>
</parameter>
<parameter name="offset" type="integer" min="0" max="100000">
<label>Offset</label>
<description>The index of the first playlist to return</description>
<default>0</default>
</parameter>
</config-description>
<config-description uri="spotify:config:album-image">
<parameter name="imageIndex" type="integer" min="0" max="2">
<label>Image Index</label>
<description>Index in list of to select image to show</description>
<default>0</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -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)

View File

@ -48,6 +48,7 @@
<channel id="albumHref" typeId="currentlyPlayedAlbumHref"/> <channel id="albumHref" typeId="currentlyPlayedAlbumHref"/>
<channel id="albumType" typeId="currentlyPlayedAlbumType"/> <channel id="albumType" typeId="currentlyPlayedAlbumType"/>
<channel id="albumImage" typeId="currentlyPlayedAlbumImage"/> <channel id="albumImage" typeId="currentlyPlayedAlbumImage"/>
<channel id="albumImageUrl" typeId="currentlyPlayedAlbumImage"/>
<channel id="albumName" typeId="currentlyPlayedAlbumName"/> <channel id="albumName" typeId="currentlyPlayedAlbumName"/>
<channel id="artistId" typeId="currentlyPlayedArtistId"/> <channel id="artistId" typeId="currentlyPlayedArtistId"/>
@ -78,10 +79,8 @@
<default>10</default> <default>10</default>
<label>Connect Refresh Period (seconds)</label> <label>Connect Refresh Period (seconds)</label>
<description>This is the frequency of the polling requests to the Spotify Connect Web API. There are limits to the <description>This is the frequency of the polling requests to the Spotify Connect Web API. There are limits to the
number of requests number of requests that can be sent to the Web API. The more often you poll, the better status updates - at the
that can be sent to the Web API. The more often you poll, the better status updates - at the risk risk of running out of your request quota.</description>
of running out of
your request quota.</description>
</parameter> </parameter>
</config-description> </config-description>
</bridge-type> </bridge-type>
@ -159,6 +158,7 @@
<item-type>String</item-type> <item-type>String</item-type>
<label>Playlists</label> <label>Playlists</label>
<description>List of the users playlists</description> <description>List of the users playlists</description>
<config-description-ref uri="spotify:config:playlists"/>
</channel-type> </channel-type>
<channel-type id="playlistName"> <channel-type id="playlistName">
@ -293,6 +293,14 @@
<label>Album Image</label> <label>Album Image</label>
<description>The cover art for the album in widest size</description> <description>The cover art for the album in widest size</description>
<state readOnly="true"/> <state readOnly="true"/>
<config-description-ref uri="spotify:config:album-image"/>
</channel-type>
<channel-type id="currentlyPlayedAlbumImageUrl">
<item-type>String</item-type>
<label>Album Image URL</label>
<description>The URL to the cover art for the album in widest size</description>
<state readOnly="true"/>
<config-description-ref uri="spotify:config:album-image"/>
</channel-type> </channel-type>
<channel-type id="currentlyPlayedArtistId" advanced="true"> <channel-type id="currentlyPlayedArtistId" advanced="true">