summaryrefslogtreecommitdiff
path: root/src/libpiano/piano.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/libpiano/piano.c')
-rw-r--r--src/libpiano/piano.c1114
1 files changed, 1114 insertions, 0 deletions
diff --git a/src/libpiano/piano.c b/src/libpiano/piano.c
new file mode 100644
index 0000000..f6343b4
--- /dev/null
+++ b/src/libpiano/piano.c
@@ -0,0 +1,1114 @@
+/*
+Copyright (c) 2008-2010
+ Lars-Dominik Braun <PromyLOPh@lavabit.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#define _BSD_SOURCE /* required by strdup() */
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <time.h>
+#include <assert.h>
+#include <stdint.h>
+
+/* needed for urlencode */
+#include <waitress.h>
+
+#include "piano_private.h"
+#include "piano.h"
+#include "xml.h"
+#include "crypt.h"
+#include "config.h"
+
+#define PIANO_PROTOCOL_VERSION "29"
+#define PIANO_RPC_HOST "www.pandora.com"
+#define PIANO_RPC_PORT "80"
+#define PIANO_RPC_PATH "/radio/xmlrpc/v" PIANO_PROTOCOL_VERSION "?"
+#define PIANO_SEND_BUFFER_SIZE 10000
+
+/* initialize piano handle
+ * @param piano handle
+ * @return nothing
+ */
+void PianoInit (PianoHandle_t *ph) {
+ memset (ph, 0, sizeof (*ph));
+
+ /* route-id seems to be random. we're using time anyway... */
+ snprintf (ph->routeId, sizeof (ph->routeId), "%07luP",
+ (unsigned long) time (NULL) % 10000000);
+}
+
+/* free complete search result
+ * @public yes
+ * @param search result
+ */
+void PianoDestroySearchResult (PianoSearchResult_t *searchResult) {
+ PianoArtist_t *curArtist, *lastArtist;
+ PianoSong_t *curSong, *lastSong;
+
+ curArtist = searchResult->artists;
+ while (curArtist != NULL) {
+ free (curArtist->name);
+ free (curArtist->musicId);
+ lastArtist = curArtist;
+ curArtist = curArtist->next;
+ free (lastArtist);
+ }
+
+ curSong = searchResult->songs;
+ while (curSong != NULL) {
+ free (curSong->title);
+ free (curSong->artist);
+ free (curSong->musicId);
+ lastSong = curSong;
+ curSong = curSong->next;
+ free (lastSong);
+ }
+}
+
+/* free single station
+ * @param station
+ */
+void PianoDestroyStation (PianoStation_t *station) {
+ free (station->name);
+ free (station->id);
+ memset (station, 0, sizeof (station));
+}
+
+/* free complete station list
+ * @param piano handle
+ */
+void PianoDestroyStations (PianoStation_t *stations) {
+ PianoStation_t *curStation, *lastStation;
+
+ curStation = stations;
+ while (curStation != NULL) {
+ lastStation = curStation;
+ curStation = curStation->next;
+ PianoDestroyStation (lastStation);
+ free (lastStation);
+ }
+}
+
+/* FIXME: copy & waste */
+/* free _all_ elements of playlist
+ * @param piano handle
+ * @return nothing
+ */
+void PianoDestroyPlaylist (PianoSong_t *playlist) {
+ PianoSong_t *curSong, *lastSong;
+
+ curSong = playlist;
+ while (curSong != NULL) {
+ free (curSong->audioUrl);
+ free (curSong->coverArt);
+ free (curSong->artist);
+ free (curSong->musicId);
+ free (curSong->title);
+ free (curSong->userSeed);
+ free (curSong->stationId);
+ free (curSong->album);
+ free (curSong->artistMusicId);
+ lastSong = curSong;
+ curSong = curSong->next;
+ free (lastSong);
+ }
+}
+
+/* destroy genre linked list
+ */
+void PianoDestroyGenres (PianoGenre_t *genres) {
+ PianoGenre_t *curGenre, *lastGenre;
+
+ curGenre = genres;
+ while (curGenre != NULL) {
+ free (curGenre->name);
+ free (curGenre->musicId);
+ lastGenre = curGenre;
+ curGenre = curGenre->next;
+ free (lastGenre);
+ }
+}
+
+/* destroy user information
+ */
+void PianoDestroyUserInfo (PianoUserInfo_t *user) {
+ free (user->webAuthToken);
+ free (user->authToken);
+ free (user->listenerId);
+}
+
+/* frees the whole piano handle structure
+ * @param piano handle
+ * @return nothing
+ */
+void PianoDestroy (PianoHandle_t *ph) {
+ PianoDestroyUserInfo (&ph->user);
+ PianoDestroyStations (ph->stations);
+ /* destroy genre stations */
+ PianoGenreCategory_t *curGenreCat = ph->genreStations, *lastGenreCat;
+ while (curGenreCat != NULL) {
+ PianoDestroyGenres (curGenreCat->genres);
+ free (curGenreCat->name);
+ lastGenreCat = curGenreCat;
+ curGenreCat = curGenreCat->next;
+ free (lastGenreCat);
+ }
+ memset (ph, 0, sizeof (*ph));
+}
+
+/* destroy request, free post data. req->responseData is *not* freed here, as
+ * it might be allocated by something else than malloc!
+ * @param piano request
+ */
+void PianoDestroyRequest (PianoRequest_t *req) {
+ free (req->postData);
+ memset (req, 0, sizeof (*req));
+}
+
+/* convert audio format id to string that can be used in xml requests
+ * @param format id
+ * @return constant string
+ */
+static const char *PianoAudioFormatToString (PianoAudioFormat_t format) {
+ switch (format) {
+ case PIANO_AF_AACPLUS:
+ return "aacplus";
+ break;
+
+ case PIANO_AF_MP3:
+ return "mp3";
+ break;
+
+ case PIANO_AF_MP3_HI:
+ return "mp3-hifi";
+ break;
+
+ default:
+ return NULL;
+ break;
+ }
+}
+
+/* prepare piano request (initializes request type, urlpath and postData)
+ * @param piano handle
+ * @param request structure
+ * @param request type
+ */
+PianoReturn_t PianoRequest (PianoHandle_t *ph, PianoRequest_t *req,
+ PianoRequestType_t type) {
+ char xmlSendBuf[PIANO_SEND_BUFFER_SIZE];
+ /* corrected timestamp */
+ time_t timestamp = time (NULL) - ph->timeOffset;
+
+ assert (ph != NULL);
+ assert (req != NULL);
+
+ req->type = type;
+
+ switch (req->type) {
+ case PIANO_REQUEST_LOGIN: {
+ /* authenticate user */
+ PianoRequestDataLogin_t *logindata = req->data;
+
+ assert (logindata != NULL);
+
+ switch (logindata->step) {
+ case 0:
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf),
+ "<?xml version=\"1.0\"?><methodCall>"
+ "<methodName>misc.sync</methodName>"
+ "<params></params></methodCall>");
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&method=sync", ph->routeId);
+ break;
+
+ case 1:
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf),
+ "<?xml version=\"1.0\"?><methodCall>"
+ "<methodName>listener.authenticateListener</methodName>"
+ "<params><param><value><int>%lu</int></value></param>"
+ "<param><value><string>%s</string></value></param>"
+ "<param><value><string>%s</string></value></param>"
+ "</params></methodCall>", (unsigned long) timestamp,
+ logindata->user, logindata->password);
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&method=authenticateListener", ph->routeId);
+ break;
+ }
+ break;
+ }
+
+ case PIANO_REQUEST_GET_STATIONS:
+ /* get stations, user must be authenticated */
+ assert (ph->user.listenerId != NULL);
+
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf), "<?xml version=\"1.0\"?>"
+ "<methodCall><methodName>station.getStations</methodName>"
+ "<params><param><value><int>%lu</int></value></param>"
+ "<param><value><string>%s</string></value></param>"
+ "</params></methodCall>", (unsigned long) timestamp,
+ ph->user.authToken);
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&lid=%s&method=getStations", ph->routeId,
+ ph->user.listenerId);
+ break;
+
+ case PIANO_REQUEST_GET_PLAYLIST: {
+ /* get playlist for specified station */
+ PianoRequestDataGetPlaylist_t *reqData = req->data;
+
+ assert (reqData != NULL);
+ assert (reqData->station != NULL);
+ assert (reqData->station->id != NULL);
+ assert (reqData->format != PIANO_AF_UNKNOWN);
+
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf), "<?xml version=\"1.0\"?>"
+ "<methodCall><methodName>playlist.getFragment</methodName>"
+ "<params><param><value><int>%lu</int></value></param>"
+ /* auth token */
+ "<param><value><string>%s</string></value></param>"
+ /* station id */
+ "<param><value><string>%s</string></value></param>"
+ /* total listening time */
+ "<param><value><string>0</string></value></param>"
+ /* time since last session */
+ "<param><value><string></string></value></param>"
+ /* tracking code */
+ "<param><value><string></string></value></param>"
+ /* audio format */
+ "<param><value><string>%s</string></value></param>"
+ /* delta listening time */
+ "<param><value><string>0</string></value></param>"
+ /* listening timestamp */
+ "<param><value><string>0</string></value></param>"
+ "</params></methodCall>", (unsigned long) timestamp,
+ ph->user.authToken, reqData->station->id,
+ PianoAudioFormatToString (reqData->format));
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&lid=%s&method=getFragment&arg1=%s&arg2=0"
+ "&arg3=&arg4=&arg5=%s&arg6=0&arg7=0", ph->routeId,
+ ph->user.listenerId, reqData->station->id,
+ PianoAudioFormatToString (reqData->format));
+ break;
+ }
+
+ case PIANO_REQUEST_ADD_FEEDBACK: {
+ /* low-level, don't use directly (see _RATE_SONG and _MOVE_SONG) */
+ PianoRequestDataAddFeedback_t *reqData = req->data;
+
+ assert (reqData != NULL);
+ assert (reqData->stationId != NULL);
+ assert (reqData->musicId != NULL);
+ assert (reqData->rating != PIANO_RATE_NONE);
+
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf), "<?xml version=\"1.0\"?>"
+ "<methodCall><methodName>station.addFeedback</methodName>"
+ "<params><param><value><int>%lu</int></value></param>"
+ /* auth token */
+ "<param><value><string>%s</string></value></param>"
+ /* station id */
+ "<param><value><string>%s</string></value></param>"
+ /* music id */
+ "<param><value><string>%s</string></value></param>"
+ /* user seed */
+ "<param><value><string>%s</string></value></param>"
+ /* test strategy */
+ "<param><value>%u</value></param>"
+ /* positive */
+ "<param><value><boolean>%i</boolean></value></param>"
+ /* "is-creator-quickmix" */
+ "<param><value><boolean>0</boolean></value></param>"
+ /* song type */
+ "<param><value><int>%u</int></value></param>"
+ "</params></methodCall>", (unsigned long) timestamp,
+ ph->user.authToken, reqData->stationId, reqData->musicId,
+ (reqData->userSeed == NULL) ? "" : reqData->userSeed,
+ reqData->testStrategy,
+ (reqData->rating == PIANO_RATE_LOVE) ? 1 : 0,
+ reqData->songType);
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&lid=%s&method=addFeedback&arg1=%s&arg2=%s"
+ "&arg3=%s&arg4=%u&arg5=%s&arg6=false&arg7=%u",
+ ph->routeId, ph->user.listenerId, reqData->stationId,
+ reqData->musicId,
+ (reqData->userSeed == NULL) ? "" : reqData->userSeed,
+ reqData->testStrategy,
+ (reqData->rating == PIANO_RATE_LOVE) ? "true" : "false",
+ reqData->songType);
+ break;
+ }
+
+ case PIANO_REQUEST_RENAME_STATION: {
+ /* rename stations */
+ PianoRequestDataRenameStation_t *reqData = req->data;
+ char *urlencodedNewName, *xmlencodedNewName;
+
+ assert (reqData != NULL);
+ assert (reqData->station != NULL);
+ assert (reqData->newName != NULL);
+
+ if ((xmlencodedNewName = PianoXmlEncodeString (reqData->newName)) == NULL) {
+ return PIANO_RET_OUT_OF_MEMORY;
+ }
+ urlencodedNewName = WaitressUrlEncode (reqData->newName);
+
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf), "<?xml version=\"1.0\"?>"
+ "<methodCall><methodName>station.setStationName</methodName>"
+ "<params><param><value><int>%lu</int></value></param>"
+ /* auth token */
+ "<param><value><string>%s</string></value></param>"
+ /* station id */
+ "<param><value><string>%s</string></value></param>"
+ /* new name */
+ "<param><value><string>%s</string></value></param>"
+ "</params></methodCall>", (unsigned long) timestamp,
+ ph->user.authToken, reqData->station->id,
+ xmlencodedNewName);
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&lid=%s&method=setStationName&arg1=%s&arg2=%s",
+ ph->routeId, ph->user.listenerId, reqData->station->id,
+ urlencodedNewName);
+
+ free (urlencodedNewName);
+ free (xmlencodedNewName);
+ break;
+ }
+
+ case PIANO_REQUEST_DELETE_STATION: {
+ /* delete station */
+ PianoStation_t *station = req->data;
+
+ assert (station != NULL);
+
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf), "<?xml version=\"1.0\"?>"
+ "<methodCall><methodName>station.removeStation</methodName>"
+ "<params><param><value><int>%lu</int></value></param>"
+ /* auth token */
+ "<param><value><string>%s</string></value></param>"
+ /* station id */
+ "<param><value><string>%s</string></value></param>"
+ "</params></methodCall>", (unsigned long) timestamp,
+ ph->user.authToken, station->id);
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&lid=%s&method=removeStation&arg1=%s", ph->routeId,
+ ph->user.listenerId, station->id);
+ break;
+ }
+
+ case PIANO_REQUEST_SEARCH: {
+ /* search for artist/song title */
+ PianoRequestDataSearch_t *reqData = req->data;
+ char *xmlencodedSearchStr, *urlencodedSearchStr;
+
+ assert (reqData != NULL);
+ assert (reqData->searchStr != NULL);
+
+ if ((xmlencodedSearchStr = PianoXmlEncodeString (reqData->searchStr)) == NULL) {
+ return PIANO_RET_OUT_OF_MEMORY;
+ }
+ urlencodedSearchStr = WaitressUrlEncode (reqData->searchStr);
+
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf), "<?xml version=\"1.0\"?>"
+ "<methodCall><methodName>music.search</methodName>"
+ "<params><param><value><int>%lu</int></value></param>"
+ /* auth token */
+ "<param><value><string>%s</string></value></param>"
+ /* search string */
+ "<param><value><string>%s</string></value></param>"
+ "</params></methodCall>", (unsigned long) timestamp,
+ ph->user.authToken, xmlencodedSearchStr);
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&lid=%s&method=search&arg1=%s", ph->routeId,
+ ph->user.listenerId, urlencodedSearchStr);
+
+ free (urlencodedSearchStr);
+ free (xmlencodedSearchStr);
+ break;
+ }
+
+ case PIANO_REQUEST_CREATE_STATION: {
+ /* create new station from specified musicid (type=mi, get one by
+ * performing a search) or shared station id (type=sh) */
+ PianoRequestDataCreateStation_t *reqData = req->data;
+
+ assert (reqData != NULL);
+ assert (reqData->id != NULL);
+ assert (reqData->type != NULL);
+
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf), "<?xml version=\"1.0\"?>"
+ "<methodCall><methodName>station.createStation</methodName>"
+ "<params><param><value><int>%lu</int></value></param>"
+ "<param><value><string>%s</string></value></param>"
+ "<param><value><string>%s%s</string></value></param>"
+ "</params></methodCall>", (unsigned long) timestamp,
+ ph->user.authToken, reqData->type, reqData->id);
+
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&lid=%s&method=createStation&arg1=%s%s", ph->routeId,
+ ph->user.listenerId, reqData->type, reqData->id);
+ break;
+ }
+
+ case PIANO_REQUEST_ADD_SEED: {
+ /* add another seed to specified station */
+ PianoRequestDataAddSeed_t *reqData = req->data;
+
+ assert (reqData != NULL);
+ assert (reqData->station != NULL);
+ assert (reqData->musicId != NULL);
+
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf), "<?xml version=\"1.0\"?>"
+ "<methodCall><methodName>station.addSeed</methodName><params>"
+ "<param><value><int>%lu</int></value></param>"
+ "<param><value><string>%s</string></value></param>"
+ "<param><value><string>%s</string></value></param>"
+ "<param><value><string>%s</string></value></param>"
+ "</params></methodCall>", (unsigned long) timestamp,
+ ph->user.authToken, reqData->station->id, reqData->musicId);
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&lid=%s&method=addSeed&arg1=%s&arg2=%s", ph->routeId,
+ ph->user.listenerId, reqData->station->id, reqData->musicId);
+ break;
+ }
+
+ case PIANO_REQUEST_ADD_TIRED_SONG: {
+ /* ban song for a month from all stations */
+ PianoSong_t *song = req->data;
+
+ assert (song != NULL);
+
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf), "<?xml version=\"1.0\"?>"
+ "<methodCall><methodName>listener.addTiredSong</methodName><params>"
+ "<param><value><int>%lu</int></value></param>"
+ "<param><value><string>%s</string></value></param>"
+ /* key */
+ "<param><value><string>%s</string></value></param>"
+ /* user seed */
+ "<param><value><string>%s</string></value></param>"
+ /* station id */
+ "<param><value><string>%s</string></value></param>"
+ "</params></methodCall>", (unsigned long) timestamp,
+ ph->user.authToken,
+ (song->musicId == NULL) ? "" : song->musicId,
+ (song->userSeed == NULL) ? "" : song->userSeed,
+ song->stationId);
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&lid=%s&method=addTiredSong&arg1=%s&arg2=%s&arg3=%s",
+ ph->routeId, ph->user.listenerId,
+ (song->musicId == NULL) ? "" : song->musicId,
+ (song->userSeed == NULL) ? "" : song->userSeed,
+ song->stationId);
+ break;
+ }
+
+ case PIANO_REQUEST_SET_QUICKMIX: {
+ /* select stations included in quickmix (see useQuickMix flag of
+ * PianoStation_t) */
+ char valueBuf[1000], urlArgBuf[1000];
+ PianoStation_t *curStation = ph->stations;
+
+ memset (urlArgBuf, 0, sizeof (urlArgBuf));
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf), "<?xml version=\"1.0\"?>"
+ "<methodCall><methodName>station.setQuickMix</methodName><params>"
+ "<param><value><int>%lu</int></value></param>"
+ "<param><value><string>%s</string></value></param>"
+ /* quick mix type */
+ "<param><value><string>RANDOM</string></value></param>"
+ "<param><value><array><data>", (unsigned long) timestamp,
+ ph->user.authToken);
+ while (curStation != NULL) {
+ /* quick mix can't contain itself */
+ if (!curStation->useQuickMix || curStation->isQuickMix) {
+ curStation = curStation->next;
+ continue;
+ }
+ /* append to xml doc */
+ snprintf (valueBuf, sizeof (valueBuf),
+ "<value><string>%s</string></value>", curStation->id);
+ strncat (xmlSendBuf, valueBuf, sizeof (xmlSendBuf) -
+ strlen (xmlSendBuf) - 1);
+ /* append to url arg */
+ strncat (urlArgBuf, curStation->id, sizeof (urlArgBuf) -
+ strlen (urlArgBuf) - 1);
+ curStation = curStation->next;
+ /* if not last item: append "," */
+ if (curStation != NULL) {
+ strncat (urlArgBuf, "%2C", sizeof (urlArgBuf) -
+ strlen (urlArgBuf) - 1);
+ }
+ }
+ strncat (xmlSendBuf,
+ "</data></array></value></param>"
+ /* content type */
+ "<param><value><string>CUSTOM</string></value></param>"
+ /* genre */
+ "<param><value><string></string></value></param>"
+ "</params></methodCall>",
+ sizeof (xmlSendBuf) - strlen (xmlSendBuf) - 1);
+
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&lid=%s&method=setQuickMix&arg1=RANDOM&arg2=%s&arg3=CUSTOM&arg4=",
+ ph->routeId, ph->user.listenerId, urlArgBuf);
+ break;
+ }
+
+ case PIANO_REQUEST_GET_GENRE_STATIONS:
+ /* receive list of pandora's genre stations */
+ xmlSendBuf[0] = '\0';
+ snprintf (req->urlPath, sizeof (req->urlPath), "/xml/genre?r=%lu",
+ (unsigned long) timestamp);
+ break;
+
+ case PIANO_REQUEST_TRANSFORM_STATION: {
+ /* transform shared station into private */
+ PianoStation_t *station = req->data;
+
+ assert (station != NULL);
+
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf), "<?xml version=\"1.0\"?>"
+ "<methodCall><methodName>station.transformShared</methodName>"
+ "<params><param><value><int>%lu</int></value></param>"
+ /* auth token */
+ "<param><value><string>%s</string></value></param>"
+ /* station id */
+ "<param><value><string>%s</string></value></param>"
+ "</params></methodCall>", (unsigned long) timestamp,
+ ph->user.authToken, station->id);
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&lid=%s&method=transformShared&arg1=%s", ph->routeId,
+ ph->user.listenerId, station->id);
+ break;
+ }
+
+ case PIANO_REQUEST_EXPLAIN: {
+ /* explain why particular song was played */
+ PianoRequestDataExplain_t *reqData = req->data;
+
+ assert (reqData != NULL);
+ assert (reqData->song != NULL);
+
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf), "<?xml version=\"1.0\"?>"
+ "<methodCall><methodName>playlist.narrative</methodName>"
+ "<params><param><value><int>%lu</int></value></param>"
+ /* auth token */
+ "<param><value><string>%s</string></value></param>"
+ /* station id */
+ "<param><value><string>%s</string></value></param>"
+ /* music id */
+ "<param><value><string>%s</string></value></param>"
+ "</params></methodCall>", (unsigned long) timestamp,
+ ph->user.authToken, reqData->song->stationId,
+ reqData->song->musicId);
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&lid=%s&method=narrative&arg1=%s&arg2=%s",
+ ph->routeId, ph->user.listenerId, reqData->song->stationId,
+ reqData->song->musicId);
+ break;
+ }
+
+ case PIANO_REQUEST_GET_SEED_SUGGESTIONS: {
+ /* find similar artists */
+ PianoRequestDataGetSeedSuggestions_t *reqData = req->data;
+
+ assert (reqData != NULL);
+ assert (reqData->musicId != NULL);
+ assert (reqData->max != 0);
+
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf), "<?xml version=\"1.0\"?>"
+ "<methodCall><methodName>music.getSeedSuggestions</methodName>"
+ "<params><param><value><int>%lu</int></value></param>"
+ /* auth token */
+ "<param><value><string>%s</string></value></param>"
+ /* seed music id */
+ "<param><value><string>%s</string></value></param>"
+ /* max */
+ "<param><value><int>%u</int></value></param>"
+ "</params></methodCall>", (unsigned long) timestamp,
+ ph->user.authToken, reqData->musicId, reqData->max);
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&lid=%s&method=getSeedSuggestions&arg1=%s&arg2=%u",
+ ph->routeId, ph->user.listenerId, reqData->musicId, reqData->max);
+ break;
+ }
+
+ case PIANO_REQUEST_BOOKMARK_SONG: {
+ /* bookmark song */
+ PianoSong_t *song = req->data;
+
+ assert (song != NULL);
+
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf), "<?xml version=\"1.0\"?>"
+ "<methodCall><methodName>station.createBookmark</methodName>"
+ "<params><param><value><int>%lu</int></value></param>"
+ /* auth token */
+ "<param><value><string>%s</string></value></param>"
+ /* station id */
+ "<param><value><string>%s</string></value></param>"
+ /* music id */
+ "<param><value><string>%s</string></value></param>"
+ "</params></methodCall>", (unsigned long) timestamp,
+ ph->user.authToken, song->stationId, song->musicId);
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&lid=%s&method=createBookmark&arg1=%s&arg2=%s",
+ ph->routeId, ph->user.listenerId, song->stationId,
+ song->musicId);
+ break;
+ }
+
+ case PIANO_REQUEST_BOOKMARK_ARTIST: {
+ /* bookmark artist */
+ PianoSong_t *song = req->data;
+
+ assert (song != NULL);
+
+ snprintf (xmlSendBuf, sizeof (xmlSendBuf), "<?xml version=\"1.0\"?>"
+ "<methodCall><methodName>station.createArtistBookmark</methodName>"
+ "<params><param><value><int>%lu</int></value></param>"
+ /* auth token */
+ "<param><value><string>%s</string></value></param>"
+ /* music id */
+ "<param><value><string>%s</string></value></param>"
+ "</params></methodCall>", (unsigned long) timestamp,
+ ph->user.authToken, song->artistMusicId);
+ snprintf (req->urlPath, sizeof (req->urlPath), PIANO_RPC_PATH
+ "rid=%s&lid=%s&method=createArtistBookmark&arg1=%s",
+ ph->routeId, ph->user.listenerId, song->artistMusicId);
+ break;
+ }
+
+ /* "high-level" wrapper */
+ case PIANO_REQUEST_RATE_SONG: {
+ /* love/ban song */
+ PianoRequestDataRateSong_t *reqData = req->data;
+ PianoReturn_t pRet;
+
+ assert (reqData != NULL);
+ assert (reqData->song != NULL);
+ assert (reqData->rating != PIANO_RATE_NONE);
+
+ PianoRequestDataAddFeedback_t transformedReqData;
+ transformedReqData.stationId = reqData->song->stationId;
+ transformedReqData.musicId = reqData->song->musicId;
+ transformedReqData.userSeed = reqData->song->userSeed;
+ transformedReqData.rating = reqData->rating;
+ transformedReqData.testStrategy = reqData->song->testStrategy;
+ transformedReqData.songType = reqData->song->songType;
+ req->data = &transformedReqData;
+
+ /* create request data (url, post data) */
+ pRet = PianoRequest (ph, req, PIANO_REQUEST_ADD_FEEDBACK);
+ /* and reset request type/data */
+ req->type = PIANO_REQUEST_RATE_SONG;
+ req->data = reqData;
+
+ return pRet;
+ break;
+ }
+
+ case PIANO_REQUEST_MOVE_SONG: {
+ /* move song to a different station, needs two requests */
+ PianoRequestDataMoveSong_t *reqData = req->data;
+ PianoRequestDataAddFeedback_t transformedReqData;
+ PianoReturn_t pRet;
+
+ assert (reqData != NULL);
+ assert (reqData->song != NULL);
+ assert (reqData->from != NULL);
+ assert (reqData->to != NULL);
+ assert (reqData->step < 2);
+
+ transformedReqData.musicId = reqData->song->musicId;
+ transformedReqData.userSeed = "";
+ transformedReqData.songType = reqData->song->songType;
+ transformedReqData.testStrategy = reqData->song->testStrategy;
+ req->data = &transformedReqData;
+
+ switch (reqData->step) {
+ case 0:
+ transformedReqData.stationId = reqData->from->id;
+ transformedReqData.rating = PIANO_RATE_BAN;
+ break;
+
+ case 1:
+ transformedReqData.stationId = reqData->to->id;
+ transformedReqData.rating = PIANO_RATE_LOVE;
+ break;
+ }
+
+ /* create request data (url, post data) */
+ pRet = PianoRequest (ph, req, PIANO_REQUEST_ADD_FEEDBACK);
+ /* and reset request type/data */
+ req->type = PIANO_REQUEST_MOVE_SONG;
+ req->data = reqData;
+
+ return pRet;
+ break;
+ }
+ }
+
+ if ((req->postData = PianoEncryptString (xmlSendBuf)) == NULL) {
+ return PIANO_RET_OUT_OF_MEMORY;
+ }
+
+ return PIANO_RET_OK;
+}
+
+/* parse xml response and update data structures/return new data structure
+ * @param piano handle
+ * @param initialized request (expects responseData to be a NUL-terminated
+ * string)
+ */
+PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) {
+ PianoReturn_t ret = PIANO_RET_ERR;
+
+ assert (ph != NULL);
+ assert (req != NULL);
+
+ switch (req->type) {
+ case PIANO_REQUEST_LOGIN: {
+ /* authenticate user */
+ PianoRequestDataLogin_t *reqData = req->data;
+
+ assert (req->responseData != NULL);
+ assert (reqData != NULL);
+
+ switch (reqData->step) {
+ case 0: {
+ char *cryptedTimestamp = NULL;
+
+ assert (req->responseData != NULL);
+
+ /* abusing parseNarrative; has same xml structure */
+ ret = PianoXmlParseNarrative (req->responseData, &cryptedTimestamp);
+ if (cryptedTimestamp != NULL) {
+ unsigned long timestamp = 0;
+ time_t realTimestamp = time (NULL);
+ char *decryptedTimestamp = NULL, *decryptedPos = NULL;
+ unsigned char i = 4;
+
+ if ((decryptedTimestamp = PianoDecryptString (cryptedTimestamp)) != NULL) {
+ decryptedPos = decryptedTimestamp;
+ /* skip four bytes garbage? at beginning */
+ while (i-- > 0 && *decryptedPos++ != '\0');
+ timestamp = strtoul (decryptedPos, NULL, 0);
+ ph->timeOffset = realTimestamp - timestamp;
+
+ free (decryptedTimestamp);
+ }
+ free (cryptedTimestamp);
+ }
+ ret = PIANO_RET_CONTINUE_REQUEST;
+ ++reqData->step;
+ break;
+ }
+
+ case 1:
+ /* information exists when reauthenticating, destroy to
+ * avoid memleak */
+ if (ph->user.listenerId != NULL) {
+ PianoDestroyUserInfo (&ph->user);
+ }
+ ret = PianoXmlParseUserinfo (ph, req->responseData);
+ break;
+ }
+ break;
+ }
+
+ case PIANO_REQUEST_GET_STATIONS:
+ /* get stations */
+ assert (req->responseData != NULL);
+
+ ret = PianoXmlParseStations (ph, req->responseData);
+ break;
+
+ case PIANO_REQUEST_GET_PLAYLIST: {
+ /* get playlist, usually four songs */
+ PianoRequestDataGetPlaylist_t *reqData = req->data;
+
+ assert (req->responseData != NULL);
+ assert (reqData != NULL);
+
+ reqData->retPlaylist = NULL;
+ ret = PianoXmlParsePlaylist (ph, req->responseData,
+ &reqData->retPlaylist);
+ break;
+ }
+
+ case PIANO_REQUEST_RATE_SONG:
+ /* love/ban song */
+ assert (req->responseData != NULL);
+
+ ret = PianoXmlParseSimple (req->responseData);
+ if (ret == PIANO_RET_OK) {
+ PianoRequestDataRateSong_t *reqData = req->data;
+ reqData->song->rating = reqData->rating;
+ }
+ break;
+
+ case PIANO_REQUEST_ADD_FEEDBACK:
+ /* never ever use this directly, low-level call */
+ assert (0);
+ break;
+
+ case PIANO_REQUEST_MOVE_SONG: {
+ /* move song to different station */
+ PianoRequestDataMoveSong_t *reqData = req->data;
+
+ assert (req->responseData != NULL);
+ assert (reqData != NULL);
+ assert (reqData->step < 2);
+
+ ret = PianoXmlParseSimple (req->responseData);
+ if (ret == PIANO_RET_OK && reqData->step == 0) {
+ ret = PIANO_RET_CONTINUE_REQUEST;
+ ++reqData->step;
+ }
+ break;
+ }
+
+ case PIANO_REQUEST_RENAME_STATION:
+ /* rename station and update PianoStation_t structure */
+ assert (req->responseData != NULL);
+
+ if ((ret = PianoXmlParseSimple (req->responseData)) == PIANO_RET_OK) {
+ PianoRequestDataRenameStation_t *reqData = req->data;
+
+ assert (reqData != NULL);
+ assert (reqData->station != NULL);
+ assert (reqData->newName != NULL);
+
+ free (reqData->station->name);
+ reqData->station->name = strdup (reqData->newName);
+ }
+ break;
+
+ case PIANO_REQUEST_DELETE_STATION:
+ /* delete station from server and station list */
+ assert (req->responseData != NULL);
+
+ if ((ret = PianoXmlParseSimple (req->responseData)) == PIANO_RET_OK) {
+ PianoStation_t *station = req->data;
+
+ assert (station != NULL);
+
+ /* delete station from local station list */
+ PianoStation_t *curStation = ph->stations, *lastStation = NULL;
+ while (curStation != NULL) {
+ if (curStation == station) {
+ if (lastStation != NULL) {
+ lastStation->next = curStation->next;
+ } else {
+ /* first station in list */
+ ph->stations = curStation->next;
+ }
+ PianoDestroyStation (curStation);
+ free (curStation);
+ break;
+ }
+ lastStation = curStation;
+ curStation = curStation->next;
+ }
+ }
+ break;
+
+ case PIANO_REQUEST_SEARCH: {
+ /* search artist/song */
+ PianoRequestDataSearch_t *reqData = req->data;
+
+ assert (req->responseData != NULL);
+ assert (reqData != NULL);
+
+ ret = PianoXmlParseSearch (req->responseData, &reqData->searchResult);
+ break;
+ }
+
+ case PIANO_REQUEST_CREATE_STATION: {
+ /* create station, insert new station into station list on success */
+ assert (req->responseData != NULL);
+
+ ret = PianoXmlParseCreateStation (ph, req->responseData);
+ break;
+ }
+
+ case PIANO_REQUEST_ADD_SEED: {
+ /* add seed to station, updates station structure */
+ PianoRequestDataAddSeed_t *reqData = req->data;
+
+ assert (req->responseData != NULL);
+ assert (reqData != NULL);
+ assert (reqData->station != NULL);
+
+ /* FIXME: update station data instead of replacing them */
+ ret = PianoXmlParseAddSeed (ph, req->responseData, reqData->station);
+ break;
+ }
+
+ case PIANO_REQUEST_ADD_TIRED_SONG:
+ case PIANO_REQUEST_SET_QUICKMIX:
+ case PIANO_REQUEST_BOOKMARK_SONG:
+ case PIANO_REQUEST_BOOKMARK_ARTIST:
+ assert (req->responseData != NULL);
+
+ ret = PianoXmlParseSimple (req->responseData);
+ break;
+
+ case PIANO_REQUEST_GET_GENRE_STATIONS:
+ /* get genre stations */
+ assert (req->responseData != NULL);
+
+ ret = PianoXmlParseGenreExplorer (ph, req->responseData);
+ break;
+
+ case PIANO_REQUEST_TRANSFORM_STATION: {
+ /* transform shared station into private and update isCreator flag */
+ PianoStation_t *station = req->data;
+
+ assert (req->responseData != NULL);
+ assert (station != NULL);
+
+ /* though this call returns a bunch of "new" data only this one is
+ * changed and important (at the moment) */
+ if ((ret = PianoXmlParseTranformStation (req->responseData)) ==
+ PIANO_RET_OK) {
+ station->isCreator = 1;
+ }
+ break;
+ }
+
+ case PIANO_REQUEST_EXPLAIN: {
+ /* explain why song was selected */
+ PianoRequestDataExplain_t *reqData = req->data;
+
+ assert (req->responseData != NULL);
+ assert (reqData != NULL);
+
+ ret = PianoXmlParseNarrative (req->responseData, &reqData->retExplain);
+ break;
+ }
+
+ case PIANO_REQUEST_GET_SEED_SUGGESTIONS: {
+ /* find similar artists */
+ PianoRequestDataGetSeedSuggestions_t *reqData = req->data;
+
+ assert (req->responseData != NULL);
+ assert (reqData != NULL);
+
+ ret = PianoXmlParseSeedSuggestions (req->responseData,
+ &reqData->searchResult);
+ break;
+ }
+ }
+
+ return ret;
+}
+
+/* get station from list by id
+ * @param search here
+ * @param search for this
+ * @return the first station structure matching the given id
+ */
+PianoStation_t *PianoFindStationById (PianoStation_t *stations,
+ const char *searchStation) {
+ while (stations != NULL) {
+ if (strcmp (stations->id, searchStation) == 0) {
+ return stations;
+ }
+ stations = stations->next;
+ }
+ return NULL;
+}
+
+/* convert return value to human-readable string
+ * @param enum
+ * @return error string
+ */
+const char *PianoErrorToStr (PianoReturn_t ret) {
+ switch (ret) {
+ case PIANO_RET_OK:
+ return "Everything is fine :)";
+ break;
+
+ case PIANO_RET_ERR:
+ return "Unknown.";
+ break;
+
+ case PIANO_RET_XML_INVALID:
+ return "Invalid XML.";
+ break;
+
+ case PIANO_RET_AUTH_TOKEN_INVALID:
+ return "Invalid auth token.";
+ break;
+
+ case PIANO_RET_AUTH_USER_PASSWORD_INVALID:
+ return "Username and/or password not correct.";
+ break;
+
+ case PIANO_RET_NOT_AUTHORIZED:
+ return "Not authorized.";
+ break;
+
+ case PIANO_RET_PROTOCOL_INCOMPATIBLE:
+ return "Protocol incompatible. Please upgrade " PACKAGE ".";
+ break;
+
+ case PIANO_RET_READONLY_MODE:
+ return "Request cannot be completed at this time, please try "
+ "again later.";
+ break;
+
+ case PIANO_RET_STATION_CODE_INVALID:
+ return "Station id is invalid.";
+ break;
+
+ case PIANO_RET_IP_REJECTED:
+ return "Your ip address was rejected. Please setup a control "
+ "proxy (see manpage).";
+ break;
+
+ case PIANO_RET_STATION_NONEXISTENT:
+ return "Station does not exist.";
+ break;
+
+ case PIANO_RET_OUT_OF_MEMORY:
+ return "Out of memory.";
+ break;
+
+ case PIANO_RET_OUT_OF_SYNC:
+ return "Out of sync. Please correct your system's time.";
+ break;
+
+ case PIANO_RET_PLAYLIST_END:
+ return "Playlist end.";
+ break;
+
+ case PIANO_RET_QUICKMIX_NOT_PLAYABLE:
+ return "Quickmix not playable.";
+ break;
+
+ default:
+ return "No error message available.";
+ break;
+ }
+}
+