/* * Copyright (C) 2014 Espen Jürgensen * * Stiched together from libspotify examples * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "spotify.h" #include "logger.h" #include "conffile.h" #include "filescanner.h" /* --- Types --- */ typedef struct audio_fifo_data { TAILQ_ENTRY(audio_fifo_data) link; int nsamples; int16_t samples[0]; } audio_fifo_data_t; typedef struct audio_fifo { TAILQ_HEAD(, audio_fifo_data) q; int qlen; int fullcount; pthread_mutex_t mutex; pthread_cond_t cond; } audio_fifo_t; enum spotify_event { SPOTIFY_EVENT_NONE, SPOTIFY_EVENT_LIBCB, SPOTIFY_EVENT_PLAY, SPOTIFY_EVENT_PAUSE, SPOTIFY_EVENT_EOT, SPOTIFY_EVENT_STOP, SPOTIFY_EVENT_SEEK, SPOTIFY_EVENT_EXIT, SPOTIFY_EVENT_RESUME, }; enum spotify_state { SPOTIFY_STATE_INACTIVE, SPOTIFY_STATE_WAIT, SPOTIFY_STATE_PLAYING, SPOTIFY_STATE_PAUSED, SPOTIFY_STATE_STOPPING, SPOTIFY_STATE_STOPPED, SPOTIFY_STATE_SEEKED, SPOTIFY_STATE_EXITING, }; /* Context for communicating with player */ struct spotify_ctx { sp_link *link; int seek_ms; }; /** * The application key is specific to forked-daapd, and allows Spotify * to produce statistics on how their service is used. */ const uint8_t g_appkey[] = { 0x01, 0xC6, 0x9D, 0x18, 0xA4, 0xF7, 0x79, 0x12, 0x43, 0x55, 0x0F, 0xAD, 0xBF, 0x23, 0x23, 0x10, 0x2E, 0x51, 0x46, 0x8F, 0x06, 0x3D, 0xEE, 0xC3, 0xF0, 0x2A, 0x5D, 0x8E, 0x72, 0x35, 0xD1, 0x21, 0x44, 0xE3, 0x19, 0x80, 0xED, 0xD5, 0xAD, 0xE6, 0xE1, 0xDD, 0xBE, 0xCB, 0xA9, 0x84, 0xBD, 0xC2, 0xAF, 0xB1, 0xF2, 0xD5, 0x87, 0xFC, 0x35, 0xD6, 0x1C, 0x5F, 0x5B, 0x76, 0x38, 0x1D, 0x6E, 0x49, 0x6D, 0x85, 0x15, 0xCD, 0x38, 0x14, 0xD6, 0xB8, 0xFE, 0x05, 0x0A, 0xAC, 0x9B, 0x31, 0xD1, 0xC0, 0xAF, 0x16, 0x78, 0x48, 0x49, 0x27, 0x41, 0xCA, 0xAF, 0x07, 0xEC, 0x10, 0x5D, 0x19, 0x43, 0x2E, 0x84, 0xEB, 0x43, 0x5D, 0x4B, 0xBF, 0xD0, 0x5C, 0xDF, 0x3D, 0x12, 0x6D, 0x1C, 0x76, 0x4E, 0x9F, 0xBF, 0x14, 0xC9, 0x46, 0x95, 0x99, 0x32, 0x6A, 0xC2, 0xF1, 0x89, 0xA4, 0xB3, 0xF3, 0xA0, 0xEB, 0xDA, 0x84, 0x67, 0x27, 0x07, 0x1F, 0xF6, 0x19, 0xAC, 0xF1, 0xB8, 0xB6, 0xCF, 0xAB, 0xF8, 0x0A, 0xEE, 0x4D, 0xAC, 0xC2, 0x39, 0x63, 0x50, 0x13, 0x7B, 0x51, 0x3A, 0x50, 0xE0, 0x03, 0x6E, 0xB7, 0x17, 0xEE, 0x58, 0xCE, 0xF8, 0x15, 0x3C, 0x70, 0xDE, 0xE6, 0xEB, 0xE6, 0xD4, 0x2C, 0x27, 0xB9, 0xCA, 0x15, 0xCE, 0x2E, 0x31, 0x54, 0xF5, 0x0A, 0x98, 0x8D, 0x78, 0xE5, 0xB6, 0xF8, 0xE4, 0x62, 0x43, 0xAA, 0x37, 0x93, 0xFF, 0xE3, 0xAB, 0x17, 0xC5, 0x81, 0x4F, 0xFD, 0xF1, 0x84, 0xE1, 0x8A, 0x99, 0xB0, 0x1D, 0x85, 0x80, 0xA2, 0x49, 0x35, 0x8D, 0xDD, 0xBC, 0x74, 0x0B, 0xBA, 0x33, 0x5B, 0xD5, 0x7A, 0xB9, 0x2F, 0x9B, 0x24, 0xA5, 0xAB, 0xF6, 0x1E, 0xE3, 0xA3, 0xA8, 0x0D, 0x1E, 0x48, 0xF7, 0xDB, 0xE2, 0x54, 0x65, 0x43, 0xA6, 0xD3, 0x3F, 0x2C, 0x9B, 0x13, 0x9A, 0xBE, 0x0F, 0x4D, 0x51, 0xC3, 0x73, 0xA5, 0xFE, 0xFC, 0x93, 0x12, 0xEF, 0x9C, 0x4D, 0x68, 0xE3, 0xDA, 0x52, 0x67, 0x28, 0x41, 0x17, 0x22, 0x3E, 0x33, 0xB0, 0x3A, 0xFB, 0x44, 0xB0, 0x2E, 0xA6, 0xD2, 0x95, 0xC0, 0x9A, 0xBA, 0x32, 0xA3, 0xC5, 0xFE, 0x86, 0x5D, 0xC8, 0xBB, 0xB5, 0xDE, 0x92, 0x8C, 0x7D, 0xE4, 0x03, 0xD4, 0xF9, 0xAE, 0x41, 0xE3, 0xBD, 0x35, 0x4B, 0x94, 0x27, 0xE0, 0x12, 0x21, 0x46, 0xE9, 0x09, }; // Spotify thread static pthread_t tid_spotify; // Synchronization mutex for the spotify thread static pthread_mutex_t g_notify_mutex; // Synchronization condition variable for the spotify thread static pthread_cond_t g_notify_cond; // Synchronization variable telling the spotify thread to process libspotify events static int g_notify; // Synchronization variable telling the spotify thread to process player events static enum spotify_event g_event; // Synchronization mutex for the spotify player state static pthread_mutex_t g_state_mutex; // Synchronization condition variable for the spotify player state static pthread_cond_t g_state_cond; // Synchronization variable telling the caller thread about the state static enum spotify_state g_state; // The global session handle static sp_session *g_sess; // The global library handle static void *g_libhandle; // The global spotify context static struct spotify_ctx g_ctx; // Synchronization mutex for the database (possibly useless) static pthread_mutex_t g_db_mutex; // Audio fifo static audio_fifo_t *g_audio_fifo; // This section defines and assigns function pointers to the libspotify functions // The arguments and return values must be in sync with the spotify api // Please scroll through the ugliness which follows typedef const char* (*fptr_sp_error_message_t)(sp_error error); typedef sp_error (*fptr_sp_session_create_t)(const sp_session_config *config, sp_session **sess); typedef sp_error (*fptr_sp_session_release_t)(sp_session *sess); typedef sp_error (*fptr_sp_session_login_t)(sp_session *session, const char *username, const char *password, bool remember_me, const char *blob); typedef sp_error (*fptr_sp_session_relogin_t)(sp_session *session); typedef sp_error (*fptr_sp_session_logout_t)(sp_session *session); typedef sp_error (*fptr_sp_session_process_events_t)(sp_session *session, int *next_timeout); typedef sp_playlistcontainer* (*fptr_sp_session_playlistcontainer_t)(sp_session *session); typedef sp_error (*fptr_sp_session_player_load_t)(sp_session *session, sp_track *track); typedef sp_error (*fptr_sp_session_player_unload_t)(sp_session *session); typedef sp_error (*fptr_sp_session_player_play_t)(sp_session *session, bool play); typedef sp_error (*fptr_sp_session_player_seek_t)(sp_session *session, int offset); typedef sp_error (*fptr_sp_playlistcontainer_add_callbacks_t)(sp_playlistcontainer *pc, sp_playlistcontainer_callbacks *callbacks, void *userdata); typedef int (*fptr_sp_playlistcontainer_num_playlists_t)(sp_playlistcontainer *pc); typedef sp_playlist* (*fptr_sp_playlistcontainer_playlist_t)(sp_playlistcontainer *pc, int index); typedef sp_error (*fptr_sp_playlist_add_callbacks_t)(sp_playlist *playlist, sp_playlist_callbacks *callbacks, void *userdata); typedef const char* (*fptr_sp_playlist_name_t)(sp_playlist *playlist); typedef sp_error (*fptr_sp_playlist_remove_callbacks_t)(sp_playlist *playlist, sp_playlist_callbacks *callbacks, void *userdata); typedef int (*fptr_sp_playlist_num_tracks_t)(sp_playlist *playlist); typedef sp_track* (*fptr_sp_playlist_track_t)(sp_playlist *playlist, int index); typedef bool (*fptr_sp_playlist_is_loaded_t)(sp_playlist *playlist); typedef sp_error (*fptr_sp_track_error_t)(sp_track *track); typedef bool (*fptr_sp_track_is_loaded_t)(sp_track *track); typedef const char* (*fptr_sp_track_name_t)(sp_track *track); typedef int (*fptr_sp_track_duration_t)(sp_track *track); typedef int (*fptr_sp_track_index_t)(sp_track *track); typedef int (*fptr_sp_track_disc_t)(sp_track *track); typedef sp_album* (*fptr_sp_track_album_t)(sp_track *track); typedef sp_link* (*fptr_sp_link_create_from_playlist_t)(sp_playlist *playlist); typedef sp_link* (*fptr_sp_link_create_from_track_t)(sp_track *track, int offset); typedef sp_link* (*fptr_sp_link_create_from_string_t)(const char *link); typedef int (*fptr_sp_link_as_string_t)(sp_link *link, char *buffer, int buffer_size); typedef sp_track* (*fptr_sp_link_as_track_t)(sp_link *link); typedef sp_error (*fptr_sp_link_release_t)(sp_link *link); typedef const char* (*fptr_sp_album_name_t)(sp_album *album); typedef sp_artist* (*fptr_sp_album_artist_t)(sp_album *album); typedef int (*fptr_sp_album_year_t)(sp_album *album); typedef sp_albumtype (*fptr_sp_album_type_t)(sp_album *album); typedef const char* (*fptr_sp_artist_name_t)(sp_artist *artist); /* Define actual function pointers */ fptr_sp_error_message_t fptr_sp_error_message; fptr_sp_session_create_t fptr_sp_session_create; fptr_sp_session_release_t fptr_sp_session_release; fptr_sp_session_login_t fptr_sp_session_login; fptr_sp_session_relogin_t fptr_sp_session_relogin; fptr_sp_session_logout_t fptr_sp_session_logout; fptr_sp_session_playlistcontainer_t fptr_sp_session_playlistcontainer; fptr_sp_session_process_events_t fptr_sp_session_process_events; fptr_sp_session_player_load_t fptr_sp_session_player_load; fptr_sp_session_player_unload_t fptr_sp_session_player_unload; fptr_sp_session_player_play_t fptr_sp_session_player_play; fptr_sp_session_player_seek_t fptr_sp_session_player_seek; fptr_sp_playlistcontainer_add_callbacks_t fptr_sp_playlistcontainer_add_callbacks; fptr_sp_playlistcontainer_num_playlists_t fptr_sp_playlistcontainer_num_playlists; fptr_sp_playlistcontainer_playlist_t fptr_sp_playlistcontainer_playlist; fptr_sp_playlist_add_callbacks_t fptr_sp_playlist_add_callbacks; fptr_sp_playlist_name_t fptr_sp_playlist_name; fptr_sp_playlist_remove_callbacks_t fptr_sp_playlist_remove_callbacks; fptr_sp_playlist_num_tracks_t fptr_sp_playlist_num_tracks; fptr_sp_playlist_track_t fptr_sp_playlist_track; fptr_sp_playlist_is_loaded_t fptr_sp_playlist_is_loaded; fptr_sp_track_error_t fptr_sp_track_error; fptr_sp_track_is_loaded_t fptr_sp_track_is_loaded; fptr_sp_track_name_t fptr_sp_track_name; fptr_sp_track_duration_t fptr_sp_track_duration; fptr_sp_track_index_t fptr_sp_track_index; fptr_sp_track_disc_t fptr_sp_track_disc; fptr_sp_track_album_t fptr_sp_track_album; fptr_sp_link_create_from_playlist_t fptr_sp_link_create_from_playlist; fptr_sp_link_create_from_track_t fptr_sp_link_create_from_track; fptr_sp_link_create_from_string_t fptr_sp_link_create_from_string; fptr_sp_link_as_string_t fptr_sp_link_as_string; fptr_sp_link_as_track_t fptr_sp_link_as_track; fptr_sp_link_release_t fptr_sp_link_release; fptr_sp_album_name_t fptr_sp_album_name; fptr_sp_album_artist_t fptr_sp_album_artist; fptr_sp_album_year_t fptr_sp_album_year; fptr_sp_album_type_t fptr_sp_album_type; fptr_sp_artist_name_t fptr_sp_artist_name; /* Assign function pointers to libspotify symbol */ static int fptr_assign_all() { void *h; char *err; int ret; h = g_libhandle; // The following is non-ISO compliant ret = (fptr_sp_error_message = dlsym(h, "sp_error_message")) && (fptr_sp_session_create = dlsym(h, "sp_session_create")) && (fptr_sp_session_release = dlsym(h, "sp_session_release")) && (fptr_sp_session_login = dlsym(h, "sp_session_login")) && (fptr_sp_session_relogin = dlsym(h, "sp_session_relogin")) && (fptr_sp_session_logout = dlsym(h, "sp_session_logout")) && (fptr_sp_session_playlistcontainer = dlsym(h, "sp_session_playlistcontainer")) && (fptr_sp_session_process_events = dlsym(h, "sp_session_process_events")) && (fptr_sp_session_player_load = dlsym(h, "sp_session_player_load")) && (fptr_sp_session_player_unload = dlsym(h, "sp_session_player_unload")) && (fptr_sp_session_player_play = dlsym(h, "sp_session_player_play")) && (fptr_sp_session_player_seek = dlsym(h, "sp_session_player_seek")) && (fptr_sp_playlistcontainer_add_callbacks = dlsym(h, "sp_playlistcontainer_add_callbacks")) && (fptr_sp_playlistcontainer_num_playlists = dlsym(h, "sp_playlistcontainer_num_playlists")) && (fptr_sp_playlistcontainer_playlist = dlsym(h, "sp_playlistcontainer_playlist")) && (fptr_sp_playlist_add_callbacks = dlsym(h, "sp_playlist_add_callbacks")) && (fptr_sp_playlist_name = dlsym(h, "sp_playlist_name")) && (fptr_sp_playlist_remove_callbacks = dlsym(h, "sp_playlist_remove_callbacks")) && (fptr_sp_playlist_num_tracks = dlsym(h, "sp_playlist_num_tracks")) && (fptr_sp_playlist_track = dlsym(h, "sp_playlist_track")) && (fptr_sp_playlist_is_loaded = dlsym(h, "sp_playlist_is_loaded")) && (fptr_sp_track_error = dlsym(h, "sp_track_error")) && (fptr_sp_track_is_loaded = dlsym(h, "sp_track_is_loaded")) && (fptr_sp_track_name = dlsym(h, "sp_track_name")) && (fptr_sp_track_duration = dlsym(h, "sp_track_duration")) && (fptr_sp_track_index = dlsym(h, "sp_track_index")) && (fptr_sp_track_disc = dlsym(h, "sp_track_disc")) && (fptr_sp_track_album = dlsym(h, "sp_track_album")) && (fptr_sp_link_create_from_playlist = dlsym(h, "sp_link_create_from_playlist")) && (fptr_sp_link_create_from_track = dlsym(h, "sp_link_create_from_track")) && (fptr_sp_link_create_from_string = dlsym(h, "sp_link_create_from_string")) && (fptr_sp_link_as_string = dlsym(h, "sp_link_as_string")) && (fptr_sp_link_as_track = dlsym(h, "sp_link_as_track")) && (fptr_sp_link_release = dlsym(h, "sp_link_release")) && (fptr_sp_album_name = dlsym(h, "sp_album_name")) && (fptr_sp_album_artist = dlsym(h, "sp_album_artist")) && (fptr_sp_album_year = dlsym(h, "sp_album_year")) && (fptr_sp_album_type = dlsym(h, "sp_album_type")) && (fptr_sp_artist_name = dlsym(h, "sp_artist_name")) ; err = dlerror(); if (ret && !err) return ret; else if (err) DPRINTF(E_LOG, L_SPOTIFY, "Assignment error (%d): %s\n", ret, err); else DPRINTF(E_LOG, L_SPOTIFY, "Unknown assignment error (%d)\n", ret); return -1; } // End of ugly part /* -------------------------- PLAYLIST HELPERS ------------------------- */ static int spotify_metadata_get(sp_track *track, struct media_file_info *mfi) { sp_album *album; sp_artist *artist; sp_albumtype albumtype; album = fptr_sp_track_album(track); if (!album) return -1; artist = fptr_sp_album_artist(album); if (!artist) return -1; albumtype = fptr_sp_album_type(album); mfi->title = strdup(fptr_sp_track_name(track)); mfi->album = strdup(fptr_sp_album_name(album)); mfi->artist = strdup(fptr_sp_artist_name(artist)); mfi->year = fptr_sp_album_year(album); mfi->song_length = fptr_sp_track_duration(track); mfi->track = fptr_sp_track_index(track); mfi->disc = fptr_sp_track_disc(track); mfi->compilation = (albumtype == SP_ALBUMTYPE_COMPILATION); mfi->artwork = ARTWORK_SPOTIFY; mfi->type = strdup("spotify"); mfi->codectype = strdup("wav"); mfi->description = strdup("Spotify audio"); return 0; } static int spotify_track_save(int plid, sp_track *track) { struct media_file_info mfi; sp_link *link; char url[1024]; int ret; if (!fptr_sp_track_is_loaded(track)) { DPRINTF(E_INFO, L_SPOTIFY, "Metadata for track not ready yet\n"); return 0; } link = fptr_sp_link_create_from_track(track, 0); if (!link) { DPRINTF(E_LOG, L_SPOTIFY, "Could not create link for track\n"); return -1; } 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); /* Add to playlistitems table */ ret = db_pl_add_item_bypath(plid, url); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Could not save playlist item\n"); return -1; } memset(&mfi, 0, sizeof(struct media_file_info)); ret = spotify_metadata_get(track, &mfi); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Metadata missing (but track should be loaded?)\n"); free_mfi(&mfi, 1); return -1; } filescanner_process_media(url, time(NULL), 0, F_SCAN_TYPE_SPOTIFY, &mfi); free_mfi(&mfi, 1); return 0; } static int spotify_playlist_save(sp_playlist *pl) { struct playlist_info *pli; sp_track *track; sp_link *link; char url[1024]; const char *name; char title[512]; int plid; int num_tracks; int ret; int i; if (!fptr_sp_playlist_is_loaded(pl)) { DPRINTF(E_DBG, L_SPOTIFY, "Playlist still not loaded - wait for rename callback\n"); return 0; } name = fptr_sp_playlist_name(pl); DPRINTF(E_DBG, L_SPOTIFY, "Saving playlist: %s\n", name); /* Save playlist (playlists table) */ link = fptr_sp_link_create_from_playlist(pl); if (!link) { DPRINTF(E_LOG, L_SPOTIFY, "Could not create link for playlist (wait)\n"); return -1; } 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); sleep(1); // Primitive way of preventing database locking (the mutex wasn't working) pli = db_pl_fetch_bypath(url); snprintf(title, sizeof(title), "[s] %s", name); if (pli) { DPRINTF(E_DBG, L_SPOTIFY, "Playlist found (%s, link %s), updating\n", name, url); plid = pli->id; free_pli(pli, 0); ret = db_pl_update(title, url, plid); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Error updating playlist (%s, link %s)\n", name, url); return -1; } db_pl_ping(plid); db_pl_clear_items(plid); } else { DPRINTF(E_DBG, L_SPOTIFY, "Adding playlist (%s, link %s)\n", name, url); ret = db_pl_add(title, url, &plid); if ((ret < 0) || (plid < 1)) { DPRINTF(E_LOG, L_SPOTIFY, "Error adding playlist (%s, link %s, ret %d, plid %d)\n", name, url, ret, plid); return -1; } } /* Save tracks and playlistitems (files and playlistitems table) */ num_tracks = fptr_sp_playlist_num_tracks(pl); for (i = 0; i < num_tracks; i++) { track = fptr_sp_playlist_track(pl, i); if (!track) { DPRINTF(E_LOG, L_SPOTIFY, "Track %d in playlist %s (id %d) is invalid\n", i, name, plid); continue; } ret = spotify_track_save(plid, track); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Error saving track %d to playlist %s (id %d)\n", i, name, plid); continue; } } return plid; } /* -------------------------- AUDIO HELPER ------------------------------- */ static void spotify_audio_fifo_flush(void) { audio_fifo_data_t *afd; DPRINTF(E_DBG, L_SPOTIFY, "Flushing audio fifo\n"); pthread_mutex_lock(&g_audio_fifo->mutex); while((afd = TAILQ_FIRST(&g_audio_fifo->q))) { TAILQ_REMOVE(&g_audio_fifo->q, afd, link); free(afd); } g_audio_fifo->qlen = 0; g_audio_fifo->fullcount = 0; pthread_mutex_unlock(&g_audio_fifo->mutex); } /* -------------------------- PLAYLIST CALLBACKS ------------------------- */ /** * Called when a playlist is updating or is done updating * * This is called before and after a series of changes are applied to the * playlist. It allows e.g. the user interface to defer updating until the * entire operation is complete. * * @param[in] pl Playlist object * @param[in] done True iff the update is completed * @param[in] userdata Userdata passed to sp_playlist_add_callbacks() */ static void playlist_update_in_progress(sp_playlist *pl, bool done, void *userdata) { if (done) { DPRINTF(E_DBG, L_SPOTIFY, "Playlist update (status %d): %s\n", done, fptr_sp_playlist_name(pl)); pthread_mutex_lock(&g_db_mutex); spotify_playlist_save(pl); pthread_mutex_unlock(&g_db_mutex); } } 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)); pthread_mutex_lock(&g_db_mutex); spotify_playlist_save(pl); pthread_mutex_unlock(&g_db_mutex); } /** * The callbacks we are interested in for individual playlists. */ static sp_playlist_callbacks pl_callbacks = { .playlist_update_in_progress = &playlist_update_in_progress, .playlist_metadata_updated = &playlist_metadata_updated, }; /* -------------------- PLAYLIST CONTAINER CALLBACKS --------------------- */ /** * Callback from libspotify, telling us a playlist was added to the playlist container. * * We add our playlist callbacks to the newly added playlist. * * @param pc The playlist container handle * @param pl The playlist handle * @param position Index of the added playlist * @param userdata The opaque pointer */ static void playlist_added(sp_playlistcontainer *pc, sp_playlist *pl, int position, void *userdata) { DPRINTF(E_INFO, L_SPOTIFY, "Playlist added: %s (%d tracks)\n", fptr_sp_playlist_name(pl), fptr_sp_playlist_num_tracks(pl)); fptr_sp_playlist_add_callbacks(pl, &pl_callbacks, NULL); pthread_mutex_lock(&g_db_mutex); spotify_playlist_save(pl); pthread_mutex_unlock(&g_db_mutex); } /** * Callback from libspotify, telling us a playlist was removed from the playlist container. * * This is the place to remove our playlist callbacks. * * @param pc The playlist container handle * @param pl The playlist handle * @param position Index of the removed playlist * @param userdata The opaque pointer */ 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); link = fptr_sp_link_create_from_playlist(pl); if (!link) { DPRINTF(E_LOG, L_SPOTIFY, "Could not find link for deleted playlist\n"); 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); pthread_mutex_lock(&g_db_mutex); pli = db_pl_fetch_bypath(url); if (!pli) { DPRINTF(E_DBG, L_SPOTIFY, "Playlist %s not found, can't delete\n", url); pthread_mutex_unlock(&g_db_mutex); return; } plid = pli->id; free_pli(pli, 0); db_spotify_pl_delete(plid); pthread_mutex_unlock(&g_db_mutex); } /** * Callback from libspotify, telling us the rootlist is fully synchronized * * @param pc The playlist container handle * @param userdata The opaque pointer */ static void container_loaded(sp_playlistcontainer *pc, void *userdata) { int num; num = fptr_sp_playlistcontainer_num_playlists(pc); DPRINTF(E_INFO, L_SPOTIFY, "Rootlist synchronized (%d playlists)\n", num); } /** * The playlist container callbacks */ static sp_playlistcontainer_callbacks pc_callbacks = { .playlist_added = &playlist_added, .playlist_removed = &playlist_removed, .container_loaded = &container_loaded, }; /* --------------------------- SESSION CALLBACKS ------------------------- */ /** * This callback is called when an attempt to login has succeeded or failed. * * @sa sp_session_callbacks#logged_in */ static void logged_in(sp_session *sess, sp_error error) { sp_playlist *pl; sp_playlistcontainer *pc; int i; if (SP_ERROR_OK != error) { DPRINTF(E_LOG, L_SPOTIFY, "Login failed: %s\n", fptr_sp_error_message(error)); pthread_exit(NULL); } DPRINTF(E_LOG, L_SPOTIFY, "Login to Spotify succeeded. Reloading playlists.\n"); pthread_mutex_lock(&g_db_mutex); db_spotify_purge(); pthread_mutex_unlock(&g_db_mutex); pc = fptr_sp_session_playlistcontainer(sess); fptr_sp_playlistcontainer_add_callbacks(pc, &pc_callbacks, NULL); DPRINTF(E_DBG, L_SPOTIFY, "Found %d playlists\n", fptr_sp_playlistcontainer_num_playlists(pc)); for (i = 0; i < fptr_sp_playlistcontainer_num_playlists(pc); i++) { pl = fptr_sp_playlistcontainer_playlist(pc, i); fptr_sp_playlist_add_callbacks(pl, &pl_callbacks, NULL); } } /** * This callback is used from libspotify whenever there is PCM data available. * * @sa sp_session_callbacks#music_delivery */ static int music_delivery(sp_session *sess, const sp_audioformat *format, const void *frames, int num_frames) { audio_fifo_data_t *afd; size_t s; /* No support for resampling right now */ if ((format->sample_rate != 44100) || (format->channels != 2)) { DPRINTF(E_LOG, L_SPOTIFY, "Got music with unsupported samplerate or channels, stopping playback\n"); pthread_mutex_lock(&g_notify_mutex); g_event = SPOTIFY_EVENT_STOP; pthread_cond_signal(&g_notify_cond); pthread_mutex_unlock(&g_notify_mutex); return num_frames; } if (num_frames == 0) return 0; // Audio discontinuity, do nothing pthread_mutex_lock(&g_audio_fifo->mutex); /* Buffer three seconds of audio */ if (g_audio_fifo->qlen > (3 * format->sample_rate)) { // If the buffer has been full the last 300 times (~about a minute) we // assume the player thread paused/died without telling us, so we signal pause if (g_audio_fifo->fullcount < 300) g_audio_fifo->fullcount++; else { DPRINTF(E_WARN, L_SPOTIFY, "Buffer full more than 300 times, signaling pause\n"); pthread_mutex_lock(&g_notify_mutex); g_event = SPOTIFY_EVENT_PAUSE; pthread_cond_signal(&g_notify_cond); pthread_mutex_unlock(&g_notify_mutex); g_audio_fifo->fullcount = 0; } pthread_mutex_unlock(&g_audio_fifo->mutex); return 0; } else g_audio_fifo->fullcount = 0; s = num_frames * sizeof(int16_t) * format->channels; afd = malloc(sizeof(*afd) + s); memcpy(afd->samples, frames, s); afd->nsamples = num_frames; TAILQ_INSERT_TAIL(&g_audio_fifo->q, afd, link); g_audio_fifo->qlen += num_frames; pthread_cond_signal(&g_audio_fifo->cond); pthread_mutex_unlock(&g_audio_fifo->mutex); return num_frames; } /** * This callback is called from an internal libspotify thread to ask us to * reiterate the main loop. * * We notify the main thread using a condition variable and a protected variable. * * @sa sp_session_callbacks#notify_main_thread */ static void notify_main_thread(sp_session *sess) { DPRINTF(E_SPAM, L_SPOTIFY, "Notify main thread\n"); pthread_mutex_lock(&g_notify_mutex); g_notify = 1; pthread_cond_signal(&g_notify_cond); pthread_mutex_unlock(&g_notify_mutex); } /** * Called whenever metadata has been updated * * If you have metadata cached outside of libspotify, you should purge * your caches and fetch new versions. * * @param[in] session Session */ static void metadata_updated(sp_session *session) { DPRINTF(E_DBG, L_SPOTIFY, "Session metadata updated.\n"); } /** * Notification that some other connection has started playing on this account. * Playback has been stopped. * * @sa sp_session_callbacks#play_token_lost */ static void play_token_lost(sp_session *sess) { DPRINTF(E_DBG, L_SPOTIFY, "Play token lost - playback has been stopped\n"); pthread_mutex_lock(&g_notify_mutex); g_event = SPOTIFY_EVENT_EOT; pthread_cond_signal(&g_notify_cond); pthread_mutex_unlock(&g_notify_mutex); } /** * This callback is used from libspotify when the current track has ended * * @sa sp_session_callbacks#end_of_track */ static void end_of_track(sp_session *sess) { DPRINTF(E_DBG, L_SPOTIFY, "End of track\n"); pthread_mutex_lock(&g_notify_mutex); g_event = SPOTIFY_EVENT_EOT; pthread_cond_signal(&g_notify_cond); pthread_mutex_unlock(&g_notify_mutex); } /** * The session callbacks */ static sp_session_callbacks session_callbacks = { .logged_in = &logged_in, .notify_main_thread = ¬ify_main_thread, .music_delivery = &music_delivery, .metadata_updated = &metadata_updated, .play_token_lost = &play_token_lost, .log_message = NULL, .end_of_track = &end_of_track, }; /** * The session configuration. */ static sp_session_config spconfig = { .api_version = SPOTIFY_API_VERSION, .cache_location = NULL, .settings_location = NULL, .application_key = g_appkey, .application_key_size = sizeof(g_appkey), .user_agent = "forked-daapd", .callbacks = &session_callbacks, NULL, }; /* ------------------------- END SESSION CALLBACKS ----------------------- */ /* Thread: spotify */ static int playback_play(void) { sp_track *track; sp_error err; DPRINTF(E_DBG, L_SPOTIFY, "Starting playback\n"); if (!g_ctx.link) { DPRINTF(E_LOG, L_SPOTIFY, "Playback setup failed, no Spotify link"); return -1; } track = fptr_sp_link_as_track(g_ctx.link); if (!track) { DPRINTF(E_LOG, L_SPOTIFY, "Playback setup failed, invalid Spotify track"); return -1; } err = fptr_sp_session_player_load(g_sess, track); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Playback setup failed: %s\n", fptr_sp_error_message(err)); return -1; } spotify_audio_fifo_flush(); err = fptr_sp_session_player_play(g_sess, 1); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Playback failed: %s\n", fptr_sp_error_message(err)); return -1; } return 0; } /* Thread: spotify */ static int playback_pause(void) { sp_error err; DPRINTF(E_DBG, L_SPOTIFY, "Pausing playback\n"); err = fptr_sp_session_player_play(g_sess, 0); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Playback pause failed: %s\n", fptr_sp_error_message(err)); return -1; } return 0; } /* Thread: spotify */ static int playback_resume(void) { sp_error err; DPRINTF(E_DBG, L_SPOTIFY, "Resuming playback\n"); err = fptr_sp_session_player_play(g_sess, 1); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Playback resume failed: %s\n", fptr_sp_error_message(err)); return -1; } return 0; } /* Thread: spotify */ static int playback_stop(void) { sp_error err; DPRINTF(E_DBG, L_SPOTIFY, "Stopping playback\n"); err = fptr_sp_session_player_unload(g_sess); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Playback stop failed: %s\n", fptr_sp_error_message(err)); return -1; } return 0; } /* Thread: spotify */ static int playback_seek(void) { sp_error err; DPRINTF(E_DBG, L_SPOTIFY, "Playback seek\n"); err = fptr_sp_session_player_seek(g_sess, g_ctx.seek_ms); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Could not seek: %s\n", fptr_sp_error_message(err)); return -1; } spotify_audio_fifo_flush(); return 0; } /* Thread: spotify */ static void * spotify(void *arg) { struct timespec ts; enum spotify_event this_event; enum spotify_state state; int ret; int next_timeout; DPRINTF(E_DBG, L_SPOTIFY, "Main loop begin\n"); ret = db_perthread_init(); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Error: DB init failed\n"); pthread_exit(NULL); } state = SPOTIFY_STATE_WAIT; next_timeout = 0; for (;;) { pthread_mutex_lock(&g_notify_mutex); if (next_timeout == 0) { while(!g_notify) pthread_cond_wait(&g_notify_cond, &g_notify_mutex); } else { #if _POSIX_TIMERS > 0 clock_gettime(CLOCK_REALTIME, &ts); #else struct timeval tv; gettimeofday(&tv, NULL); TIMEVAL_TO_TIMESPEC(&tv, &ts); #endif ts.tv_sec += next_timeout / 1000; ts.tv_nsec += (next_timeout % 1000) * 1000000; while (!g_notify && (g_event == SPOTIFY_EVENT_NONE)) if (pthread_cond_timedwait(&g_notify_cond, &g_notify_mutex, &ts)) break; } this_event = g_event; g_notify = 0; g_event = SPOTIFY_EVENT_NONE; pthread_mutex_unlock(&g_notify_mutex); switch (this_event) { case SPOTIFY_EVENT_PLAY: if ((ret = playback_play()) == 0) state = SPOTIFY_STATE_PLAYING; else state = SPOTIFY_STATE_STOPPED; break; case SPOTIFY_EVENT_PAUSE: if ((ret = playback_pause()) == 0) state = SPOTIFY_STATE_PAUSED; else state = SPOTIFY_STATE_PLAYING; break; case SPOTIFY_EVENT_RESUME: if ((ret = playback_resume()) == 0) state = SPOTIFY_STATE_PLAYING; else state = SPOTIFY_STATE_PAUSED; break; case SPOTIFY_EVENT_EOT: playback_stop(); state = SPOTIFY_STATE_STOPPING; break; case SPOTIFY_EVENT_STOP: if ((ret = playback_stop()) == 0) state = SPOTIFY_STATE_STOPPED; else state = SPOTIFY_STATE_PLAYING; break; case SPOTIFY_EVENT_SEEK: if ((ret = playback_seek()) == 0) state = SPOTIFY_STATE_SEEKED; break; case SPOTIFY_EVENT_EXIT: ret = playback_stop(); fptr_sp_session_logout(g_sess); state = SPOTIFY_STATE_EXITING; break; default: ret = 0; state = 0; } if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Playback action failed (event code %d)\n", this_event); } if (state == SPOTIFY_STATE_EXITING) break; do { fptr_sp_session_process_events(g_sess, &next_timeout); } while (next_timeout == 0); pthread_mutex_lock(&g_state_mutex); if (state) { g_state = state; DPRINTF(E_DBG, L_SPOTIFY, "Event was %d, new state is %d\n", this_event, g_state); pthread_cond_signal(&g_state_cond); } pthread_mutex_unlock(&g_state_mutex); } db_perthread_deinit(); DPRINTF(E_DBG, L_SPOTIFY, "Main loop end\n"); pthread_exit(NULL); } /* ------------------------- PLAYER API ----------------------- */ /* Thread: player */ int spotify_playback_play(struct media_file_info *mfi) { enum spotify_state state; sp_link *link; DPRINTF(E_DBG, L_SPOTIFY, "Playback request\n"); link = fptr_sp_link_create_from_string(mfi->path); if (!link) { DPRINTF(E_LOG, L_SPOTIFY, "Playback setup failed, invalid Spotify link: %s\n", mfi->path); return -1; } pthread_mutex_lock(&g_notify_mutex); g_state = SPOTIFY_STATE_WAIT; g_event = SPOTIFY_EVENT_PLAY; if (g_ctx.link) fptr_sp_link_release(g_ctx.link); g_ctx.link = link; pthread_cond_signal(&g_notify_cond); pthread_mutex_unlock(&g_notify_mutex); // Wait until state changed so we know the event was processed pthread_mutex_lock(&g_state_mutex); while (g_state == SPOTIFY_STATE_WAIT) pthread_cond_wait(&g_state_cond, &g_state_mutex); state = g_state; pthread_mutex_unlock(&g_state_mutex); DPRINTF(E_DBG, L_SPOTIFY, "Playback reply\n"); if (state == SPOTIFY_STATE_PLAYING) return 0; else return -1; } /* Thread: player */ // This is not used by player.c, because the player pauses music by a // combintation of ending audio_get requests and seeking back int spotify_playback_pause(void) { enum spotify_state state; pthread_mutex_lock(&g_notify_mutex); g_state = SPOTIFY_STATE_WAIT; g_event = SPOTIFY_EVENT_PAUSE; pthread_cond_signal(&g_notify_cond); pthread_mutex_unlock(&g_notify_mutex); // Wait until state changed so we know the event was processed pthread_mutex_lock(&g_state_mutex); while (g_state == SPOTIFY_STATE_WAIT) pthread_cond_wait(&g_state_cond, &g_state_mutex); state = g_state; pthread_mutex_unlock(&g_state_mutex); if (state == SPOTIFY_STATE_PAUSED) return 0; else return -1; } /* Thread: player */ int spotify_playback_stop(void) { enum spotify_state state; DPRINTF(E_DBG, L_SPOTIFY, "Stop request\n"); pthread_mutex_lock(&g_notify_mutex); g_state = SPOTIFY_STATE_WAIT; g_event = SPOTIFY_EVENT_STOP; pthread_cond_signal(&g_notify_cond); pthread_mutex_unlock(&g_notify_mutex); // Wait until state changed so we know the event was processed pthread_mutex_lock(&g_state_mutex); while (g_state == SPOTIFY_STATE_WAIT) pthread_cond_wait(&g_state_cond, &g_state_mutex); state = g_state; pthread_mutex_unlock(&g_state_mutex); DPRINTF(E_DBG, L_SPOTIFY, "Stop reply\n"); if (state == SPOTIFY_STATE_STOPPED) return 0; else return -1; } /* Thread: player */ int spotify_playback_seek(int ms) { pthread_mutex_lock(&g_notify_mutex); g_state = SPOTIFY_STATE_WAIT; g_event = SPOTIFY_EVENT_SEEK; g_ctx.seek_ms = ms; pthread_cond_signal(&g_notify_cond); pthread_mutex_unlock(&g_notify_mutex); // Wait until state changed so we know the event was processed pthread_mutex_lock(&g_state_mutex); while (g_state == SPOTIFY_STATE_WAIT) pthread_cond_wait(&g_state_cond, &g_state_mutex); pthread_mutex_unlock(&g_state_mutex); return ms; } /* Thread: player */ int spotify_audio_get(struct evbuffer *evbuf, int wanted) { struct timespec ts; audio_fifo_data_t *afd; int processed; int ret; int s; afd = NULL; processed = 0; // If spotify was paused begin by resuming playback if (g_state == SPOTIFY_STATE_PAUSED) { pthread_mutex_lock(&g_notify_mutex); g_event = SPOTIFY_EVENT_RESUME; pthread_cond_signal(&g_notify_cond); pthread_mutex_unlock(&g_notify_mutex); } pthread_mutex_lock(&g_audio_fifo->mutex); while ((processed < wanted) && (g_state != SPOTIFY_STATE_STOPPED)) { // If track has ended and buffer is empty if ((g_state == SPOTIFY_STATE_STOPPING) && (g_audio_fifo->qlen <= 0)) g_state = SPOTIFY_STATE_STOPPED; // If buffer is empty wait for audio, but use timed wait so we don't // risk waiting forever (maybe the player stopped while we were waiting) while ( !(afd = TAILQ_FIRST(&g_audio_fifo->q)) && (g_state != SPOTIFY_STATE_STOPPED) && (g_state != SPOTIFY_STATE_STOPPING) ) { DPRINTF(E_DBG, L_SPOTIFY, "Waiting for audio\n"); #if _POSIX_TIMERS > 0 clock_gettime(CLOCK_REALTIME, &ts); #else struct timeval tv; gettimeofday(&tv, NULL); TIMEVAL_TO_TIMESPEC(&tv, &ts); #endif ts.tv_sec += 5; pthread_cond_timedwait(&g_audio_fifo->cond, &g_audio_fifo->mutex, &ts); } if (!afd) break; TAILQ_REMOVE(&g_audio_fifo->q, afd, link); g_audio_fifo->qlen -= afd->nsamples; s = afd->nsamples * sizeof(int16_t) * 2; ret = evbuffer_add(evbuf, afd->samples, s); free(afd); afd = NULL; if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Out of memory for evbuffer (tried to add %d bytes)\n", s); pthread_mutex_unlock(&g_audio_fifo->mutex); return -1; } processed += s; } pthread_mutex_unlock(&g_audio_fifo->mutex); return processed; } /* Thread: filescanner */ void spotify_login(char *path) { char buf[256]; FILE *fp; char *username; char *password; int len; int ret; sp_error err; if (!g_sess) { DPRINTF(E_LOG, L_SPOTIFY, "Can't login! No valid Spotify session.\n"); return; } fp = fopen(path, "rb"); if (!fp) { DPRINTF(E_LOG, L_SPOTIFY, "Could not open Spotify credentials file %s: %s\n", path, strerror(errno)); return; } username = fgets(buf, sizeof(buf), fp); if (!username) { DPRINTF(E_LOG, L_SPOTIFY, "Empty Spotify credentials file %s\n", path); fclose(fp); return; } len = strlen(username); if (buf[len - 1] != '\n') { DPRINTF(E_LOG, L_SPOTIFY, "Invalid Spotify credentials file %s: username name too long or missing password\n", path); fclose(fp); return; } while (len) { if ((buf[len - 1] == '\r') || (buf[len - 1] == '\n')) { buf[len - 1] = '\0'; len--; } else break; } if (!len) { DPRINTF(E_LOG, L_SPOTIFY, "Invalid Spotify credentials file %s: empty line where username expected\n", path); fclose(fp); return; } username = strdup(buf); if (!username) { DPRINTF(E_LOG, L_SPOTIFY, "Out of memory for username while reading %s\n", path); fclose(fp); return; } password = fgets(buf, sizeof(buf), fp); fclose(fp); if (!password) { DPRINTF(E_LOG, L_SPOTIFY, "Invalid Spotify credentials file %s: no password\n", path); free(username); return; } len = strlen(password); while (len) { if ((buf[len - 1] == '\r') || (buf[len - 1] == '\n')) { buf[len - 1] = '\0'; len--; } else break; } password = strdup(buf); if (!password) { DPRINTF(E_LOG, L_SPOTIFY, "Out of memory for password while reading %s\n", path); free(username); return; } if (g_state != SPOTIFY_STATE_INACTIVE) { DPRINTF(E_DBG, L_SPOTIFY, "Killing previous Spotify thread\n"); pthread_mutex_lock(&g_notify_mutex); g_event = SPOTIFY_EVENT_EXIT; pthread_cond_signal(&g_notify_cond); pthread_mutex_unlock(&g_notify_mutex); pthread_join(tid_spotify, NULL); g_state = SPOTIFY_STATE_INACTIVE; } DPRINTF(E_DBG, L_SPOTIFY, "Spotify credentials file OK, logging in with %s/%s\n", username, password); err = fptr_sp_session_login(g_sess, username, password, 1, NULL); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Could not login into Spotify: %s\n", fptr_sp_error_message(err)); return; } ret = pthread_create(&tid_spotify, NULL, spotify, NULL); if (ret < 0) { DPRINTF(E_LOG, L_SPOTIFY, "Could not spawn Spotify thread: %s\n", strerror(errno)); return; } } /* Thread: main */ int spotify_init(void) { cfg_t *lib; sp_session *sp; sp_error err; int ret; /* Initialize libspotify */ g_libhandle = dlopen("libspotify.so", RTLD_LAZY); if (!g_libhandle) { DPRINTF(E_INFO, L_SPOTIFY, "libspotify.so not installed or not found\n"); return -1; } DPRINTF(E_INFO, L_SPOTIFY, "Spotify session init\n"); ret = fptr_assign_all(); if (ret < 0) return -1; /* Initialize session */ g_notify = 0; g_event = SPOTIFY_EVENT_NONE; g_state = SPOTIFY_STATE_INACTIVE; lib = cfg_getsec(cfg, "spotify"); spconfig.settings_location = cfg_getstr(lib, "settings_dir"); spconfig.cache_location = cfg_getstr(lib, "cache_dir"); DPRINTF(E_DBG, L_SPOTIFY, "Creating Spotify session\n"); err = fptr_sp_session_create(&spconfig, &sp); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Could not create Spotify session: %s\n", fptr_sp_error_message(err)); return -1; } g_sess = sp; /* Prepare thread and audio buffer */ pthread_mutex_init(&g_notify_mutex, NULL); pthread_cond_init(&g_notify_cond, NULL); pthread_mutex_init(&g_state_mutex, NULL); pthread_cond_init(&g_state_cond, NULL); pthread_mutex_init(&g_db_mutex, NULL); g_audio_fifo = (audio_fifo_t *)malloc(sizeof(audio_fifo_t)); TAILQ_INIT(&g_audio_fifo->q); g_audio_fifo->qlen = 0; pthread_mutex_init(&g_audio_fifo->mutex, NULL); pthread_cond_init(&g_audio_fifo->cond, NULL); /* Log in and spawn thread */ DPRINTF(E_DBG, L_SPOTIFY, "Logging into Spotify\n"); err = fptr_sp_session_relogin(sp); if (SP_ERROR_OK != err) { DPRINTF(E_LOG, L_SPOTIFY, "Could not login into Spotify: %s\n", fptr_sp_error_message(err)); return -1; } ret = pthread_create(&tid_spotify, NULL, spotify, NULL); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not spawn Spotify thread: %s\n", strerror(errno)); return -1; } DPRINTF(E_DBG, L_SPOTIFY, "Spotify init complete\n"); return 0; } void spotify_deinit(void) { int ret; /* libspotify not installed or no session - just exit */ if (!g_libhandle || !g_sess) return; /* Send exit signal to thread (if active) */ if (g_state != SPOTIFY_STATE_INACTIVE) { pthread_mutex_lock(&g_notify_mutex); g_event = SPOTIFY_EVENT_EXIT; pthread_cond_signal(&g_notify_cond); pthread_mutex_unlock(&g_notify_mutex); ret = pthread_join(tid_spotify, NULL); if (ret != 0) { DPRINTF(E_FATAL, L_SPOTIFY, "Could not join Spotify thread: %s\n", strerror(errno)); return; } } /* Release session and destroy pthread mutex/cond */ fptr_sp_session_release(g_sess); DPRINTF(E_SPAM, L_SPOTIFY, "Destroy pthread mutex and cond\n"); pthread_cond_destroy(&g_notify_cond); pthread_mutex_destroy(&g_notify_mutex); pthread_cond_destroy(&g_state_cond); pthread_mutex_destroy(&g_state_mutex); pthread_mutex_destroy(&g_db_mutex); pthread_cond_destroy(&g_audio_fifo->cond); pthread_mutex_destroy(&g_audio_fifo->mutex); /* Free audio buffer */ DPRINTF(E_SPAM, L_SPOTIFY, "Free audio fifo\n"); free(g_audio_fifo); /* Release libspotify handle */ if (g_libhandle) dlclose(g_libhandle); }