Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4175e3a
feature-media-spotify : divide branch feature-media to separate branches
lo92fr Apr 12, 2025
d1b6a58
add forget files
lo92fr Apr 12, 2025
a1b9077
fix for Isiepiel review of 8/04
lo92fr Apr 12, 2025
9b4fc2b
spotless:apply
lo92fr Apr 12, 2025
62b65c7
add support for MediaSink
lo92fr Jun 18, 2025
8f6f998
add support for enqueue track
lo92fr Jun 19, 2025
52d1137
add binding info to media sink dto
lo92fr Jun 19, 2025
31de504
introduce extended MediaType state on Player item to handle complex s…
lo92fr Jun 21, 2025
f6bd212
fix compilation errors
lo92fr Jun 22, 2025
bfe7a45
implements paging and lazy loading on MediaCollection (for now only t…
lo92fr Jun 26, 2025
0c7f464
start review spotify plugin to handle paging
lo92fr Jun 27, 2025
d55801a
review spotify plugin to handle paging
lo92fr Jun 27, 2025
89ff1a4
start handling search functionnality
lo92fr Jun 28, 2025
04cab03
refactor SpotifyBridgeHandler to simplify code for media entry managment
lo92fr Jun 28, 2025
93703fb
refactor SpotifyBridgeHandler to simplify code for media entry managment
lo92fr Jun 28, 2025
0000b10
first working version for searchbar (only working currently with spot…
lo92fr Jun 28, 2025
2b0a3c6
first working version of global player widget
lo92fr Jul 2, 2025
c723334
temp fix to prevent hitting quota getting to many time playlist
lo92fr Jul 2, 2025
233a3fc
add handling of volume change, track duration, and artUri
lo92fr Jul 2, 2025
edfbf69
review search functionnalites (WIP)
lo92fr Jul 3, 2025
cf4994b
code cleanup : introduce MediaCommandType & MediaStateType
lo92fr Jul 3, 2025
21a7860
move to openhab 5.1.0
lo92fr Jul 27, 2025
91f9e11
add getStreamUri signature from mediaService
lo92fr Oct 13, 2025
f1ad02b
review spotify refresh period
lo92fr Oct 15, 2025
fd38f79
add primary support for MediaQueue
lo92fr Oct 19, 2025
f8e3401
code refactoring to remove duplicate code between addon that use medi…
lo92fr Oct 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion bundles/org.openhab.binding.spotify/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>5.1.0-SNAPSHOT</version>
</parent>
</parent>

<artifactId>org.openhab.binding.spotify</artifactId>

<name>openHAB Add-ons :: Bundles :: Spotify Binding</name>

<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.media</artifactId>
<version>5.1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,13 @@ public class SpotifyBindingConstants {
/**
* Spotify scopes needed by this binding to work.
*/
public static final String SPOTIFY_SCOPES = Stream.of("user-read-playback-state", "user-modify-playback-state",
"playlist-read-private", "playlist-read-collaborative").collect(Collectors.joining(" "));
public static final String SPOTIFY_SCOPES = Stream.of("ugc-image-upload", "user-read-playback-state",
"user-modify-playback-state", "user-read-currently-playing", "app-remote-control streaming",
"playlist-read-private", "playlist-read-collaborative", "playlist-modify-private", "playlist-modify-public",
"user-follow-modify", "user-follow-read", "user-read-playback-position", "user-top-read",
"user-read-recently-played", "user-library-modify", "user-library-read", "user-read-email",
"user-read-private").collect(Collectors.joining(" "));
public static final String SPOTIFY_API_BASE_URL = "https://api.spotify.com/v1";
public static final String SPOTIFY_API_URL = "https://api.spotify.com/v1/me";
public static final String SPOTIFY_API_PLAYER_URL = SPOTIFY_API_URL + "/player";

Expand All @@ -42,7 +47,8 @@ public class SpotifyBindingConstants {
public static final String SPOTIFY_IMG_ALIAS = "/img";

// List of all Thing Type UIDs
private static final String BINDING_ID = "spotify";
public static final String BINDING_ID = "spotify";
public static final String BINDING_LABEL = "Spotify";
public static final ThingTypeUID THING_TYPE_PLAYER = new ThingTypeUID(BINDING_ID, "player");
public static final ThingTypeUID THING_TYPE_DEVICE = new ThingTypeUID(BINDING_ID, "device");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.openhab.binding.spotify.internal.handler.SpotifyDynamicStateDescriptionProvider;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.media.MediaService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
Expand All @@ -46,14 +47,17 @@ public class SpotifyHandlerFactory extends BaseThingHandlerFactory {
private final HttpClient httpClient;
private final SpotifyAuthService authService;
private final SpotifyDynamicStateDescriptionProvider spotifyDynamicStateDescriptionProvider;
private final MediaService mediaService;

@Activate
public SpotifyHandlerFactory(@Reference OAuthFactory oAuthFactory,
@Reference final HttpClientFactory httpClientFactory, @Reference SpotifyAuthService authService,
@Reference SpotifyDynamicStateDescriptionProvider spotifyDynamicStateDescriptionProvider) {
@Reference SpotifyDynamicStateDescriptionProvider spotifyDynamicStateDescriptionProvider,
@Reference MediaService mediaService) {
this.oAuthFactory = oAuthFactory;
this.httpClient = httpClientFactory.getCommonHttpClient();
this.authService = authService;
this.mediaService = mediaService;
this.spotifyDynamicStateDescriptionProvider = spotifyDynamicStateDescriptionProvider;
}

Expand All @@ -69,7 +73,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {

if (SpotifyBindingConstants.THING_TYPE_PLAYER.equals(thingTypeUID)) {
final SpotifyBridgeHandler handler = new SpotifyBridgeHandler((Bridge) thing, oAuthFactory, httpClient,
spotifyDynamicStateDescriptionProvider);
spotifyDynamicStateDescriptionProvider, mediaService);
authService.addSpotifyAccountHandler(handler);
return handler;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@
*/
package org.openhab.binding.spotify.internal.api;

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 static org.eclipse.jetty.http.HttpMethod.*;
import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
Expand All @@ -37,13 +35,32 @@
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.SpotifyTokenExpiredException;
import org.openhab.binding.spotify.internal.api.model.AddedShow;
import org.openhab.binding.spotify.internal.api.model.AddedShows;
import org.openhab.binding.spotify.internal.api.model.Album;
import org.openhab.binding.spotify.internal.api.model.Albums;
import org.openhab.binding.spotify.internal.api.model.ApiSearchResult;
import org.openhab.binding.spotify.internal.api.model.Artist;
import org.openhab.binding.spotify.internal.api.model.Artists;
import org.openhab.binding.spotify.internal.api.model.Categorie;
import org.openhab.binding.spotify.internal.api.model.CategoriesResult;
import org.openhab.binding.spotify.internal.api.model.CurrentPlay;
import org.openhab.binding.spotify.internal.api.model.CurrentlyPlayingContext;
import org.openhab.binding.spotify.internal.api.model.Device;
import org.openhab.binding.spotify.internal.api.model.Devices;
import org.openhab.binding.spotify.internal.api.model.FollowedArtists;
import org.openhab.binding.spotify.internal.api.model.Me;
import org.openhab.binding.spotify.internal.api.model.ModelUtil;
import org.openhab.binding.spotify.internal.api.model.NewReleases;
import org.openhab.binding.spotify.internal.api.model.Playlist;
import org.openhab.binding.spotify.internal.api.model.Playlists;
import org.openhab.binding.spotify.internal.api.model.SavedAlbum;
import org.openhab.binding.spotify.internal.api.model.SavedAlbums;
import org.openhab.binding.spotify.internal.api.model.Show;
import org.openhab.binding.spotify.internal.api.model.Track;
import org.openhab.binding.spotify.internal.api.model.Tracks;
import org.openhab.binding.spotify.internal.api.model.UserTrackEntries;
import org.openhab.binding.spotify.internal.api.model.UserTrackEntry;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
Expand All @@ -70,6 +87,7 @@ public class SpotifyApi {
private static final CurrentlyPlayingContext EMPTY_CURRENTLYPLAYINGCONTEXT = new CurrentlyPlayingContext();
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 ENQUEUE_URI = "uri=%s";
private static final String TRANSFER_PLAY = "{\"device_ids\":[\"%s\"],\"play\":%b}";

private final Logger logger = LoggerFactory.getLogger(SpotifyApi.class);
Expand Down Expand Up @@ -119,6 +137,22 @@ public void playTrack(String deviceId, String trackId, int offset, int positionM
requestPlayer(PUT, url, play, String.class);
}

/**
* Call Spotify Api to play the given track on the given device. If the device id is empty it will be played 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 offset offset
* @param positionMs position in ms
*/
public void queueTrack(String deviceId, String trackId, int offset, int positionMs) {
String url = "queue";
url = url + "?uri=" + trackId;
url = url + optionalDeviceId(deviceId, QSM);
requestPlayer(POST, url, "", String.class);
}

/**
* Call Spotify Api to start playing. If the device id is empty it will start play of the active device.
*
Expand Down Expand Up @@ -225,13 +259,185 @@ public List<Device> getDevices() {
/**
* @return Returns the playlists of the user.
*/
public List<Playlist> getPlaylists(int offset, int limit) {
final Playlists playlists = request(GET, SPOTIFY_API_URL + "/playlists?offset" + offset + "&limit=" + limit, "",
Playlists.class);
public @Nullable CurrentPlay getCurrentPlaylist(long offset, long limit) {
final CurrentPlay currentPlay = request(GET, SPOTIFY_API_URL + "/player/queue", "", CurrentPlay.class);
return currentPlay;
}

/**
* @return Returns the playlists of the user.
*/
public List<Playlist> getPlaylists(long offset, long 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();
}

/**
* @return Returns the albums of the user.
*/
public List<Album> getAlbums(long offset, long limit) {
final SavedAlbums savedAlbums = request(GET, SPOTIFY_API_URL + "/albums?offset" + offset + "&limit=" + limit,
"", SavedAlbums.class);

List<Album> albums = new ArrayList<Album>();
if (savedAlbums != null) {
for (SavedAlbum savedAlbum : savedAlbums.getItems()) {
albums.add(savedAlbum.album);
}
}
return albums;
}

/**
* @return Returns the albums of the user.
*/
public List<Album> getNewReleases(long offset, long limit) {
final NewReleases newReleases = request(GET,
SPOTIFY_API_BASE_URL + "/browse/new-releases?offset" + offset + "&limit=" + limit, "",
NewReleases.class);

return newReleases == null || newReleases.albums.getItems() == null ? Collections.emptyList()
: newReleases.albums.getItems();
}

/**
* @return Returns an album
*/
public @Nullable Album getAlbum(String albumId) {
final Album album = request(GET, SPOTIFY_API_BASE_URL + "/albums/" + albumId, "", Album.class);

return album;
}

/**
* @return Returns the artists of the user.
*/
public List<Artist> getArtists(long offset, long limit) {
final FollowedArtists followedArtists = request(GET,
SPOTIFY_API_URL + "/following?type=artist&offset" + offset + "&limit=" + limit, "",
FollowedArtists.class);

return followedArtists == null || followedArtists.getArtists() == null ? Collections.emptyList()
: followedArtists.getArtists().getItems();
}

/**
* @return Returns the artists of the user.
*/
public List<Categorie> getCategories(long offset, long limit) {
final CategoriesResult categoriesRes = request(GET,
SPOTIFY_API_BASE_URL + "/browse/categories?offset" + offset + "&limit=" + limit, "",
CategoriesResult.class);

return categoriesRes == null || categoriesRes.categories.getItems() == null ? Collections.emptyList()
: categoriesRes.categories.getItems();
}

/**
* @return Returns the artists of the user.
*/
public List<Album> getArtistAlbums(String artistId) {
final Albums albums = request(GET, SPOTIFY_API_BASE_URL + "/artists/" + artistId + "/albums", "", Albums.class);

return albums == null || albums.getItems() == null ? Collections.emptyList() : albums.getItems();
}

/**
* @return Returns the artists of the user.
*/
public List<Artist> getTopArtists(long offset, long limit) {
final Artists topArtists = request(GET, SPOTIFY_API_URL + "/top/artists?offset" + offset + "&limit=" + limit,
"", Artists.class);

return topArtists == null || topArtists.getItems() == null ? Collections.emptyList() : topArtists.getItems();
}

/**
* @return Returns the artists of the user.
*/
public List<Show> getShows(long offset, long limit) {
final AddedShows addedShows = request(GET, SPOTIFY_API_URL + "/shows?offset" + offset + "&limit=" + limit, "",
AddedShows.class);

List<Show> shows = new ArrayList<Show>();
if (addedShows != null) {
for (AddedShow addedShow : addedShows.getItems()) {
shows.add(addedShow.show);
}
}

return shows;
}

/**
* @return Returns the artists of the user.
*/
public List<Track> getTopTracks(long offset, long limit) {
final Tracks topTracks = request(GET, SPOTIFY_API_URL + "/top/tracks?offset" + offset + "&limit=" + limit, "",
Tracks.class);

return topTracks == null || topTracks.getItems() == null ? Collections.emptyList() : topTracks.getItems();
}

/**
* @return Returns the artists of the user.
*/
public List<Track> getTracks(long offset, long limit) {
String url = SPOTIFY_API_URL + "/tracks?offset" + offset;
if (limit != 0) {
url = url + "&limit=" + limit;
}
final UserTrackEntries userTracks = request(GET, url, "", UserTrackEntries.class);

return getTrackFromUserTrackEntries(userTracks);
}

/**
* @return Returns the artists of the user.
*/
public List<Track> getRecentlyPlayedTracks(long offset, long limit) {
final UserTrackEntries userTracks = request(GET,
SPOTIFY_API_URL + "/player/recently-played?offset" + offset + "&limit=" + limit, "",
UserTrackEntries.class);

return getTrackFromUserTrackEntries(userTracks);
}

public List<Track> getTrackFromUserTrackEntries(@Nullable UserTrackEntries userTracks) {
List<Track> tracks = new ArrayList<Track>();
if (userTracks != null) {
for (UserTrackEntry userTrack : userTracks.getItems()) {
tracks.add(userTrack.track);
}
}

return tracks;
}

/**
* @return Returns a playlist details
*/
public @Nullable Playlist getPlaylist(String uri) {
final Playlist playlist = request(GET,
SPOTIFY_API_BASE_URL + "/playlists/" + uri.replace("spotify:playlist:", ""), "", Playlist.class);
return playlist;
}

/**
* @return Returns the artists of the user.
*/
public @Nullable ApiSearchResult search(String searchQuery, long offset, long limit) {
final ApiSearchResult searchResult = request(GET,
SPOTIFY_API_BASE_URL + "/search?q=" + searchQuery
+ "&type=show%2Cepisode%2Caudiobook%2Calbum%2Cartist%2Cplaylist%2Ctrack" + "&offset=" + offset
+ "&limit=" + limit,
"", ApiSearchResult.class);

return searchResult;
}

/**
* @return Calls Spotify Api and returns the current playing context of the user or an empty object if no context as
* returned by Spotify
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2010-2025 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.api.model;

/**
* Spotify Web Api Added Show data class.
*
* @author Laurent Arnal - Initial contribution
*/
public class AddedShow {
public String added_at;
public Show show;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2010-2025 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.api.model;

/**
* Spotify Web Api Shows data class : a collection of Podcast
*
* @author Laurent Arnal - Initial contribution
*/
public class AddedShows extends Paging<AddedShow> {
}
Loading
Loading