mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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:
parent
3fc968a980
commit
196e4e2210
@ -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="<your client id>", clientSecret="<yo
|
||||
Things:
|
||||
device device1 "Device 1" [deviceName="<spotify device name>"]
|
||||
device device2 "Device 2" [deviceName="<spotify device name>"]
|
||||
Channels:
|
||||
String : playlists [limit=50]
|
||||
String : albumImageUrl [imageIndex=1]
|
||||
}
|
||||
```
|
||||
|
||||
spotify.items:
|
||||
|
||||
```
|
||||
Player spotifyTrackPlayer "Player" {channel="spotify:player:user1:trackPlayer"}
|
||||
String spotifyDevices "Active device [%s]" {channel="spotify:player:user1:devices"}
|
||||
Switch spotifyDeviceShuffle "Shuffle mode" {channel="spotify:player:user1:deviceShuffle"}
|
||||
String spotifyTrackRepeat "Repeat mode: [%s]" {channel="spotify:player:user1:trackRepeat"}
|
||||
String spotifyTrackProgress "Track progress: [%s]" {channel="spotify:player:user1:trackProgress"}
|
||||
String spotifyTrackDuration "Track duration: [%s]" {channel="spotify:player:user1:trackDuration"}
|
||||
String spotifyTrackName "Track Name: [%s]" {channel="spotify:player:user1:trackName"}
|
||||
String spotifyAlbumName "Album Name: [%s]" {channel="spotify:player:user1:albumName"}
|
||||
String spotifyArtistName "Artist Name: [%s]" {channel="spotify:player:user1:artistName"}
|
||||
Image spotifyAlbumImage "Album Art" {channel="spotify:player:user1:albumImage"}
|
||||
String spotifyPlaylists "Playlists [%s]" {channel="spotify:player:user1:playlists"}
|
||||
String spotifyPlayName "Playlist [%s]" {channel="spotify:player:user1:playlistName"}
|
||||
Player spotifyTrackPlayer "Player" {channel="spotify:player:user1:trackPlayer"}
|
||||
String spotifyDevices "Active device [%s]" {channel="spotify:player:user1:devices"}
|
||||
Switch spotifyDeviceShuffle "Shuffle mode" {channel="spotify:player:user1:deviceShuffle"}
|
||||
String spotifyTrackRepeat "Repeat mode: [%s]" {channel="spotify:player:user1:trackRepeat"}
|
||||
String spotifyTrackProgress "Track progress: [%s]" {channel="spotify:player:user1:trackProgress"}
|
||||
String spotifyTrackDuration "Track duration: [%s]" {channel="spotify:player:user1:trackDuration"}
|
||||
String spotifyTrackName "Track Name: [%s]" {channel="spotify:player:user1:trackName"}
|
||||
String spotifyAlbumName "Album Name: [%s]" {channel="spotify:player:user1:albumName"}
|
||||
String spotifyArtistName "Artist Name: [%s]" {channel="spotify:player:user1:artistName"}
|
||||
String spotifyAlbumImageUrl "Album Art" {channel="spotify:player:user1:albumImageUrl"}
|
||||
String spotifyPlaylists "Playlists [%s]" {channel="spotify:player:user1:playlists"}
|
||||
String spotifyPlayName "Playlist [%s]" {channel="spotify:player:user1:playlistName"}
|
||||
|
||||
String device1DeviceName {channel="spotify:device:user1:device1:deviceName"}
|
||||
Player device1Player {channel="spotify:device:user1:device1:devicePlayer"}
|
||||
@ -217,7 +247,7 @@ sitemap spotify label="Spotify Sitemap" {
|
||||
Text item=spotifyTrackProgress label="Track progress: [%s]"
|
||||
Text item=spotifyTrackDuration label="Track duration: [%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=spotifyArtistName label="Currently Played Artist Name: [%s]"
|
||||
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
|
||||
|
||||
The model of the binding is such that the bridge acts as a player in the context of a specific user.
|
||||
|
@ -55,6 +55,8 @@ public class SpotifyBindingConstants {
|
||||
|
||||
public static final String CHANNEL_PLAYLISTS = "playlists";
|
||||
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_TRACKURI = "trackUri";
|
||||
@ -76,6 +78,8 @@ public class SpotifyBindingConstants {
|
||||
public static final String CHANNEL_PLAYED_ALBUMNAME = "albumName";
|
||||
public static final String CHANNEL_PLAYED_ALBUMTYPE = "albumType";
|
||||
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_ARTISTURI = "artistUri";
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -12,8 +12,11 @@
|
||||
*/
|
||||
package org.openhab.binding.spotify.internal.api;
|
||||
|
||||
import static org.eclipse.jetty.http.HttpMethod.*;
|
||||
import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.*;
|
||||
import static org.eclipse.jetty.http.HttpMethod.GET;
|
||||
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.util.Arrays;
|
||||
@ -25,6 +28,7 @@ import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
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.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* Class to handle Spotify Web Api calls.
|
||||
*
|
||||
@ -62,8 +68,8 @@ public class SpotifyApi {
|
||||
private static final char AMP = '&';
|
||||
private static final char QSM = '?';
|
||||
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_CONTEXT_URI = "{\"context_uri\":\"%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\":%d},\"position_ms\":%d}}";
|
||||
private static final String TRANSFER_PLAY = "{\"device_ids\":[\"%s\"],\"play\":%b}";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SpotifyApi.class);
|
||||
@ -87,9 +93,7 @@ public class SpotifyApi {
|
||||
* @return Returns the Spotify user information
|
||||
*/
|
||||
public Me getMe() {
|
||||
final ContentResponse response = request(GET, SPOTIFY_API_URL, "");
|
||||
|
||||
return Objects.requireNonNull(ModelUtil.gsonInstance().fromJson(response.getContentAsString(), Me.class));
|
||||
return Objects.requireNonNull(request(GET, SPOTIFY_API_URL, "", Me.class));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -98,17 +102,20 @@ public class SpotifyApi {
|
||||
*
|
||||
* @param deviceId device to play on or empty if play on the active device
|
||||
* @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 play;
|
||||
if (trackId.contains(":track:")) {
|
||||
play = String.format(PLAY_TRACK_URIS, Arrays.asList(trackId.split(",")).stream().map(t -> '"' + 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<Device> 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<Playlist> getPlaylists() {
|
||||
final ContentResponse response = request(GET, SPOTIFY_API_URL + "/playlists", "");
|
||||
final Playlists playlists = ModelUtil.gsonInstance().fromJson(response.getContentAsString(), Playlists.class);
|
||||
public List<Playlist> 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 <T> @Nullable T requestPlayer(HttpMethod method, String url, String requestData, Class<T> clazz) {
|
||||
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.
|
||||
*
|
||||
* @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 <T> @Nullable T request(HttpMethod method, String url, String requestData, Class<T> clazz) {
|
||||
logger.debug("Request: ({}) {} - {}", method, url, requestData);
|
||||
final Function<HttpClient, Request> 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());
|
||||
}
|
||||
|
@ -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<String, Object> properties = new HashMap<>();
|
||||
final Map<String, Object> 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<String, Object> properties = new HashMap<>();
|
||||
final Map<String, Object> 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);
|
||||
}
|
||||
|
@ -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<Class<? extends ThingHandlerService>> 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<Device> ld = devicesCache.getValue();
|
||||
final List<Device> 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<Image> 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<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
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
@ -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)
|
@ -48,6 +48,7 @@
|
||||
<channel id="albumHref" typeId="currentlyPlayedAlbumHref"/>
|
||||
<channel id="albumType" typeId="currentlyPlayedAlbumType"/>
|
||||
<channel id="albumImage" typeId="currentlyPlayedAlbumImage"/>
|
||||
<channel id="albumImageUrl" typeId="currentlyPlayedAlbumImage"/>
|
||||
<channel id="albumName" typeId="currentlyPlayedAlbumName"/>
|
||||
|
||||
<channel id="artistId" typeId="currentlyPlayedArtistId"/>
|
||||
@ -78,10 +79,8 @@
|
||||
<default>10</default>
|
||||
<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
|
||||
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.</description>
|
||||
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.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
@ -159,6 +158,7 @@
|
||||
<item-type>String</item-type>
|
||||
<label>Playlists</label>
|
||||
<description>List of the users playlists</description>
|
||||
<config-description-ref uri="spotify:config:playlists"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="playlistName">
|
||||
@ -293,6 +293,14 @@
|
||||
<label>Album Image</label>
|
||||
<description>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 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 id="currentlyPlayedArtistId" advanced="true">
|
||||
|
Loading…
Reference in New Issue
Block a user