From f108b6b498b57c1a6b44e75bbd6c2ef04ce7ef63 Mon Sep 17 00:00:00 2001 From: chme Date: Fri, 9 Mar 2018 19:21:58 +0100 Subject: [PATCH 1/5] [spotify] Support adding arbitrary spotify-uris to the queue Allows adding non-library spotify tracks to be added to the queue. The path given to queue_add should either be a spotify track, album or playlist uri. --- src/spotify_webapi.c | 332 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) diff --git a/src/spotify_webapi.c b/src/spotify_webapi.c index e2d7facc..25e7add1 100644 --- a/src/spotify_webapi.c +++ b/src/spotify_webapi.c @@ -119,9 +119,13 @@ static const char *spotify_client_secret = "232af95f39014c9ba218285a5c11a239"; static const char *spotify_auth_uri = "https://accounts.spotify.com/authorize"; static const char *spotify_token_uri = "https://accounts.spotify.com/api/token"; static const char *spotify_playlist_uri = "https://api.spotify.com/v1/users/%s/playlists/%s"; +static const char *spotify_track_uri = "https://api.spotify.com/v1/tracks/%s"; static const char *spotify_me_uri = "https://api.spotify.com/v1/me"; static const char *spotify_albums_uri = "https://api.spotify.com/v1/me/albums?limit=50"; +static const char *spotify_album_uri = "https://api.spotify.com/v1/albums/%s"; +static const char *spotify_album_tracks_uri = "https://api.spotify.com/v1/albums/%s/tracks"; static const char *spotify_playlists_uri = "https://api.spotify.com/v1/me/playlists?limit=50"; +static const char *spotify_playlist_tracks_uri = "https://api.spotify.com/v1/users/%s/playlists/%s/tracks"; @@ -716,6 +720,7 @@ get_owner_plid_from_uri(const char *uri, char **owner, char **plid) if (!ptr1) { free(tmp); + *owner = NULL; return -1; } ptr1++; @@ -731,6 +736,22 @@ get_owner_plid_from_uri(const char *uri, char **owner, char **plid) * @param uri Playlist uri (e. g. "spotify:user:username:playlist:59ZbFPES4DQwEjBpWHzrtC") * @return Playlist endpoint uri (e. g. "https://api.spotify.com/v1/users/username/playlists/59ZbFPES4DQwEjBpWHzrtC") */ +static int +get_id_from_uri(const char *uri, char **id) +{ + char *tmp; + tmp = strrchr(uri, ':'); + if (!tmp) + { + return -1; + } + tmp++; + + *id = strdup(tmp); + + return 0; +} + static char * get_playlist_endpoint_uri(const char *uri) { @@ -754,6 +775,105 @@ get_playlist_endpoint_uri(const char *uri) return endpoint_uri; } +static char * +get_playlist_tracks_endpoint_uri(const char *uri) +{ + char *endpoint_uri = NULL; + char *owner = NULL; + char *id = NULL; + int ret; + + ret = get_owner_plid_from_uri(uri, &owner, &id); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Error extracting owner and id from playlist uri '%s'\n", uri); + goto out; + } + + endpoint_uri = safe_asprintf(spotify_playlist_tracks_uri, owner, id); + + out: + free(owner); + free(id); + return endpoint_uri; +} + +static char * +get_album_endpoint_uri(const char *uri) +{ + char *endpoint_uri = NULL; + char *id = NULL; + int ret; + + ret = get_id_from_uri(uri, &id); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Error extracting id from uri '%s'\n", uri); + goto out; + } + + endpoint_uri = safe_asprintf(spotify_album_uri, id); + + out: + free(id); + return endpoint_uri; +} + +static char * +get_album_tracks_endpoint_uri(const char *uri) +{ + char *endpoint_uri = NULL; + char *id = NULL; + int ret; + + ret = get_id_from_uri(uri, &id); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Error extracting id from uri '%s'\n", uri); + goto out; + } + + endpoint_uri = safe_asprintf(spotify_album_tracks_uri, id); + + out: + free(id); + return endpoint_uri; +} + +static char * +get_track_endpoint_uri(const char *uri) +{ + char *endpoint_uri = NULL; + char *id = NULL; + int ret; + + ret = get_id_from_uri(uri, &id); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Error extracting id from track uri '%s'\n", uri); + goto out; + } + + endpoint_uri = safe_asprintf(spotify_track_uri, id); + + out: + free(id); + return endpoint_uri; +} + +static json_object * +request_track(const char *path) +{ + char *endpoint_uri; + json_object *response; + + endpoint_uri = get_track_endpoint_uri(path); + response = request_endpoint_with_token_refresh(endpoint_uri); + free(endpoint_uri); + + return response; +} + /* Thread: httpd */ char * spotifywebapi_oauth_uri_get(const char *redirect_uri) @@ -841,6 +961,217 @@ transaction_end(void *arg) return 0; } +static void +map_track_to_queueitem(struct db_queue_item *item, const struct spotify_track *track, const struct spotify_album *album) +{ + char virtual_path[PATH_MAX]; + + memset(item, 0, sizeof(struct db_queue_item)); + + item->file_id = DB_MEDIA_FILE_NON_PERSISTENT_ID; + item->title = safe_strdup(track->name); + item->artist = safe_strdup(track->artist); + + if (album) + { + item->album_artist = safe_strdup(album->artist); + item->album = safe_strdup(album->name); + } + else + { + item->album_artist = safe_strdup(track->album_artist); + item->album = safe_strdup(track->album); + } + + item->disc = track->disc_number; + item->song_length = track->duration_ms; + item->track = track->track_number; + + item->data_kind = DATA_KIND_SPOTIFY; + item->media_kind = MEDIA_KIND_MUSIC; + + item->path = safe_strdup(track->uri); + + snprintf(virtual_path, PATH_MAX, "/%s", track->uri); + item->virtual_path = strdup(virtual_path); +} + +static int +queue_add_track(const char *uri) +{ + json_object *response; + struct spotify_track track; + struct db_queue_item item; + struct db_queue_add_info queue_add_info; + int ret; + + response = request_track(uri); + if (!response) + return -1; + + parse_metadata_track(response, &track); + + DPRINTF(E_DBG, L_SPOTIFY, "Got track: '%s' (%s) \n", track.name, track.uri); + + map_track_to_queueitem(&item, &track, NULL); + + ret = db_queue_add_start(&queue_add_info); + if (ret == 0) + { + ret = db_queue_add_item(&queue_add_info, &item); + db_queue_add_end(&queue_add_info, ret); + } + + free_queue_item(&item, 1); + jparse_free(response); + + return 0; +} + +struct queue_add_album_param { + struct spotify_album album; + struct db_queue_add_info queue_add_info; +}; + +static int +queue_add_album_tracks(json_object *item, int index, int total, void *arg) +{ + struct queue_add_album_param *param; + struct spotify_track track; + struct db_queue_item queue_item; + int ret; + + param = arg; + + parse_metadata_track(item, &track); + + if (!track.uri || !track.is_playable) + { + DPRINTF(E_LOG, L_SPOTIFY, "Track not available for playback: '%s' - '%s' (%s) (restrictions: %s)\n", track.artist, track.name, track.uri, track.restrictions); + return -1; + } + + map_track_to_queueitem(&queue_item, &track, ¶m->album); + + ret = db_queue_add_item(¶m->queue_add_info, &queue_item); + + free_queue_item(&queue_item, 1); + + return ret; +} + +static int +queue_add_album(const char *uri) +{ + char *album_endpoint_uri = NULL; + char *endpoint_uri = NULL; + json_object *json_album; + struct queue_add_album_param param; + int ret; + + album_endpoint_uri = get_album_endpoint_uri(uri); + json_album = request_endpoint_with_token_refresh(album_endpoint_uri); + parse_metadata_album(json_album, ¶m.album); + + ret = db_queue_add_start(¶m.queue_add_info); + if (ret < 0) + goto out; + + endpoint_uri = get_album_tracks_endpoint_uri(uri); + + ret = request_pagingobject_endpoint(endpoint_uri, queue_add_album_tracks, NULL, NULL, true, ¶m); + + db_queue_add_end(¶m.queue_add_info, ret); + + out: + free(album_endpoint_uri); + free(endpoint_uri); + jparse_free(json_album); + + return ret; +} + +static int +queue_add_playlist_tracks(json_object *item, int index, int total, void *arg) +{ + struct db_queue_add_info *queue_add_info; + struct spotify_track track; + json_object *jsontrack; + struct db_queue_item queue_item; + int ret; + + queue_add_info = arg; + + if (!(item && json_object_object_get_ex(item, "track", &jsontrack))) + { + DPRINTF(E_LOG, L_SPOTIFY, "Unexpected JSON: missing 'track' in JSON object at index %d\n", index); + return -1; + } + + parse_metadata_track(jsontrack, &track); + track.added_at = jparse_str_from_obj(item, "added_at"); + track.mtime = jparse_time_from_obj(item, "added_at"); + + if (!track.uri || !track.is_playable) + { + DPRINTF(E_LOG, L_SPOTIFY, "Track not available for playback: '%s' - '%s' (%s) (restrictions: %s)\n", track.artist, track.name, track.uri, track.restrictions); + return -1; + } + + map_track_to_queueitem(&queue_item, &track, NULL); + + ret = db_queue_add_item(queue_add_info, &queue_item); + + free_queue_item(&queue_item, 1); + + return ret; +} + +static int +queue_add_playlist(const char *uri) +{ + char *endpoint_uri; + struct db_queue_add_info queue_add_info; + int ret; + + ret = db_queue_add_start(&queue_add_info); + if (ret < 0) + return -1; + + endpoint_uri = get_playlist_tracks_endpoint_uri(uri); + + ret = request_pagingobject_endpoint(endpoint_uri, queue_add_playlist_tracks, NULL, NULL, true, &queue_add_info); + + db_queue_add_end(&queue_add_info, ret); + + free(endpoint_uri); + + return ret; +} + +static int +queue_add(const char *uri) +{ + if (strncasecmp(uri, "spotify:track:", strlen("spotify:track:")) == 0) + { + queue_add_track(uri); + return LIBRARY_OK; + } + else if (strncasecmp(uri, "spotify:album:", strlen("spotify:album:")) == 0) + { + queue_add_album(uri); + return LIBRARY_OK; + } + else if (strncasecmp(uri, "spotify:", strlen("spotify:")) == 0) + { + queue_add_playlist(uri); + return LIBRARY_OK; + } + + return LIBRARY_PATH_INVALID; +} + + /* * Returns the directory id for /spotify://, if the directory (or the parent * directories) does not yet exist, they will be created. @@ -1481,5 +1812,6 @@ struct library_source spotifyscanner = .rescan = rescan, .initscan = initscan, .fullrescan = fullrescan, + .queue_add = queue_add, }; From d562cb9b6ba6fb1e5a91191f80eedc38baa58a3b Mon Sep 17 00:00:00 2001 From: chme Date: Thu, 1 Mar 2018 17:29:10 +0100 Subject: [PATCH 2/5] [spotify] Retry playback setup if song is still loading --- src/inputs/spotify.c | 18 +++++++++++++++++- src/spotify.c | 2 +- src/spotify.h | 2 ++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/inputs/spotify.c b/src/inputs/spotify.c index f4d5cc8a..a52ebc07 100644 --- a/src/inputs/spotify.c +++ b/src/inputs/spotify.c @@ -22,14 +22,30 @@ #include #include "input.h" +#include "logger.h" #include "spotify.h" +// How many retries to start playback if resource is still loading +#define SPOTIFY_SETUP_RETRIES 5 +// How long to wait between retries in microseconds (500000 = 0.5 seconds) +#define SPOTIFY_SETUP_RETRY_WAIT 500000 + static int setup(struct player_source *ps) { + int i = 0; int ret; - ret = spotify_playback_setup(ps->path); + while((ret = spotify_playback_setup(ps->path)) == SPOTIFY_SETUP_ERROR_IS_LOADING) + { + if (i >= SPOTIFY_SETUP_RETRIES) + break; + + DPRINTF(E_DBG, L_SPOTIFY, "Resource still loading (%d)\n", i); + usleep(SPOTIFY_SETUP_RETRY_WAIT); + i++; + } + if (ret < 0) return -1; diff --git a/src/spotify.c b/src/spotify.c index 67a483a7..ed6d5745 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -604,7 +604,7 @@ playback_setup(void *arg, int *retval) if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Playback setup failed: %s\n", fptr_sp_error_message(err)); - *retval = -1; + *retval = (SP_ERROR_IS_LOADING == err) ? SPOTIFY_SETUP_ERROR_IS_LOADING : -1; return COMMAND_END; } diff --git a/src/spotify.h b/src/spotify.h index 988892e1..2e2f47c9 100644 --- a/src/spotify.h +++ b/src/spotify.h @@ -15,6 +15,8 @@ struct spotify_status_info char libspotify_user[100]; }; +#define SPOTIFY_SETUP_ERROR_IS_LOADING -2 + int spotify_playback_setup(const char *path); From 7bab990eb342812908ae7ca5da47d374819bbb72 Mon Sep 17 00:00:00 2001 From: chme Date: Sat, 17 Mar 2018 12:21:24 +0100 Subject: [PATCH 3/5] [spotify/jsonapi] Expose access token and user country in JSON API spoitfy endpoint --- src/httpd_jsonapi.c | 10 ++++++++-- src/spotify_webapi.c | 23 +++++++++++++++++++++++ src/spotify_webapi.h | 9 +++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index 7e4a5914..d860a887 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -627,6 +627,7 @@ jsonapi_reply_spotify(struct httpd_request *hreq) char *oauth_uri; struct spotify_status_info info; struct spotifywebapi_status_info webapi_info; + struct spotifywebapi_access_token webapi_token; json_object_object_add(jreply, "enabled", json_object_new_boolean(true)); @@ -647,11 +648,16 @@ jsonapi_reply_spotify(struct httpd_request *hreq) spotify_status_info_get(&info); json_object_object_add(jreply, "libspotify_installed", json_object_new_boolean(info.libspotify_installed)); json_object_object_add(jreply, "libspotify_logged_in", json_object_new_boolean(info.libspotify_logged_in)); - json_object_object_add(jreply, "libspotify_user", json_object_new_string(info.libspotify_user)); + safe_json_add_string(jreply, "libspotify_user", info.libspotify_user); spotifywebapi_status_info_get(&webapi_info); json_object_object_add(jreply, "webapi_token_valid", json_object_new_boolean(webapi_info.token_valid)); - json_object_object_add(jreply, "webapi_user", json_object_new_string(webapi_info.user)); + safe_json_add_string(jreply, "webapi_user", webapi_info.user); + safe_json_add_string(jreply, "webapi_country", webapi_info.country); + + spotifywebapi_access_token_get(&webapi_token); + safe_json_add_string(jreply, "webapi_token", webapi_token.token); + json_object_object_add(jreply, "webapi_token_expires_in", json_object_new_int(webapi_token.expires_in)); #else json_object_object_add(jreply, "enabled", json_object_new_boolean(false)); diff --git a/src/spotify_webapi.c b/src/spotify_webapi.c index 25e7add1..2a734b3a 100644 --- a/src/spotify_webapi.c +++ b/src/spotify_webapi.c @@ -1780,6 +1780,29 @@ spotifywebapi_status_info_get(struct spotifywebapi_status_info *info) { memcpy(info->user, spotify_user, (sizeof(info->user) - 1)); } + if (spotify_user_country) + { + memcpy(info->country, spotify_user_country, (sizeof(info->country) - 1)); + } + + CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&token_lck)); +} + +void +spotifywebapi_access_token_get(struct spotifywebapi_access_token *info) +{ + token_refresh(); + + memset(info, 0, sizeof(struct spotifywebapi_access_token)); + + CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&token_lck)); + + if (token_requested > 0) + info->expires_in = expires_in - difftime(time(NULL), token_requested); + else + info->expires_in = 0; + + info->token = safe_strdup(spotify_access_token); CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&token_lck)); } diff --git a/src/spotify_webapi.h b/src/spotify_webapi.h index 19261ff7..00090441 100644 --- a/src/spotify_webapi.h +++ b/src/spotify_webapi.h @@ -30,6 +30,13 @@ struct spotifywebapi_status_info { bool token_valid; char user[100]; + char country[3]; // ISO 3166-1 alpha-2 country code +}; + +struct spotifywebapi_access_token +{ + int expires_in; + char *token; }; @@ -49,5 +56,7 @@ spotifywebapi_pl_remove(const char *uri); void spotifywebapi_status_info_get(struct spotifywebapi_status_info *info); +void +spotifywebapi_access_token_get(struct spotifywebapi_access_token *info); #endif /* SRC_SPOTIFY_WEBAPI_H_ */ From 843fbeb06616cb9eea1795ec862516a3f97cc7d0 Mon Sep 17 00:00:00 2001 From: chme Date: Tue, 20 Mar 2018 21:53:38 +0100 Subject: [PATCH 4/5] [jsonapi] Add non-library uris to the queue --- src/httpd_jsonapi.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/httpd_jsonapi.c b/src/httpd_jsonapi.c index d860a887..dfa00c34 100644 --- a/src/httpd_jsonapi.c +++ b/src/httpd_jsonapi.c @@ -1618,7 +1618,12 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq) } else { - DPRINTF(E_LOG, L_WEB, "Invalid uri '%s'\n", uri); + ret = library_queue_add(uri); + if (ret != LIBRARY_OK) + { + DPRINTF(E_LOG, L_WEB, "Invalid uri '%s'\n", uri); + break; + } } } while ((uri = strtok(NULL, ","))); From c77acbddf29cfa80059880268af7099d6747b82f Mon Sep 17 00:00:00 2001 From: chme Date: Thu, 10 May 2018 07:23:11 +0200 Subject: [PATCH 5/5] [artwork] Ignore artwork requests for items with a non persistent id --- src/artwork.c | 3 +++ src/artwork_legacy.c | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/artwork.c b/src/artwork.c index 7ae53ed4..dece08b8 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -1072,6 +1072,9 @@ artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h) DPRINTF(E_DBG, L_ART, "Artwork request for item %d\n", id); + if (id == DB_MEDIA_FILE_NON_PERSISTENT_ID) + return -1; + memset(&ctx, 0, sizeof(struct artwork_ctx)); ctx.qp.type = Q_ITEMS; diff --git a/src/artwork_legacy.c b/src/artwork_legacy.c index e25d502c..777ded57 100644 --- a/src/artwork_legacy.c +++ b/src/artwork_legacy.c @@ -1499,6 +1499,9 @@ artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h) DPRINTF(E_DBG, L_ART, "Artwork request for item %d\n", id); + if (id == DB_MEDIA_FILE_NON_PERSISTENT_ID) + return -1; + memset(&ctx, 0, sizeof(struct artwork_ctx)); ctx.qp.type = Q_ITEMS;