Merge pull request #530 from chme/spotify_rework

Allow playing arbitrary spotify tracks
This commit is contained in:
ejurgensen 2018-05-11 19:12:04 +02:00 committed by GitHub
commit f5d7477ddb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 404 additions and 5 deletions

View File

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

View File

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

View File

@ -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));
@ -1611,8 +1617,13 @@ jsonapi_reply_queue_tracks_add(struct httpd_request *hreq)
queue_tracks_add_playlist(id);
}
else
{
ret = library_queue_add(uri);
if (ret != LIBRARY_OK)
{
DPRINTF(E_LOG, L_WEB, "Invalid uri '%s'\n", uri);
break;
}
}
}
while ((uri = strtok(NULL, ",")));

View File

@ -22,14 +22,30 @@
#include <stdint.h>
#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;

View File

@ -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;
}

View File

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

View File

@ -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, &param->album);
ret = db_queue_add_item(&param->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, &param.album);
ret = db_queue_add_start(&param.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, &param);
db_queue_add_end(&param.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:/<artist>/<album>, if the directory (or the parent
* directories) does not yet exist, they will be created.
@ -1449,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));
}
@ -1481,5 +1835,6 @@ struct library_source spotifyscanner =
.rescan = rescan,
.initscan = initscan,
.fullrescan = fullrescan,
.queue_add = queue_add,
};

View File

@ -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_ */