From 0bea83cafa46189b8c94d1933b98b8b0b65bf0a3 Mon Sep 17 00:00:00 2001 From: chme Date: Sun, 1 Jan 2017 11:06:00 +0100 Subject: [PATCH] [spotify] Scan saved albums and playlist using the spotify web api --- src/cache.c | 2 +- src/cache.h | 2 +- src/spotify.c | 262 +++++++++++++++++++++++++++++++++++++------ src/spotify_webapi.c | 16 ++- src/spotify_webapi.h | 8 +- 5 files changed, 247 insertions(+), 43 deletions(-) diff --git a/src/cache.c b/src/cache.c index a42ce018..66bdd0ce 100644 --- a/src/cache.c +++ b/src/cache.c @@ -1415,7 +1415,7 @@ cache_daap_threshold(void) * @return 0 if successful, -1 if an error occurred */ void -cache_artwork_ping(char *path, time_t mtime, int del) +cache_artwork_ping(const char *path, time_t mtime, int del) { struct cache_arg *cmdarg; diff --git a/src/cache.h b/src/cache.h index 16b42350..7e4e112f 100644 --- a/src/cache.h +++ b/src/cache.h @@ -31,7 +31,7 @@ cache_daap_threshold(void); #define CACHE_ARTWORK_INDIVIDUAL 1 void -cache_artwork_ping(char *path, time_t mtime, int del); +cache_artwork_ping(const char *path, time_t mtime, int del); int cache_artwork_delete_by_path(char *path); diff --git a/src/spotify.c b/src/spotify.c index fedc0945..9eb93127 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -156,8 +156,8 @@ static void *g_libhandle; static enum spotify_state g_state; // The base playlist id for all Spotify playlists in the db static int spotify_base_plid; -// The base playlist id for Spotify saved tracks in the db -static int spotify_saved_plid; +// Flag telling us if access to the web api was granted +static bool spotify_access_token_valid; // Audio fifo static audio_fifo_t *g_audio_fifo; @@ -707,7 +707,7 @@ spotify_track_save(int plid, sp_track *track, const char *pltitle, int time_adde // DPRINTF(E_DBG, L_SPOTIFY, "Saving track '%s': '%s' by %s (%s)\n", url, mfi.title, mfi.artist, mfi.album); - library_process_media(url, time(NULL), 0, DATA_KIND_SPOTIFY, 0, 0, &mfi, dir_id); + library_process_media(url, time(NULL), 0, DATA_KIND_SPOTIFY, 0, false, &mfi, dir_id); free_mfi(&mfi, 1); @@ -910,7 +910,7 @@ spotify_playlist_save(sp_playlist *pl) // told which track it was...). Note that this function will result in a ref // count on the sp_link, which the caller must decrease with sp_link_release. static enum command_state -spotify_uri_register(void *arg, int *retval) +uri_register(void *arg, int *retval) { sp_link *link; sp_track *track; @@ -963,7 +963,8 @@ static void playlist_update_in_progress(sp_playlist *pl, bool done, void *userda { DPRINTF(E_DBG, L_SPOTIFY, "Playlist update (status %d): %s\n", done, fptr_sp_playlist_name(pl)); - spotify_playlist_save(pl); + if (!spotify_access_token_valid) // TODO Trigger update of library on pl change + spotify_playlist_save(pl); } } @@ -971,7 +972,8 @@ static void playlist_metadata_updated(sp_playlist *pl, void *userdata) { DPRINTF(E_DBG, L_SPOTIFY, "Playlist metadata updated: %s\n", fptr_sp_playlist_name(pl)); - spotify_playlist_save(pl); + if (!spotify_access_token_valid) // TODO Trigger update of library on pl change + spotify_playlist_save(pl); } /** @@ -1001,7 +1003,8 @@ static void playlist_added(sp_playlistcontainer *pc, sp_playlist *pl, fptr_sp_playlist_add_callbacks(pl, &pl_callbacks, NULL); - spotify_playlist_save(pl); + if (!spotify_access_token_valid) // TODO Trigger update of library on pl change + spotify_playlist_save(pl); } /** @@ -1027,6 +1030,9 @@ playlist_removed(sp_playlistcontainer *pc, sp_playlist *pl, int position, void * fptr_sp_playlist_remove_callbacks(pl, &pl_callbacks, NULL); + if (spotify_access_token_valid) // TODO Trigger update of library on pl change + return; + link = fptr_sp_link_create_from_playlist(pl); if (!link) { @@ -1545,11 +1551,8 @@ artwork_get(void *arg, int *retval) static void logged_in(sp_session *sess, sp_error error) { - cfg_t *spotify_cfg; sp_playlist *pl; sp_playlistcontainer *pc; - struct playlist_info pli; - int ret; int i; if (SP_ERROR_OK != error) @@ -1565,24 +1568,6 @@ logged_in(sp_session *sess, sp_error error) pl = fptr_sp_session_starred_create(sess); fptr_sp_playlist_add_callbacks(pl, &pl_callbacks, NULL); - spotify_cfg = cfg_getsec(cfg, "spotify"); - if (! cfg_getbool(spotify_cfg, "base_playlist_disable")) - { - memset(&pli, 0, sizeof(struct playlist_info)); - pli.title = "Spotify"; - pli.type = PL_FOLDER; - pli.path = "spotify:playlistfolder"; - - ret = db_pl_add(&pli, &spotify_base_plid); - if (ret < 0) - { - DPRINTF(E_LOG, L_SPOTIFY, "Error adding base playlist\n"); - return; - } - } - else - spotify_base_plid = 0; - pc = fptr_sp_session_playlistcontainer(sess); fptr_sp_playlistcontainer_add_callbacks(pc, &pc_callbacks, NULL); @@ -2005,6 +1990,9 @@ spotify_oauth_callback(struct evbuffer *evbuf, struct evkeyvalq *param, const ch return; } + // Received a valid access token + spotify_access_token_valid = true; + // TODO Trigger init-scan after successful access to spotifywebapi evbuffer_add_printf(evbuf, "ok, all done

\n"); @@ -2012,6 +2000,15 @@ spotify_oauth_callback(struct evbuffer *evbuf, struct evkeyvalq *param, const ch return; } +static void +spotify_uri_register(const char *uri) +{ + char *tmp; + + tmp = strdup(uri); + commands_exec_async(cmdbase, uri_register, tmp); +} + /* Thread: library */ void spotify_login(char *path) @@ -2055,9 +2052,6 @@ spotify_login(char *path) if (path) { - db_spotify_purge(); - spotify_saved_plid = 0; - ret = spotify_file_read(path, &username, &password); if (ret < 0) return; @@ -2068,9 +2062,6 @@ spotify_login(char *path) } else { - db_spotify_purge(); - spotify_saved_plid = 0; - err = fptr_sp_session_relogin(g_sess); } @@ -2081,11 +2072,212 @@ spotify_login(char *path) } } +static void +map_track_to_mfi(const struct spotify_track *track, struct media_file_info* mfi) +{ + mfi->title = safe_strdup(track->name); + mfi->album = safe_strdup(track->album); + mfi->artist = safe_strdup(track->artist); + mfi->album_artist = safe_strdup(track->album_artist); + mfi->disc = track->disc_number; + mfi->song_length = track->duration_ms; + mfi->track = track->track_number; + mfi->compilation = track->is_compilation; +} + +static void +map_album_to_mfi(const struct spotify_album *album, struct media_file_info* mfi) +{ + mfi->album = safe_strdup(album->name); + mfi->album_artist = safe_strdup(album->artist); + mfi->genre = safe_strdup(album->genre); + mfi->compilation = album->is_compilation; +} + +/* Thread: library */ +static int +scan_saved_albums() +{ + struct spotify_request request; + json_object *jsontracks; + int track_count; + struct spotify_album album; + struct spotify_track track; + struct media_file_info mfi; + int dir_id; + int i; + int ret; + + db_transaction_begin(); + + memset(&request, 0, sizeof(struct spotify_request)); + + while (0 == spotifywebapi_request_next(&request, SPOTIFY_WEBAPI_SAVED_ALBUMS)) + { + while (0 == spotifywebapi_saved_albums_fetch(&request, &jsontracks, &track_count, &album)) + { + DPRINTF(E_DBG, L_SPOTIFY, "Got saved album: '%s' - '%s' (%s) - track-count: %d\n", + album.artist, album.name, album.uri, track_count); + + dir_id = prepare_directories(album.artist, album.name); + ret = 0; + for (i = 0; i < track_count && ret == 0; i++) + { + ret = spotifywebapi_album_track_fetch(jsontracks, i, &track); + if (ret == 0 && track.uri) + { + memset(&mfi, 0, sizeof(struct media_file_info)); + map_track_to_mfi(&track, &mfi); + map_album_to_mfi(&album, &mfi); + + library_process_media(track.uri, album.mtime, 0, DATA_KIND_SPOTIFY, 0, album.is_compilation, &mfi, dir_id); + spotify_uri_register(track.uri); + + cache_artwork_ping(track.uri, album.mtime, 0); + + free_mfi(&mfi, 1); + } + } + } + } + + spotifywebapi_request_end(&request); + + db_transaction_end(); + + return 0; +} + +/* Thread: library */ +static int +scan_playlisttracks(struct spotify_playlist *playlist, int plid) +{ + struct spotify_request request; + struct spotify_track track; + struct media_file_info mfi; + int dir_id; + + memset(&request, 0, sizeof(struct spotify_request)); + + while (0 == spotifywebapi_request_next(&request, playlist->tracks_href)) + { +// DPRINTF(E_DBG, L_SPOTIFY, "Playlist tracks\n%s\n", request.response_body); + while (0 == spotifywebapi_playlisttracks_fetch(&request, &track)) + { + DPRINTF(E_DBG, L_SPOTIFY, "Got playlist track: '%s' (%s) \n", track.name, track.uri); + + if (track.uri) + { + dir_id = prepare_directories(track.album_artist, track.album); + + memset(&mfi, 0, sizeof(struct media_file_info)); + map_track_to_mfi(&track, &mfi); + + library_process_media(track.uri, 1 /* TODO passing one prevents overwriting existing entries */, 0, DATA_KIND_SPOTIFY, 0, track.is_compilation, &mfi, dir_id); + spotify_uri_register(track.uri); + + cache_artwork_ping(track.uri, 1, 0); + + free_mfi(&mfi, 1); + + db_pl_add_item_bypath(plid, track.uri); + } + } + } + + spotifywebapi_request_end(&request); + + return 0; +} + +/* Thread: library */ +static int +scan_playlists() +{ + struct spotify_request request; + struct spotify_playlist playlist; + char virtual_path[PATH_MAX]; + int plid; + + db_transaction_begin(); + + memset(&request, 0, sizeof(struct spotify_request)); + + while (0 == spotifywebapi_request_next(&request, SPOTIFY_WEBAPI_SAVED_PLAYLISTS)) + { + while (0 == spotifywebapi_playlists_fetch(&request, &playlist)) + { + DPRINTF(E_DBG, L_SPOTIFY, "Got playlist: '%s' (%s) \n", playlist.name, playlist.uri); + + if (playlist.owner) + { + snprintf(virtual_path, PATH_MAX, "/spotify:/%s (%s)", playlist.name, playlist.owner); + } + else + { + snprintf(virtual_path, PATH_MAX, "/spotify:/%s", playlist.name); + } + + plid = library_add_playlist_info(playlist.uri, playlist.name, virtual_path, PL_PLAIN, spotify_base_plid, DIR_SPOTIFY); + + if (plid) + scan_playlisttracks(&playlist, plid); + } + } + + spotifywebapi_request_end(&request); + db_transaction_end(); + + return 0; +} + /* Thread: library */ static int spotify_initscan() { + cfg_t *spotify_cfg; + int ret; + + /* Refresh access token for the spotify webapi */ + spotify_access_token_valid = (0 == spotifywebapi_token_refresh()); + if (!spotify_access_token_valid) + { + DPRINTF(E_LOG, L_SPOTIFY, "Spotify webapi token refresh failed. " + "In order to use the web api, authorize forked-daapd to access " + "your saved tracks by visiting http://forked-daapd.local:3689/oauth\n"); + + db_spotify_purge(); + } + + /* + * Add playlist folder for all spotify playlists + */ + spotify_base_plid = 0; + spotify_cfg = cfg_getsec(cfg, "spotify"); + if (! cfg_getbool(spotify_cfg, "base_playlist_disable")) + { + ret = library_add_playlist_info("spotify:playlistfolder", "Spotify", NULL, PL_FOLDER, 0, 0); + if (ret < 0) + DPRINTF(E_LOG, L_SPOTIFY, "Error adding base playlist\n"); + else + spotify_base_plid = ret; + } + + /* + * Login to spotify needs to be done before scanning tracks from the web api. + * (Scanned tracks need to be registered with libspotify for playback) + */ spotify_login(NULL); + + /* + * Scan saved tracks from the web api + */ + if (spotify_access_token_valid) + { + scan_saved_albums(); + scan_playlists(); + } + return 0; } @@ -2112,6 +2304,8 @@ spotify_init(void) sp_error err; int ret; + spotify_access_token_valid = false; + /* Initialize libspotify */ g_libhandle = dlopen("libspotify.so", RTLD_LAZY); if (!g_libhandle) diff --git a/src/spotify_webapi.c b/src/spotify_webapi.c index 7184ffcc..e97d1d78 100644 --- a/src/spotify_webapi.c +++ b/src/spotify_webapi.c @@ -392,14 +392,14 @@ spotifywebapi_request_uri(struct spotify_request *request, const char *uri) snprintf(bearer_token, sizeof(bearer_token), "Bearer %s", spotify_access_token); if (keyval_add(request->ctx->output_headers, "Authorization", bearer_token) < 0) { - DPRINTF(E_LOG, L_SPOTIFY, "Add bearer_token to keyval failed"); + DPRINTF(E_LOG, L_SPOTIFY, "Add bearer_token to keyval failed\n"); return -1; } ret = http_client_request(request->ctx); if (ret < 0) { - DPRINTF(E_LOG, L_SPOTIFY, "Request for saved tracks/albums failed"); + DPRINTF(E_LOG, L_SPOTIFY, "Request for saved tracks/albums failed\n"); return -1; } @@ -409,10 +409,12 @@ spotifywebapi_request_uri(struct spotify_request *request, const char *uri) request->response_body = (char *) evbuffer_pullup(request->ctx->input_body, -1); if (!request->response_body || (strlen(request->response_body) == 0)) { - DPRINTF(E_LOG, L_SPOTIFY, "Request for saved tracks/albums failed, response was empty"); + DPRINTF(E_LOG, L_SPOTIFY, "Request for saved tracks/albums failed, response was empty\n"); return -1; } + //DPRINTF(E_DBG, L_SPOTIFY, "Wep api response for '%s'\n%s\n", uri, request->response_body); + request->haystack = json_tokener_parse(request->response_body); if (!request->haystack) { @@ -498,6 +500,8 @@ track_metadata(json_object* jsontrack, struct spotify_track* track) track->artist = jparse_artist_from_obj(jsonartists); } track->disc_number = jparse_int_from_obj(jsontrack, "disc_number"); + track->album_type = jparse_str_from_obj(jsonalbum, "album_type"); + track->is_compilation = (track->album_type && 0 == strcmp(track->album_type, "compilation")); track->duration_ms = jparse_int_from_obj(jsontrack, "duration_ms"); track->name = jparse_str_from_obj(jsontrack, "name"); track->track_number = jparse_int_from_obj(jsontrack, "track_number"); @@ -586,9 +590,13 @@ album_metadata(json_object *jsonalbum, struct spotify_album *album) album->id = jparse_str_from_obj(jsonalbum, "id"); album->album_type = jparse_str_from_obj(jsonalbum, "album_type"); - album->genre = jparse_str_from_obj(jsonalbum, "genre"); // FIXME Genre is an array of strings ('genres'), but it seems to be always empty + album->is_compilation = (album->album_type && 0 == strcmp(album->album_type, "compilation")); + album->label = jparse_str_from_obj(jsonalbum, "label"); album->release_date = jparse_str_from_obj(jsonalbum, "release_date"); + + // TODO Genre is an array of strings ('genres'), but it is always empty (https://github.com/spotify/web-api/issues/157) + //album->genre = jparse_str_from_obj(jsonalbum, "genre"); } int diff --git a/src/spotify_webapi.h b/src/spotify_webapi.h index 455d0758..81cd00ad 100644 --- a/src/spotify_webapi.h +++ b/src/spotify_webapi.h @@ -22,15 +22,14 @@ #include - -#include "http.h" - #ifdef HAVE_JSON_C_OLD # include #else # include #endif +#include +#include "http.h" #define SPOTIFY_WEBAPI_SAVED_TRACKS "https://api.spotify.com/v1/me/tracks?limit=50" #define SPOTIFY_WEBAPI_TRACKS "https://api.spotify.com/v1/tracks/" @@ -43,6 +42,7 @@ struct spotify_album time_t mtime; const char *album_type; + bool is_compilation; const char *artist; const char *genre; const char *id; @@ -61,6 +61,8 @@ struct spotify_track const char *album_artist; const char *artist; int disc_number; + const char *album_type; + bool is_compilation; int duration_ms; const char *id; const char *name;