diff --git a/src/spotify.c b/src/spotify.c index 005e04df..0781b534 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -158,6 +158,11 @@ static enum spotify_state g_state; static int spotify_base_plid; // Flag telling us if access to the web api was granted static bool spotify_access_token_valid; +// The base playlist id for Spotify saved tracks in the db +static int spotify_saved_plid; + +// Flag to avoid triggering playlist change events while the (re)scan is running +static bool scanning; // Audio fifo static audio_fifo_t *g_audio_fifo; @@ -414,7 +419,11 @@ fptr_assign_all() static enum command_state -scan_webapi(void *arg, int *ret); +webapi_scan(void *arg, int *ret); +static enum command_state +webapi_pl_save(void *arg, int *ret); +static enum command_state +webapi_pl_remove(void *arg, int *ret); /* ------------------------------- MISC HELPERS ---------------------------- */ @@ -947,6 +956,33 @@ uri_register(void *arg, int *retval) return COMMAND_END; } +static void +webapi_playlist_updated(sp_playlist *pl) +{ + sp_link *link; + char url[1024]; + int ret; + + if (!scanning) + { + // Run playlist save in the library thread + link = fptr_sp_link_create_from_playlist(pl); + if (!link) + { + DPRINTF(E_LOG, L_SPOTIFY, "Could not create link for playlist: '%s'\n", fptr_sp_playlist_name(pl)); + return; + } + + ret = fptr_sp_link_as_string(link, url, sizeof(url)); + if (ret == sizeof(url)) + { + DPRINTF(E_DBG, L_SPOTIFY, "Spotify link truncated: %s\n", url); + } + fptr_sp_link_release(link); + + library_exec_async(webapi_pl_save, strdup(url)); + } +} /* -------------------------- PLAYLIST CALLBACKS ------------------------- */ /** @@ -966,8 +1002,14 @@ 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)); - if (!spotify_access_token_valid) // TODO Trigger update of library on pl change - spotify_playlist_save(pl); + if (spotify_access_token_valid) + { + webapi_playlist_updated(pl); + } + else + { + spotify_playlist_save(pl); + } } } @@ -975,8 +1017,15 @@ 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)); - if (!spotify_access_token_valid) // TODO Trigger update of library on pl change - spotify_playlist_save(pl); + if (spotify_access_token_valid) + { + //TODO Update disabled to prevent multiple triggering of updates e. g. on adding a playlist + //webapi_playlist_updated(pl); + } + else + { + spotify_playlist_save(pl); + } } /** @@ -1006,8 +1055,37 @@ static void playlist_added(sp_playlistcontainer *pc, sp_playlist *pl, fptr_sp_playlist_add_callbacks(pl, &pl_callbacks, NULL); - if (!spotify_access_token_valid) // TODO Trigger update of library on pl change - spotify_playlist_save(pl); + if (spotify_access_token_valid) + { + webapi_playlist_updated(pl); + } + else + { + spotify_playlist_save(pl); + } +} + +static int +playlist_remove(const char *uri) +{ + struct playlist_info *pli; + int plid; + + pli = db_pl_fetch_bypath(uri); + + if (!pli) + { + DPRINTF(E_DBG, L_SPOTIFY, "Playlist %s not found, can't delete\n", uri); + return -1; + } + + plid = pli->id; + + free_pli(pli, 0); + + db_spotify_pl_delete(plid); + spotify_cleanup_files(); + return 0; } /** @@ -1023,19 +1101,14 @@ static void playlist_added(sp_playlistcontainer *pc, sp_playlist *pl, static void playlist_removed(sp_playlistcontainer *pc, sp_playlist *pl, int position, void *userdata) { - struct playlist_info *pli; sp_link *link; char url[1024]; - int plid; int ret; DPRINTF(E_INFO, L_SPOTIFY, "Playlist removed: %s\n", fptr_sp_playlist_name(pl)); 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) { @@ -1050,21 +1123,16 @@ playlist_removed(sp_playlistcontainer *pc, sp_playlist *pl, int position, void * } fptr_sp_link_release(link); - pli = db_pl_fetch_bypath(url); - - if (!pli) + if (spotify_access_token_valid) { - DPRINTF(E_DBG, L_SPOTIFY, "Playlist %s not found, can't delete\n", url); - return; + // Run playlist remove in the library thread + if (!scanning) + library_exec_async(webapi_pl_remove, strdup(url)); + } + else + { + playlist_remove(url); } - - plid = pli->id; - - free_pli(pli, 0); - - db_spotify_pl_delete(plid); - - spotify_cleanup_files(); } /** @@ -1997,7 +2065,7 @@ spotify_oauth_callback(struct evbuffer *evbuf, struct evkeyvalq *param, const ch spotify_access_token_valid = true; // Trigger scan after successful access to spotifywebapi - library_exec_async(scan_webapi, NULL); + library_exec_async(webapi_scan, NULL); evbuffer_add_printf(evbuf, "ok, all done

\n"); @@ -2140,6 +2208,9 @@ scan_saved_albums() cache_artwork_ping(track.uri, album.mtime, 0); free_mfi(&mfi, 1); + + if (spotify_saved_plid) + db_pl_add_item_bypath(spotify_saved_plid, track.uri); } } } @@ -2224,8 +2295,10 @@ scan_playlists() plid = library_add_playlist_info(playlist.uri, playlist.name, virtual_path, PL_PLAIN, spotify_base_plid, DIR_SPOTIFY); - if (plid) + if (plid > 0) scan_playlisttracks(&playlist, plid); + else + DPRINTF(E_LOG, L_SPOTIFY, "Error adding playlist: '%s' (%s) \n", playlist.name, playlist.uri); } } @@ -2235,6 +2308,58 @@ scan_playlists() return 0; } +/* Thread: library */ +static int +scan_playlist(const char *uri) +{ + 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)); + + if (0 == spotifywebapi_playlist_start(&request, uri, &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 > 0) + scan_playlisttracks(&playlist, plid); + else + DPRINTF(E_LOG, L_SPOTIFY, "Error adding playlist: '%s' (%s) \n", playlist.name, playlist.uri); + } + + spotifywebapi_request_end(&request); + db_transaction_end(); + + return 0; +} + +static void +create_saved_tracks_playlist() +{ + spotify_saved_plid = library_add_playlist_info("spotify:savedtracks", "Spotify Saved", "/spotify:/Spotify Saved", PL_PLAIN, spotify_base_plid, DIR_SPOTIFY); + + if (spotify_saved_plid <= 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Error adding playlist for saved tracks\n"); + spotify_saved_plid = 0; + } +} + /* Thread: library */ static int initscan() @@ -2242,6 +2367,8 @@ initscan() cfg_t *spotify_cfg; int ret; + scanning = true; + /* Refresh access token for the spotify webapi */ spotify_access_token_valid = (0 == spotifywebapi_token_refresh()); if (!spotify_access_token_valid) @@ -2257,6 +2384,7 @@ initscan() * Add playlist folder for all spotify playlists */ spotify_base_plid = 0; + spotify_saved_plid = 0; spotify_cfg = cfg_getsec(cfg, "spotify"); if (! cfg_getbool(spotify_cfg, "base_playlist_disable")) { @@ -2278,10 +2406,13 @@ initscan() */ if (spotify_access_token_valid) { + create_saved_tracks_playlist(); scan_saved_albums(); scan_playlists(); } + scanning = false; + return 0; } @@ -2289,11 +2420,14 @@ initscan() static int rescan() { + scanning = true; + /* * Scan saved tracks from the web api */ if (spotify_access_token_valid) { + create_saved_tracks_playlist(); scan_saved_albums(); scan_playlists(); } @@ -2306,6 +2440,8 @@ rescan() db_transaction_end(); } + scanning = false; + return 0; } @@ -2316,14 +2452,34 @@ fullrescan() return 0; } -/* Thread: httpd */ +/* Thread: library */ static enum command_state -scan_webapi(void *arg, int *ret) +webapi_scan(void *arg, int *ret) { *ret = rescan(); return COMMAND_END; } +/* Thread: library */ +static enum command_state +webapi_pl_save(void *arg, int *ret) +{ + const char *uri = arg; + + *ret = scan_playlist(uri); + return COMMAND_END; +} + +/* Thread: library */ +static enum command_state +webapi_pl_remove(void *arg, int *ret) +{ + const char *uri = arg; + + *ret = playlist_remove(uri); + return COMMAND_END; +} + /* Thread: main */ int spotify_init(void) @@ -2334,6 +2490,7 @@ spotify_init(void) int ret; spotify_access_token_valid = false; + scanning = false; /* Initialize libspotify */ g_libhandle = dlopen("libspotify.so", RTLD_LAZY); diff --git a/src/spotify_webapi.c b/src/spotify_webapi.c index e97d1d78..889df1d6 100644 --- a/src/spotify_webapi.c +++ b/src/spotify_webapi.c @@ -50,6 +50,7 @@ static const char *spotify_client_id = "0e684a5422384114a8ae7ac020f01789"; 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"; /*--------------------- HELPERS FOR SPOTIFY WEB API -------------------------*/ @@ -690,18 +691,67 @@ spotifywebapi_playlists_fetch(struct spotify_request *request, struct spotify_pl if (request->index >= request->count) { + DPRINTF(E_DBG, L_SPOTIFY, "All playlists processed\n"); return -1; } jsonplaylist = json_object_array_get_idx(request->items, request->index); + if (!jsonplaylist) + { + DPRINTF(E_LOG, L_SPOTIFY, "Error fetching playlist at index '%d'\n", request->index); + return -1; + } playlist_metadata(jsonplaylist, playlist); - request->index++; return 0; } +/* + * Extracts the owner and the id from a spotify playlist uri + * + * Playlist-uri has the following format: spotify:user:[owner]:playlist:[id] + * Owner and plid must be freed by the caller. + */ +static int +get_owner_plid_from_uri(const char *uri, char **owner, char **plid) +{ + char *ptr1; + char *ptr2; + char *tmp; + size_t len; + + ptr1 = strchr(uri, ':'); + if (!ptr1) + return -1; + ptr1++; + ptr1 = strchr(ptr1, ':'); + if (!ptr1) + return -1; + ptr1++; + ptr2 = strchr(ptr1, ':'); + + len = ptr2 - ptr1; + + tmp = malloc(sizeof(char) * (len + 1)); + strncpy(tmp, ptr1, len); + tmp[len] = '\0'; + *owner = tmp; + + ptr2++; + ptr1 = strchr(ptr2, ':'); + if (!ptr1) + { + free(tmp); + return -1; + } + ptr1++; + *plid = strdup(ptr1); + + return 0; +} + int spotifywebapi_playlisttracks_fetch(struct spotify_request *request, struct spotify_track *track) { @@ -731,3 +781,44 @@ spotifywebapi_playlisttracks_fetch(struct spotify_request *request, struct spoti return 0; } + +int +spotifywebapi_playlist_start(struct spotify_request *request, const char *path, struct spotify_playlist *playlist) +{ + char uri[1024]; + char *owner; + char *id; + int ret; + + ret = get_owner_plid_from_uri(path, &owner, &id); + if (ret < 0) + { + DPRINTF(E_LOG, L_SPOTIFY, "Error extracting owner and id from playlist uri '%s'\n", path); + return -1; + } + + ret = snprintf(uri, sizeof(uri), spotify_playlist_uri, owner, id); + if (ret < 0 || ret >= sizeof(uri)) + { + DPRINTF(E_LOG, L_SPOTIFY, "Error creating playlist endpoint uri for playlist '%s'\n", path); + free(owner); + free(id); + return -1; + } + + ret = spotifywebapi_request_uri(request, uri); + if (ret < 0) + { + free(owner); + free(id); + return -1; + } + + request->haystack = json_tokener_parse(request->response_body); + playlist_metadata(request->haystack, playlist); + + free(owner); + free(id); + return 0; +} + diff --git a/src/spotify_webapi.h b/src/spotify_webapi.h index 81cd00ad..bd96ba22 100644 --- a/src/spotify_webapi.h +++ b/src/spotify_webapi.h @@ -121,6 +121,7 @@ int spotifywebapi_playlists_fetch(struct spotify_request *request, struct spotify_playlist* playlist); int spotifywebapi_playlisttracks_fetch(struct spotify_request *request, struct spotify_track *track); - +int +spotifywebapi_playlist_start(struct spotify_request *request, const char *path, struct spotify_playlist *playlist); #endif /* SRC_SPOTIFY_WEBAPI_H_ */