diff --git a/Makefile b/Makefile index 8c647fb80..b070c596e 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,12 @@ LIBDIR:=${PREFIX}/lib INCDIR:=${PREFIX}/include MANDIR:=${PREFIX}/share/man DYNLINK:=0 -CFLAGS?=-O2 -DNDEBUG + +ifeq ($(MAKECMDGOALS),debug) + CFLAGS?=-O0 -g -DDEBUG +else + CFLAGS?=-O2 -DNDEBUG +endif ifeq (${CC},cc) OS := $(shell uname) @@ -41,13 +46,18 @@ LIBPIANO_SRC:=\ ${LIBPIANO_DIR}/piano.c \ ${LIBPIANO_DIR}/request.c \ ${LIBPIANO_DIR}/response.c \ - ${LIBPIANO_DIR}/list.c + ${LIBPIANO_DIR}/list.c \ + ${LIBPIANO_DIR}/debug_log.c LIBPIANO_OBJ:=${LIBPIANO_SRC:.c=.o} LIBPIANO_RELOBJ:=${LIBPIANO_SRC:.c=.lo} LIBPIANO_INCLUDE:=${LIBPIANO_DIR} -LIBAV_CFLAGS:=$(shell $(PKG_CONFIG) --cflags libavcodec libavformat libavutil libavfilter) -LIBAV_LDFLAGS:=$(shell $(PKG_CONFIG) --libs libavcodec libavformat libavutil libavfilter) +ifneq (${LIBAV},) +PKG_CONFIG_LIBDIR=PKG_CONFIG_LIBDIR=${LIBAV}/lib/pkgconfig +endif + +LIBAV_CFLAGS:=$(shell ${PKG_CONFIG_LIBDIR} $(PKG_CONFIG) --cflags libavcodec libavformat libavutil libavfilter) +LIBAV_LDFLAGS:=$(shell ${PKG_CONFIG_LIBDIR} $(PKG_CONFIG) --libs libavcodec libavformat libavutil libavfilter) LIBCURL_CFLAGS:=$(shell $(PKG_CONFIG) --cflags libcurl) LIBCURL_LDFLAGS:=$(shell $(PKG_CONFIG) --libs libcurl) @@ -122,6 +132,8 @@ clean: all: pianobar +debug: pianobar + ifeq (${DYNLINK},1) install: pianobar install-libpiano else @@ -150,4 +162,10 @@ uninstall: ${DESTDIR}/${LIBDIR}/libpiano.a \ ${DESTDIR}/${INCDIR}/piano.h -.PHONY: install install-libpiano uninstall test debug all +.PHONY: install install-libpiano uninstall test debug all make_debug + +make_debug: + @echo "LIBAV: '${LIBAV}'" + @echo "PKG_CONFIG_LIBDIR: '${PKG_CONFIG_LIBDIR}'" + @echo "LIBAV_CFLAGS: '${LIBAV_CFLAGS}'" + @echo "LIBAV_LDFLAGS: '${LIBAV_LDFLAGS}'" diff --git a/src/debug.h b/src/debug.h index e2e356b08..6000fab2d 100644 --- a/src/debug.h +++ b/src/debug.h @@ -25,6 +25,7 @@ THE SOFTWARE. #include "config.h" #include +#include "debug_log.h" #ifdef HAVE_DEBUGLOG #include diff --git a/src/libpiano/debug_log.c b/src/libpiano/debug_log.c new file mode 100755 index 000000000..445e56af2 --- /dev/null +++ b/src/libpiano/debug_log.c @@ -0,0 +1,30 @@ +#include +#include +#include + +#include "debug_log.h" + +static ErrMsgCallback_t gPianoPrintErrMsgCB; + +void PianoRegisterErrMsgCallback(ErrMsgCallback_t Arg) +{ + gPianoPrintErrMsgCB = Arg; +} + +void PianoPrintErrMsg(const char *format, ...) +{ + va_list fmtargs; + + assert (format != NULL); + va_start (fmtargs, format); + + if(gPianoPrintErrMsgCB != NULL) { + gPianoPrintErrMsgCB(format,fmtargs); + } + else { + va_start (fmtargs, format); + vprintf (format, fmtargs); + va_end (fmtargs); + } +} + diff --git a/src/libpiano/debug_log.h b/src/libpiano/debug_log.h new file mode 100755 index 000000000..c58a17218 --- /dev/null +++ b/src/libpiano/debug_log.h @@ -0,0 +1,21 @@ +#ifndef _DEBUG_LOG_H_ +#define _DEBUG_LOG_H_ +#include + +typedef void (*ErrMsgCallback_t) (const char *fmt,va_list fmtargs); +// typedef void (ErrMsgCallback_t) (const char *format); + +void PianoRegisterErrMsgCallback(ErrMsgCallback_t); +void PianoPrintErrMsg(const char *format,...) __attribute__((format(printf, 1, 2))); + +#define ELOG(format, ... ) PianoPrintErrMsg("%s#%d: " format, __FUNCTION__,__LINE__,## __VA_ARGS__) +#ifdef DEBUG + #define LOG(format, ... ) printf("%s: " format,__FUNCTION__,## __VA_ARGS__) + #define LOG_RAW(format, ... ) printf(format,## __VA_ARGS__) +#else + #define LOG(format, ... ) + #define LOG_RAW(format, ... ) +#endif + +#endif + diff --git a/src/libpiano/piano.h b/src/libpiano/piano.h index f7360aae1..b57298714 100644 --- a/src/libpiano/piano.h +++ b/src/libpiano/piano.h @@ -50,17 +50,32 @@ typedef struct PianoListHead { typedef struct PianoUserInfo { char *listenerId; char *authToken; + bool IsSubscriber; + bool IsPremiumUser; + int PlayListCount; + int StationCount; + int AlbumCount; + int TrackCount; + int PodcastCount; } PianoUserInfo_t; -typedef struct PianoStation { - PianoListHead_t head; - char isCreator; - char isQuickMix; - char useQuickMix; /* station will be included in quickmix */ - char *name; - char *id; - char *seedId; -} PianoStation_t; +typedef enum { + PIANO_TYPE_NONE = 0, + PIANO_TYPE_STATION = 1, + PIANO_TYPE_PODCAST = 2, + PIANO_TYPE_PLAYLIST = 3, + PIANO_TYPE_ALBUM = 4, + PIANO_TYPE_TRACK = 5, + PIANO_TYPE_LAST +} PianoStationType_t; + +typedef enum { + PIANO_MODE_STATION = 0, + PIANO_MODE_PODCAST = 1, + PIANO_MODE_PLAYLIST = 2, + PIANO_MODE_ALBUM = 3, + PIANO_MODE_ALL = 4 +} PianoMode_t; typedef enum { PIANO_RATE_NONE = 0, @@ -100,8 +115,27 @@ typedef struct PianoSong { unsigned int length; /* song length in seconds */ PianoSongRating_t rating; PianoAudioFormat_t audioFormat; + unsigned int releaseDate; } PianoSong_t; +typedef struct PianoStation { + PianoListHead_t head; + char isCreator; + char isQuickMix; + char useQuickMix; /* station will be included in quickmix */ + char *name; + char *id; + char *seedId; + PianoStationType_t stationType; + PianoSong_t *theSong; +} PianoStation_t; + +typedef struct { + PianoStation_t *station; + PianoSong_t *playList; + bool bGetAll; +} PianoRequestDataGetEpisodes_t; + /* currently only used for search results */ typedef struct PianoArtist { PianoListHead_t head; @@ -188,6 +222,14 @@ typedef enum { PIANO_REQUEST_CHANGE_SETTINGS = 24, PIANO_REQUEST_GET_STATION_MODES = 25, PIANO_REQUEST_SET_STATION_MODE = 26, + PIANO_REQUEST_GET_PLAYLISTS = 27, + PIANO_REQUEST_GET_TRACKS = 28, + PIANO_REQUEST_GET_PLAYBACK_INFO = 29, + PIANO_REQUEST_GET_ITEMS = 30, + PIANO_REQUEST_GET_USER_PROFILE = 31, + PIANO_REQUEST_ANNOTATE_OBJECTS = 32, + PIANO_REQUEST_REMOVE_ITEM = 33, + PIANO_REQUEST_GET_EPISODES = 34, } PianoRequestType_t; typedef struct PianoRequest { diff --git a/src/libpiano/piano_private.h b/src/libpiano/piano_private.h index ffc14c820..a5d954acc 100644 --- a/src/libpiano/piano_private.h +++ b/src/libpiano/piano_private.h @@ -25,6 +25,14 @@ THE SOFTWARE. #include "piano.h" +#ifdef DEBUG + #define LOG(format, ... ) printf("%s: " format,__FUNCTION__,## __VA_ARGS__) + #define LOG_RAW(format, ... ) printf(format,## __VA_ARGS__) +#else + #define LOG(format, ... ) + #define LOG_RAW(format, ... ) +#endif + void PianoDestroyStation (PianoStation_t *station); void PianoDestroyUserInfo (PianoUserInfo_t *user); diff --git a/src/libpiano/request.c b/src/libpiano/request.c index 69e49a145..cd850a197 100644 --- a/src/libpiano/request.c +++ b/src/libpiano/request.c @@ -30,7 +30,9 @@ THE SOFTWARE. #include #include "piano.h" +#include "piano_private.h" #include "crypt.h" +#include "debug.h" /* prepare piano request (initializes request type, urlpath and postData) * @param piano handle @@ -95,6 +97,8 @@ PianoReturn_t PianoRequest (PianoHandle_t *ph, PianoRequest_t *req, json_object_new_string (ph->partner.authToken)); json_object_object_add (j, "syncTime", json_object_new_int (timestamp)); + json_object_object_add (j, "returnIsSubscriber", + json_object_new_boolean (true)); CURL * const curl = curl_easy_init (); urlencAuthToken = curl_easy_escape (curl, @@ -501,8 +505,155 @@ PianoReturn_t PianoRequest (PianoHandle_t *ph, PianoRequest_t *req, goto cleanup; break; } - } + case PIANO_REQUEST_GET_PLAYLISTS: { + /* get stations, user must be authenticated */ + assert (ph->user.listenerId != NULL); + req->secure = true; + json_object *a = json_object_new_object (); + json_object_object_add(a,"listenerId",json_object_new_string(ph->user.listenerId)); + json_object_object_add(a,"offset",json_object_new_int(0)); + json_object_object_add(a,"limit",json_object_new_int(100)); + json_object_object_add(a,"annotationLimit",json_object_new_int(100)); + json_object_object_add(j,"request",a); + method = "collections.v7.getSortedPlaylists"; + break; + } + + case PIANO_REQUEST_GET_TRACKS: { + assert (ph->user.listenerId != NULL); + PianoRequestDataGetPlaylist_t *reqData = req->data; + + assert (reqData != NULL); + assert (reqData->station != NULL); + assert (reqData->station->id != NULL); + + req->secure = true; + switch(reqData->station->stationType) { + case PIANO_TYPE_PLAYLIST: { + json_object *a = json_object_new_object (); + json_object_object_add(a,"pandoraId",json_object_new_string(reqData->station->id)); + json_object_object_add(a,"limit",json_object_new_int(100)); + json_object_object_add(a,"annotationLimit",json_object_new_int(100)); + json_object_object_add(a,"bypassPrivacyRu1les",json_object_new_boolean(true)); + json_object_object_add(j,"request",a); + method = "playlists.v7.getTracks"; + break; + } + + case PIANO_TYPE_ALBUM: { + json_object *a = json_object_new_array(); + json_object_array_add(a,json_object_new_string(reqData->station->id)); + json_object_object_add(j,"pandoraIds",a); + json_object_object_add(j,"annotateAlbumTracks",json_object_new_boolean(true)); + method = "catalog.v4.annotateObjects"; + break; + } + + default: + LOG("Invalid stationType 0x%x\n",ph->stations->stationType); + break; + } + break; + } + + case PIANO_REQUEST_GET_PLAYBACK_INFO: { + PianoRequestDataGetPlaylist_t *reqData = req->data; + + assert (reqData != NULL); + assert (reqData->station != NULL); + assert (reqData->station->id != NULL); + assert (reqData->station->stationType != PIANO_TYPE_STATION); + + req->secure = true; + json_object_object_add (j, "pandoraId", + json_object_new_string (reqData->retPlaylist->trackToken)); + json_object_object_add (j, "sourcePandoraId", + json_object_new_string (reqData->retPlaylist->seedId)); + json_object_object_add (j, "includeAudioToken", + json_object_new_boolean (true)); + json_object_object_add (j, "deviceCode",json_object_new_string ("")); + method = "onDemand.getAudioPlaybackInfo"; + break; + } + case PIANO_REQUEST_GET_USER_PROFILE: { + req->secure = true; + json_object *a = json_object_new_object (); + json_object_object_add(a,"limit",json_object_new_int(10)); + json_object_object_add(a,"annotationLimit",json_object_new_int(10)); + json_object_object_add(a,"profileOwner", + json_object_new_string(ph->user.listenerId)); + json_object_object_add(j,"request",a); + method = "profile.v1.getFullProfile"; + break; + } + + case PIANO_REQUEST_GET_ITEMS: { + req->secure = true; + + json_object *a = json_object_new_object (); + json_object_object_add(j,"request",a); + #if 0 + // test ability to continue after hitting limit + json_object_object_add(a,"limit",json_object_new_int(4)); + json_object_object_add(a,"cursor",json_object_new_string("g6FjzwAFdTr1OqMYoXbAoXSWokFSolRSolBMolBDokFMolBF")); + #endif + method = "collections.v7.getItems"; + break; + } + + case PIANO_REQUEST_ANNOTATE_OBJECTS: { + json_object *a = json_object_new_array(); + PianoStation_t *station = ph->stations; + req->secure = true; + + while(station != NULL) { + assert(station->id != NULL); + if(station->name == NULL) { + switch(station->stationType) { + case PIANO_TYPE_PODCAST: + case PIANO_TYPE_ALBUM: + case PIANO_TYPE_TRACK: + json_object_array_add(a,json_object_new_string(station->id)); + break; + } + } + station = (PianoStation_t *) station->head.next; + } + json_object_object_add(j,"pandoraIds",a); + json_object_object_add(j,"annotateAlbumTracks", + json_object_new_boolean(false)); + method = "catalog.v4.annotateObjects"; + break; + } + + case PIANO_REQUEST_REMOVE_ITEM: { + /* delete item */ + PianoStation_t *station = req->data; + assert (station != NULL); + + req->secure = true; + json_object *a = json_object_new_object (); + json_object_object_add(a,"pandoraId",json_object_new_string(station->id)); + json_object_object_add(j,"request",a); + method = "collections.v7.removeItem"; + break; + } + + case PIANO_REQUEST_GET_EPISODES: { + PianoRequestDataGetEpisodes_t *reqData = req->data; + PianoStation_t *station = reqData->station; + assert (station != NULL); + + req->secure = true; + json_object_object_add(j,"annotationLimit",json_object_new_int(20)); + json_object_object_add(j,"catalogVersion",json_object_new_int(4)); + json_object_object_add(j,"pandoraId",json_object_new_string(station->id)); + json_object_object_add(j,"sortingOrder",json_object_new_string("")); + method = "aesop.v1.getDetails"; + break; + } + } /* standard parameter */ if (method != NULL) { char *urlencAuthToken; diff --git a/src/libpiano/response.c b/src/libpiano/response.c index 0be887281..bc1921449 100644 --- a/src/libpiano/response.c +++ b/src/libpiano/response.c @@ -33,6 +33,15 @@ THE SOFTWARE. #include "piano_private.h" #include "crypt.h" +static const char *qualityMap[] = { + "", "lowQuality", "mediumQuality","highQuality" +}; + +static const char *formatMap[] = { + "", "aacplus", "mp3" +}; + +static const char *imageHost = "https://content-images.p-cdn.com/"; static char *PianoJsonStrdup (json_object *j, const char *key) { assert (j != NULL); assert (key != NULL); @@ -57,6 +66,69 @@ static bool getBoolDefault (json_object * const j, const char * const key, const } } +static const char *PianoJsonGetStr(json_object *j, const char *key) { + assert (j != NULL); + assert (key != NULL); + + json_object *v; + if (json_object_object_get_ex (j, key, &v)) { + return json_object_get_string (v); + } else { + return NULL; + } +} + +static int getInt(json_object * const j, const char * const key) { + assert (j != NULL); + assert (key != NULL); + + json_object *v; + if (json_object_object_get_ex (j, key, &v)) { + return json_object_get_int(v); + } else { + return 0; + } +} + +static char *getCoverArt(struct json_object *Val) +{ + char artUrl[120]; + json_object *v = NULL; + char *Ret = NULL; + + if (json_pointer_get(Val, "/icon/artUrl", &v)) { + LOG("Couldn't get artUrl\n"); + } + else { + assert (v != NULL); + snprintf(artUrl,sizeof(artUrl),"%s%s", + imageHost,json_object_get_string(v)); + Ret = strdup(artUrl); + } + + return Ret; +} + +static int getBool(json_object * const j, const char * const key) { + assert (j != NULL); + assert (key != NULL); + + json_object *v; + if (json_pointer_get(j, key, &v)) { + return -1; + } else { + return json_object_get_boolean (v) ? 1 : 0; + } +} + +static void PianoJsonParsePlaylist(json_object *j, PianoStation_t *s) +{ + s->name = PianoJsonStrdup (j, "name"); + s->id = PianoJsonStrdup (j, "pandoraId"); + s->isCreator = false; + s->isQuickMix = false; +} + static void PianoJsonParseStation (json_object *j, PianoStation_t *s) { s->name = PianoJsonStrdup (j, "stationName"); s->id = PianoJsonStrdup (j, "stationToken"); @@ -191,6 +263,8 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { ph->user.listenerId = PianoJsonStrdup (result, "userId"); ph->user.authToken = PianoJsonStrdup (result, "userAuthToken"); + ph->user.IsSubscriber = getBoolDefault (result, + "isSubscriber", false); break; } break; @@ -213,6 +287,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { if ((tmpStation = calloc (1, sizeof (*tmpStation))) == NULL) { return PIANO_RET_OUT_OF_MEMORY; } + tmpStation->stationType = PIANO_TYPE_STATION; PianoJsonParseStation (s, tmpStation); @@ -241,6 +316,173 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { break; } + case PIANO_REQUEST_GET_PLAYLISTS: { + /* get playlists */ + assert (req->responseData != NULL); + + json_object *playlists; + + if (!json_object_object_get_ex (result, "items", &playlists)) { + break; + } + + for (int i = 0; i < json_object_array_length (playlists); i++) { + PianoStation_t *tmpStation; + json_object *s = json_object_array_get_idx (playlists, i); + + if ((tmpStation = calloc (1, sizeof (*tmpStation))) == NULL) { + return PIANO_RET_OUT_OF_MEMORY; + } + tmpStation->stationType = PIANO_TYPE_PLAYLIST; + + PianoJsonParsePlaylist(s, tmpStation); + + /* start new linked list or append */ + ph->stations = PianoListAppendP (ph->stations, tmpStation); + } + break; + } + + // Get album or playlist tracks + case PIANO_REQUEST_GET_TRACKS: { + assert (req->responseData != NULL); + + PianoRequestDataGetPlaylist_t *reqData = req->data; + PianoSong_t *playlist = NULL; + PianoSong_t *FullPlaylist = NULL; + + assert (result != NULL); + assert (req->responseData != NULL); + assert (reqData != NULL); + + switch(reqData->station->stationType) { + case PIANO_TYPE_PLAYLIST: { + json_object *tracks = NULL; + json_object *annotations = NULL; + if (!json_object_object_get_ex (result, "tracks", &tracks)) { + break; + } + assert (tracks!= NULL); + LOG("got tracks\n"); + if (!json_object_object_get_ex (result, "annotations", &annotations)) { + break; + } + assert (annotations != NULL); + LOG("got annotations\n"); + + for (int i = 0; i < json_object_array_length (tracks); i++) { + json_object *s = json_object_array_get_idx (tracks, i); + json_object *trackInfo = NULL; + PianoSong_t *song; + + if ((song = calloc (1, sizeof (*song))) == NULL) { + return PIANO_RET_OUT_OF_MEMORY; + } + + song->seedId = PianoJsonStrdup(result, "pandoraId"); + song->trackToken = PianoJsonStrdup (s, "trackPandoraId"); + assert (song->trackToken != NULL); + + LOG("track %d: %s\n",i + 1,song->trackToken); + + if (!json_object_object_get_ex (annotations, song->trackToken, &trackInfo)) { + break; + } + assert (trackInfo!= NULL); + song->artist = PianoJsonStrdup(trackInfo, "artistName"); + song->album = PianoJsonStrdup(trackInfo, "albumName"); + song->title = PianoJsonStrdup(trackInfo, "name"); + song->fileGain = 0.0; + song->length = getInt(trackInfo, "duration"); + song->coverArt = getCoverArt(trackInfo); + playlist = PianoListAppendP (playlist, song); + } + break; + } + + case PIANO_TYPE_ALBUM: { + char trackTitle[120]; + int totalTracks = 0; + int trackNumber; + + // Count tracks + json_object_object_foreach(result,Key1,Val1) { + if(Key1[0] != 'T' || Key1[1] != 'R') { + continue; + } + totalTracks++; + } + + json_object_object_foreach(result,Key,Val) { + if(Key[0] != 'T' || Key[1] != 'R') { + continue; + } + LOG("got track %s: ",Key); + trackNumber = getInt(Val,"trackNumber"); + snprintf(trackTitle,sizeof(trackTitle), + totalTracks > 9 ? "%02d %s" : "%d %s", + trackNumber,PianoJsonGetStr(Val,"name")); + LOG_RAW("%s\n",trackTitle); + if(getBool(Val,"/rightsInfo/hasInteractive") != 1) { + LOG(" ignored\n"); + LOG(" hasInteractive: %d\n",getBool(Val,"/rightsInfo/hasInteractive")); + LOG(" hasOffline: %d\n",getBool(Val,"/rightsInfo/hasOffline")); + LOG(" hasNonInteractive: %d\n",getBool(Val,"/rightsInfo/hasNonInteractive")); + LOG(" hasStatutory: %d\n",getBool(Val,"/rightsInfo/hasStatutory")); + LOG(" hasRadioRights: %d\n",getBool(Val,"/rightsInfo/hasRadioRights")); + continue; + } + PianoSong_t *song; + + if ((song = calloc (1, sizeof (*song))) == NULL) { + return PIANO_RET_OUT_OF_MEMORY; + } + + song->stationId = strdup(reqData->station->id); + song->title = strdup(trackTitle); + song->trackToken = strdup(Key); + song->seedId = PianoJsonStrdup(Val,"albumId"); + song->artist = PianoJsonStrdup(Val,"artistName"); + song->album = PianoJsonStrdup(Val,"albumName"); + song->fileGain = 0.0; + song->length = getInt(Val, "duration"); + song->coverArt = getCoverArt(Val); + // Add to playlist in track order + + if(playlist == NULL) { + playlist = song; + } + else { + PianoSong_t *lastSong = (PianoSong_t *) &playlist; + PianoSong_t *nextSong = playlist; + do { + int nextSongTrackNum; + if(nextSong == NULL) { + lastSong->head.next = (struct PianoListHead *) song; + break; + } + sscanf(nextSong->title,"%d",&nextSongTrackNum); + if(nextSongTrackNum > trackNumber) { + lastSong->head.next = (struct PianoListHead *) song; + song->head.next = (struct PianoListHead *) nextSong; + break; + } + lastSong = nextSong; + nextSong = (PianoSong_t *) nextSong->head.next; + } while(true); + } + } + break; + } + + default: + LOG("Invalid stationType 0x%x\n",ph->stations->stationType); + break; + } + reqData->retPlaylist = playlist; + break; + } + case PIANO_REQUEST_GET_PLAYLIST: { /* get playlist, usually four songs */ PianoRequestDataGetPlaylist_t *reqData = req->data; @@ -351,6 +593,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { break; } + case PIANO_REQUEST_REMOVE_ITEM: case PIANO_REQUEST_DELETE_STATION: { /* delete station from server and station list */ PianoStation_t *station = req->data; @@ -712,8 +955,371 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { } break; } - } + case PIANO_REQUEST_GET_PLAYBACK_INFO: { + PianoRequestDataGetPlaylist_t *reqData = req->data; + PianoSong_t *song = reqData->retPlaylist; + + assert (req->responseData != NULL); + assert (reqData != NULL); + assert (reqData->quality < sizeof (qualityMap)/sizeof (*qualityMap)); + assert (song != NULL); + + json_object *audioUrlMap = NULL; + if (!json_object_object_get_ex (result, "audioUrlMap", &audioUrlMap)) { + break; + } + assert (audioUrlMap != NULL); + + const char *quality = qualityMap[reqData->quality]; + json_object *umap; + + json_object *jsonEncoding = NULL; + if (json_object_object_get_ex (audioUrlMap, quality, &umap)) { + assert (umap != NULL); + if (json_object_object_get_ex (umap, "encoding", &jsonEncoding)) { + assert (jsonEncoding != NULL); + const char *encoding = json_object_get_string (jsonEncoding); + assert (encoding != NULL); + for (size_t k = 0; k < sizeof (formatMap)/sizeof (*formatMap); k++) { + if (strcmp (formatMap[k], encoding) == 0) { + song->audioFormat = k; + break; + } + } + song->audioUrl = PianoJsonStrdup (umap, "audioUrl"); + } + } + + if(song->audioUrl == NULL) { + /* requested quality is not available */ + LOG("quality %s not found in audioUrlMap\n",quality); + ret = PIANO_RET_QUALITY_UNAVAILABLE; + PianoDestroyPlaylist (reqData->retPlaylist); + goto cleanup; + } + break; + } + + case PIANO_REQUEST_GET_USER_PROFILE: { + assert (req->responseData != NULL); + + json_object *stations; + json_object *annotations = NULL; + ph->user.IsPremiumUser = getBoolDefault (result,"isPremiumUser", false); + LOG("IsPremiumUser: %d\n",ph->user.IsPremiumUser); + + if (!json_object_object_get_ex (result, "annotations", &annotations)) { + break; + } + assert (annotations != NULL); + int Annotations = 0; + json_object_object_foreach(annotations,Key,Val) { + const char *type = PianoJsonGetStr(Val,"type"); + + Annotations++; + if(strcmp(type,"PL") == 0) { + ph->user.PlayListCount++; + } + else if(strcmp(type,"ST") == 0) { + ph->user.StationCount++; + } + else if(strcmp(type,"AL") == 0) { + ph->user.AlbumCount++; + } + else if(strcmp(type,"TR") == 0) { + ph->user.TrackCount++; + } + else if(strcmp(type,"LI") == 0) { + } + else if(strcmp(type,"AR") == 0) { + } + else { + LOG("type %s ignored\n",type); + } + } + LOG("Found: %d annotations:\n",Annotations); + LOG(" PlayLists: %d:\n",ph->user.PlayListCount); + LOG(" Stations: %d:\n",ph->user.StationCount); + LOG(" Albums: %d:\n",ph->user.AlbumCount); + LOG(" Tracks: %d:\n",ph->user.TrackCount); + break; + } + + case PIANO_REQUEST_GET_ITEMS: { + assert (req->responseData != NULL); + json_object *items = NULL; + if (!json_object_object_get_ex (result, "items", &items)) { + break; + } + assert (items != NULL); + for (int i = 0; i < json_object_array_length (items); i++) { + json_object *s = json_object_array_get_idx (items, i); + const char *type = PianoJsonGetStr(s,"pandoraType"); + PianoSong_t *song; + PianoStationType_t stationType = PIANO_TYPE_NONE; + + assert(type != NULL); + if(strcmp(type,"PL") == 0 || strcmp(type,"ST") == 0) { + // Playlists and stations handled elsewhere + } + else if(strcmp(type,"AL") == 0) { + stationType = PIANO_TYPE_ALBUM; + } + else if(strcmp(type,"TR") == 0) { + stationType = PIANO_TYPE_TRACK; + } + else if(strcmp(type,"PC") == 0) { + stationType = PIANO_TYPE_PODCAST; + ph->user.PodcastCount++; + } + else { + LOG("type %s ignored\n",type); + } + + if(stationType != PIANO_TYPE_NONE) { + PianoStation_t *tmpStation; + if ((tmpStation = calloc (1, sizeof (*tmpStation))) == NULL) { + return PIANO_RET_OUT_OF_MEMORY; + } + tmpStation->stationType = stationType; + tmpStation->id = PianoJsonStrdup (s, "pandoraId"); + /* start new linked list or append */ + ph->stations = PianoListAppendP (ph->stations, tmpStation); + } + else { + LOG("type %s ignored\n",type); + } + } + + if(ph->user.PodcastCount) { + LOG("Found %d podcast stations\n",ph->user.PodcastCount); + } + break; + } + + case PIANO_REQUEST_ANNOTATE_OBJECTS: { + assert (req->responseData != NULL); + assert (req->data != NULL); + PianoStation_t *station = ph->stations; + PianoRequestDataGetPlaylist_t *reqData = req->data; + + while(station != NULL) { + assert(station->id != NULL); + switch(station->stationType) { + case PIANO_TYPE_PODCAST: { + json_object_object_foreach(result,Key,Val) { + if(strcmp(Key,station->id) == 0) { + PianoSong_t *song = station->theSong; + assert (song == NULL); + if ((song = calloc (1, sizeof (*song))) == NULL) { + return PIANO_RET_OUT_OF_MEMORY; + } + station->name = PianoJsonStrdup(Val,"name"); + station->seedId = PianoJsonStrdup(Val,"latestEpisodeId"); + station->theSong = song; + song->album = strdup(station->name); + song->coverArt = getCoverArt(Val); // podcast coverArt + LOG("podcast coverart %s\n",song->coverArt); + break; + } + } + if(station->name == NULL) { + LOG("Couldn't find station %s\n",station->id); + } + break; + } + + case PIANO_TYPE_ALBUM: { + json_object_object_foreach(result,Key,Val) { + if(strcmp(Key,station->id) == 0) { + char Temp[120]; + snprintf(Temp,sizeof(Temp),"%s - %s", + PianoJsonGetStr(Val,"artistName"), + PianoJsonGetStr(Val,"name")); + station->name = strdup(Temp); + station->seedId = PianoJsonStrdup(Val,"pandoraId"); + break; + } + } + if(station->name == NULL) { + LOG("Couldn't find station %s\n",station->id); + } + break; + } + + case PIANO_TYPE_TRACK: { + json_object_object_foreach(result,Key,Val) { + if(strcmp(Key,station->id) == 0) { + station->name = PianoJsonStrdup(Val,"name"); + station->seedId = PianoJsonStrdup(Val,"albumId"); + + PianoSong_t *song = station->theSong; + assert (song == NULL); + if ((song = calloc (1, sizeof (*song))) == NULL) { + return PIANO_RET_OUT_OF_MEMORY; + } + station->theSong = song; + song->artist = PianoJsonStrdup(Val, "artistName"); + song->album = PianoJsonStrdup(Val, "albumName"); + song->title = PianoJsonStrdup(Val, "name"); + song->length = getInt(Val, "duration"); + song->coverArt = getCoverArt(Val); + song->fileGain = 0.0; + break; + } + } + if(station->name == NULL) { + LOG("Couldn't find station %s\n",station->id); + } + break; + } + } + station = (PianoStation_t *) station->head.next; + } + break; + } + + case PIANO_REQUEST_GET_EPISODES: { + assert (req->responseData != NULL); + assert (req->data != NULL); + PianoRequestDataGetEpisodes_t *reqData = req->data; + PianoStation_t *station = reqData->station; + PianoSong_t *playlist = NULL; + int Added = 0; + PianoSong_t *song; + + json_object *annotations = NULL; + if (json_pointer_get(result, "/details/annotations", &annotations)) { + break; + } + json_object_object_foreach(annotations,Key,Val) { + if(Key[0] != 'P' || Key[1] != 'E') { + // not episode, ignore it + continue; + } + const char *EpisodeTitle = PianoJsonGetStr(Val,"name"); + + if(EpisodeTitle == NULL) { + LOG("Couldn't get title of episode\n"); + continue; + } + const char *Id = PianoJsonGetStr(Val,"podcastId"); + if(Id == NULL) { + LOG("Couldn't get podcastId\n"); + continue; + } + + if(strcmp(station->id,Id) != 0) { + LOG("Episode not for selected podcast (%s != %s)\n", + station->id,Id); + continue; + } + + const char *State = PianoJsonGetStr(Val,"contentState"); + if(State == NULL) { + LOG("Couldn't get contentState\n"); + continue; + } + + if(strcmp(State,"AVAILABLE") != 0) { + if(strlen(EpisodeTitle) > 0) { + LOG(" ignored %s\n",EpisodeTitle); + LOG(" contentState: %s\n",State); + } + continue; + } + if(getBool(Val,"/rightsInfo/hasInteractive") != 1) { + LOG(" ignored %s\n",EpisodeTitle); + LOG(" hasInteractive: %d\n",getBool(Val,"/rightsInfo/hasInteractive")); + LOG(" hasOffline: %d\n",getBool(Val,"/rightsInfo/hasOffline")); + LOG(" hasNonInteractive: %d\n",getBool(Val,"/rightsInfo/hasNonInteractive")); + LOG(" hasStatutory: %d\n",getBool(Val,"/rightsInfo/hasStatutory")); + LOG(" hasRadioRights: %d\n",getBool(Val,"/rightsInfo/hasRadioRights")); + continue; + } + + int Month; + int Day; + int Year; + char trackTitle[120]; + const char *Released = PianoJsonGetStr(Val,"releaseDate"); + if(Released == NULL) { + LOG("Couldn't get releaseDate\n"); + continue; + } + if(sscanf(Released,"%d-%d-%d",&Year,&Month,&Day) != 3) { + LOG("Couldn't convert releaseDate %s\n",Released); + continue; + } + const char *Title = PianoJsonGetStr(Val,"name"); + if(Title == NULL) { + LOG("Couldn't get episode title\n"); + continue; + } + const char *trackToken = PianoJsonGetStr(Val, "pandoraId"); + if(trackToken == NULL) { + LOG("Couldn't get trackToken\n"); + continue; + } + + snprintf(trackTitle,sizeof(trackTitle),"%02d/%02d: %s", + Month,Day,Title); + LOG("Got %s\n",trackTitle); + + if(!reqData->bGetAll) { + // just getting name of the current episode + song = reqData->playList; + if(strcmp(trackToken,song->trackToken) != 0) { + LOG("Ignoring %s, not current episode\n",trackTitle); + continue; + } + song->title = strdup(trackTitle); + LOG("Added name of current episode\n"); + break; + } + if ((song = calloc (1, sizeof (*song))) == NULL) { + return PIANO_RET_OUT_OF_MEMORY; + } + + song->title = strdup(trackTitle); + song->trackToken = strdup(trackToken); + song->length = getInt(Val, "duration"); + // Save release date for sorting + song->fileGain = ((Year - 1900) * 10000) + (Month * 100) + Day; + // Add to playlist in release data order + PianoSong_t *thisSong = playlist; + PianoSong_t *lastSong = (PianoSong_t *) &playlist; + do { + if(thisSong == NULL) { + lastSong->head.next = (struct PianoListHead *) song; + // LOG("Added to end of list\n"); + break; + } + // LOG("Comparing to %s\n",thisSong->title); + if(thisSong->fileGain > song->fileGain) { + lastSong->head.next = (struct PianoListHead *) song; + song->head.next = (struct PianoListHead *) thisSong; + // LOG("Added before\n"); + break; + } + lastSong = thisSong; + thisSong = (PianoSong_t *) thisSong->head.next; + } while(true); + Added++; + } + reqData->playList = playlist; + LOG("Added %d episodes:\n",Added); +#if 0 + song = playlist; + while(song != NULL) { + LOG(" %s\n",song->title); + song = (PianoSong_t *) song->head.next; + } +#endif + break; + } + } cleanup: json_object_put (j); diff --git a/src/main.c b/src/main.c index c726b28bf..a3eab1778 100644 --- a/src/main.c +++ b/src/main.c @@ -56,6 +56,7 @@ THE SOFTWARE. #include "ui.h" #include "ui_dispatch.h" #include "ui_readline.h" +#include "debug_log.h" /* authenticate user */ @@ -178,6 +179,53 @@ static bool BarMainGetStations (BarApp_t *app) { return ret; } +static bool BarMainGetUserProfile(BarApp_t *app) { + PianoReturn_t pRet; + CURLcode wRet; + bool ret; + + BarUiMsg (&app->settings, MSG_INFO, "Get user profile ... "); + ret = BarUiPianoCall (app, PIANO_REQUEST_GET_USER_PROFILE, NULL, &pRet, &wRet); + return ret; +} + +static bool BarMainGetAllStations (BarApp_t *app) { + PianoReturn_t pRet; + CURLcode wRet; + bool ret; + PianoRequestDataGetPlaylist_t reqData; + reqData.station = app->nextStation; + reqData.quality = app->settings.audioQuality; + reqData.retPlaylist = NULL; + + do { + ret = BarMainGetStations (app); + if(!ret) { + break; + } + BarUiMsg (&app->settings, MSG_INFO, "Get items ... "); + ret = BarUiPianoCall (app, PIANO_REQUEST_GET_ITEMS, &reqData, &pRet, &wRet); + if(!ret) { + break; + } + BarUiMsg (&app->settings, MSG_INFO, "Annotate Objects ... "); + ret = BarUiPianoCall (app, PIANO_REQUEST_ANNOTATE_OBJECTS, &reqData, &pRet, &wRet); + if(!ret) { + break; + } + if(app->ph.user.IsPremiumUser) { + BarUiMsg (&app->settings, MSG_INFO, "Get Playlists ... "); + ret = BarUiPianoCall (app, PIANO_REQUEST_GET_PLAYLISTS,NULL, &pRet, &wRet); + if(!ret) { + break; + } + } + BarUiStartEventCmd (&app->settings, "usergetstations", NULL, NULL, &app->player, + app->ph.stations, pRet, wRet); + + } while (false); + return ret; +} /* get initial station from autostart setting or user input */ static void BarMainGetInitialStation (BarApp_t *app) { @@ -214,21 +262,108 @@ static void BarMainGetPlaylist (BarApp_t *app) { PianoReturn_t pRet; CURLcode wRet; PianoRequestDataGetPlaylist_t reqData; - reqData.station = app->nextStation; + PianoStation_t *station = app->nextStation; + assert(station != NULL); + + memset(&reqData,0,sizeof(reqData)); + reqData.station = station; reqData.quality = app->settings.audioQuality; + app->stationStarted = true; - BarUiMsg (&app->settings, MSG_INFO, "Receiving new playlist... "); - if (!BarUiPianoCall (app, PIANO_REQUEST_GET_PLAYLIST, - &reqData, &pRet, &wRet)) { - app->nextStation = NULL; - } else { - app->playlist = reqData.retPlaylist; - if (app->playlist == NULL) { - BarUiMsg (&app->settings, MSG_INFO, "No tracks left.\n"); + LOG("stationType %s\n",StationType2Str(station->stationType)); + + switch(station->stationType) { + case PIANO_TYPE_STATION: + BarUiMsg (&app->settings, MSG_INFO, "Receiving new playlist... "); + if (!BarUiPianoCall (app, PIANO_REQUEST_GET_PLAYLIST, + &reqData, &pRet, &wRet)) { + app->nextStation = NULL; + } else { + app->playlist = reqData.retPlaylist; + if (app->playlist == NULL) { + BarUiMsg (&app->settings, MSG_INFO, "No tracks left.\n"); + app->nextStation = NULL; + } + } + app->curStation = app->nextStation; + break; + + case PIANO_TYPE_PLAYLIST: + BarUiMsg (&app->settings, MSG_INFO, "Get tracks ... "); + if (!BarUiPianoCall (app, PIANO_REQUEST_GET_TRACKS, + &reqData, &pRet, &wRet)) { + app->nextStation = NULL; + } else { + app->playlist = reqData.retPlaylist; + app->FullPlaylist = CopyPlaylist(app->playlist); + + if (app->playlist == NULL) { + BarUiMsg (&app->settings, MSG_INFO, "No tracks left.\n"); + app->nextStation = NULL; + } + } + app->curStation = app->nextStation; + break; + + case PIANO_TYPE_TRACK: + assert(station->theSong != NULL); + reqData.retPlaylist = CopySong(station->theSong); + assert(reqData.retPlaylist != NULL); + reqData.retPlaylist->trackToken = strdup(station->id); + reqData.retPlaylist->seedId = strdup(station->seedId); + + BarUiMsg (&app->settings, MSG_INFO, "Get playback info ... "); + if (BarUiPianoCall (app, PIANO_REQUEST_GET_PLAYBACK_INFO, + &reqData, &pRet, &wRet)) { + app->playlist = reqData.retPlaylist; + } + else { + ELOG("REQUEST_GET_PLAYBACK_INFO failed\n"); + } + app->curStation = app->nextStation; + break; + + case PIANO_TYPE_PODCAST: + PianoSong_t *song = station->theSong; + station->theSong = NULL; + assert (song != NULL); + song->trackToken = strdup(station->seedId); + song->seedId = strdup(station->id); + app->playlist = song; + app->curStation = app->nextStation; app->nextStation = NULL; - } + if(song->title == NULL) { + // Get name of episode + PianoRequestDataGetEpisodes_t reqData1; + reqData1.station = app->curStation; + reqData1.playList = song; + reqData1.bGetAll = false; + BarUiMsg (&app->settings, MSG_INFO, "Get episodes ... "); + if (!BarUiPianoCall (app, PIANO_REQUEST_GET_EPISODES, + &reqData1, &pRet, &wRet)) { + app->curStation = NULL; + ELOG("Internal error\n"); + break; + } + } + break; + + case PIANO_TYPE_ALBUM: + BarUiMsg (&app->settings, MSG_INFO, "Get tracks ... "); + if (!BarUiPianoCall (app, PIANO_REQUEST_GET_TRACKS, + &reqData, &pRet, &wRet)) { + app->nextStation = NULL; + } else { + app->playlist = reqData.retPlaylist; + app->FullPlaylist = CopyPlaylist(app->playlist); + if (app->playlist == NULL) { + BarUiMsg (&app->settings, MSG_INFO, "No tracks left.\n"); + app->nextStation = NULL; + } + } + app->curStation = app->nextStation; + break; } - app->curStation = app->nextStation; BarUiStartEventCmd (&app->settings, "stationfetchplaylist", app->curStation, app->playlist, &app->player, app->ph.stations, pRet, wRet); @@ -243,14 +378,34 @@ static void BarMainStartPlayback (BarApp_t *app, pthread_t *playerThread) { const PianoSong_t * const curSong = app->playlist; assert (curSong != NULL); + app->stationStarted = true; BarUiPrintSong (&app->settings, curSong, app->curStation->isQuickMix ? PianoFindStationById (app->ph.stations, curSong->stationId) : NULL); + if(app->curStation->stationType != PIANO_TYPE_STATION && + curSong->audioUrl == NULL) + { + PianoRequestDataGetPlaylist_t reqData; + PianoReturn_t pRet; + CURLcode wRet; + + reqData.station = app->curStation; + reqData.quality = app->settings.audioQuality; + reqData.retPlaylist = app->playlist; + + BarUiMsg (&app->settings, MSG_INFO, "Get playback info ... "); + BarUiPianoCall (app, PIANO_REQUEST_GET_PLAYBACK_INFO, + &reqData, &pRet, &wRet); + } + static const char httpPrefix[] = "http://"; + static const char httpsPrefix[] = "https://"; /* avoid playing local files */ - if (curSong->audioUrl == NULL || - strncmp (curSong->audioUrl, httpPrefix, strlen (httpPrefix)) != 0) { + if (curSong->audioUrl == NULL + || (strncmp (curSong->audioUrl, httpPrefix, strlen (httpPrefix)) != 0 + && strncmp (curSong->audioUrl, httpsPrefix, strlen (httpsPrefix))) != 0) + { BarUiMsg (&app->settings, MSG_ERR, "Invalid song url.\n"); } else { player_t * const player = &app->player; @@ -357,7 +512,11 @@ static void BarMainLoop (BarApp_t *app) { return; } - if (!BarMainGetStations (app)) { + if(app->ph.user.IsSubscriber) { + BarMainGetUserProfile (app); + } + + if (!BarMainGetAllStations (app)) { return; } @@ -386,9 +545,29 @@ static void BarMainLoop (BarApp_t *app) { } if (app->playlist == NULL && app->nextStation != NULL && !app->doQuit) { if (app->nextStation != app->curStation) { + app->stationStarted = false; BarUiPrintStation (&app->settings, app->nextStation); } - BarMainGetPlaylist (app); + switch(app->nextStation->stationType) { + case PIANO_TYPE_STATION: + case PIANO_TYPE_PLAYLIST: + // when these types finish playing just start them over + BarMainGetPlaylist (app); + break; + + case PIANO_TYPE_ALBUM: + case PIANO_TYPE_PODCAST: + case PIANO_TYPE_TRACK: + if(app->stationStarted) { + // when these types finish playing prompt user to select a new "station" + app->nextStation = NULL; + BarUiActSelectStation( app, NULL,NULL,BAR_DC_UNDEFINED); + } + else { + BarMainGetPlaylist (app); + } + break; + } } /* song ready to play */ if (app->playlist != NULL) { @@ -427,6 +606,82 @@ static void BarMainSetupSigaction () { sigaction (SIGINT, &act, NULL); } +const char *StationType2Str(PianoStationType_t Type) +{ + const char *Ret = "Invalid station type"; + const char *StationTypeStrings[] = { + "station", // PIANO_TYPE_NONE + "station", // PIANO_TYPE_STATION + "podcast", // PIANO_TYPE_PODCAST + "playlist", // PIANO_TYPE_PLAYLIST + "album", // PIANO_TYPE_ALBUM + "track", // PIANO_TYPE_TRACK + }; + if(Type <= PIANO_TYPE_TRACK) { + Ret = StationTypeStrings[Type]; + } + return Ret; +} + +PianoSong_t *CopySong(PianoSong_t *song) +{ + PianoSong_t *Ret = calloc(1, sizeof (PianoSong_t)); + + if(Ret != NULL) { + if(song->artist != NULL) { + Ret->artist = strdup(song->artist); + } + if(song->stationId != NULL) { + Ret->stationId = strdup(song->stationId); + } + if(song->album != NULL) { + Ret->album = strdup(song->album); + } + if(song->audioUrl != NULL) { + Ret->audioUrl = strdup(song->audioUrl); + } + if(song->coverArt != NULL) { + Ret->coverArt = strdup(song->coverArt); + } + if(song->musicId != NULL) { + Ret->musicId = strdup(song->musicId); + } + if(song->title != NULL) { + Ret->title = strdup(song->title); + } + if(song->seedId != NULL) { + Ret->seedId = strdup(song->seedId); + } + if(song->feedbackId != NULL) { + Ret->feedbackId = strdup(song->feedbackId); + } + if(song->detailUrl != NULL) { + Ret->detailUrl = strdup(song->detailUrl); + } + if(song->trackToken != NULL) { + Ret->trackToken = strdup(song->trackToken); + } + Ret->fileGain = song->fileGain; + Ret->length = song->length; + Ret->rating = song->rating; + Ret->audioFormat = song->audioFormat; + } + + return Ret; +} + +PianoSong_t *CopyPlaylist(PianoSong_t *song) +{ + PianoSong_t *Ret = NULL; + + while(song != NULL) { + PianoSong_t *NewSong = CopySong(song); + Ret = PianoListAppend(&Ret->head,&NewSong->head); + song = (PianoSong_t *) song->head.next; + } + return Ret; +} + int main (int argc, char **argv) { static BarApp_t app; @@ -513,6 +768,7 @@ int main (int argc, char **argv) { PianoDestroy (&app.ph); PianoDestroyPlaylist (app.songHistory); PianoDestroyPlaylist (app.playlist); + PianoDestroyPlaylist (app.FullPlaylist); curl_easy_cleanup (app.http); curl_global_cleanup (); BarPlayerDestroy (&app.player); diff --git a/src/main.h b/src/main.h index 7dc0ac8df..a5b6559b7 100644 --- a/src/main.h +++ b/src/main.h @@ -45,8 +45,14 @@ typedef struct { sig_atomic_t doQuit; BarReadlineFds_t input; unsigned int playerErrors; + PianoStationType_t Filter; + char stationStarted; + PianoSong_t *FullPlaylist; } BarApp_t; #include extern sig_atomic_t *interrupted; +const char *StationType2Str(PianoStationType_t Type); +PianoSong_t *CopySong(PianoSong_t *song); +PianoSong_t *CopyPlaylist(PianoSong_t *song); diff --git a/src/settings.c b/src/settings.c index 5859b827b..df11c4829 100644 --- a/src/settings.c +++ b/src/settings.c @@ -174,7 +174,7 @@ void BarSettingsRead (BarSettings_t *settings) { settings->tiredIcon = strdup (" zZ"); settings->atIcon = strdup (" @ "); settings->npSongFormat = strdup ("\"%t\" by \"%a\" on \"%l\"%r%@%s"); - settings->npStationFormat = strdup ("Station \"%n\" (%i)"); + settings->npStationFormat = strdup ("%s \"%n\" (%i)"); settings->listSongFormat = strdup ("%i) %a - %t%r"); settings->timeFormat = strdup ("%s%r/%t"); settings->rpcHost = strdup (PIANO_RPC_HOST); diff --git a/src/settings.h b/src/settings.h index 2e5a37824..499811da4 100644 --- a/src/settings.h +++ b/src/settings.h @@ -59,8 +59,11 @@ typedef enum { BAR_KS_PAUSE = 27, BAR_KS_VOLRESET = 28, BAR_KS_SETTINGS = 29, - /* insert new shortcuts _before_ this element and increase its value */ - BAR_KS_COUNT = 30, + BAR_KS_MODE = 30, + BAR_KS_GOTO = 31, + + /* insert new shortcuts _before_ this element and increase its value */ + BAR_KS_COUNT = 32, } BarKeyShortcutId_t; #define BAR_KS_DISABLED '\x00' diff --git a/src/ui.c b/src/ui.c index 0c7386ad3..1cb366018 100644 --- a/src/ui.c +++ b/src/ui.c @@ -467,6 +467,7 @@ PianoStation_t *BarUiSelectStation (BarApp_t *app, PianoStation_t *stations, PianoStation_t **sortedStations = NULL, *retStation = NULL; size_t stationCount, i, lastDisplayed, displayCount; char buf[100]; + PianoStationType_t Filter = app->Filter; if (stations == NULL) { BarUiMsg (&app->settings, MSG_ERR, "No station available.\n"); @@ -484,11 +485,37 @@ PianoStation_t *BarUiSelectStation (BarApp_t *app, PianoStation_t *stations, for (i = 0; i < stationCount; i++) { const PianoStation_t *currStation = sortedStations[i]; /* filter stations */ - if (BarStrCaseStr (currStation->name, buf) != NULL) { - BarUiMsg (&app->settings, MSG_LIST, "%2zi) %c%c%c %s\n", i, + if (BarStrCaseStr (currStation->name, buf) != NULL && + (Filter == PIANO_TYPE_NONE || Filter == currStation->stationType)) + { + char StationTypeChar = ' '; + switch(currStation->stationType) { + case PIANO_TYPE_STATION: + if(!currStation->isCreator) { + StationTypeChar = 'S'; + } + break; + + case PIANO_TYPE_PLAYLIST: + StationTypeChar = 'P'; + break; + + case PIANO_TYPE_PODCAST: + StationTypeChar = 'C'; + break; + + case PIANO_TYPE_ALBUM: + StationTypeChar = 'A'; + break; + + case PIANO_TYPE_TRACK: + StationTypeChar = 'T'; + break; + } + BarUiMsg (&app->settings, MSG_LIST, "%2zi) %c%c%c %s\n", displayCount, currStation->useQuickMix ? 'q' : ' ', currStation->isQuickMix ? 'Q' : ' ', - !currStation->isCreator ? 'S' : ' ', + StationTypeChar, currStation->name); ++displayCount; lastDisplayed = i; @@ -508,8 +535,22 @@ PianoStation_t *BarUiSelectStation (BarApp_t *app, PianoStation_t *stations, if (isnumeric (buf)) { unsigned long selected = strtoul (buf, NULL, 0); - if (selected < stationCount) { - retStation = sortedStations[selected]; + if (selected < displayCount) { + memset (buf, 0, sizeof (buf)); + displayCount = 0; + for (i = 0; i < stationCount; i++) { + PianoStation_t *currStation = sortedStations[i]; + /* filter stations */ + if (BarStrCaseStr (currStation->name, buf) != NULL && + (Filter == PIANO_TYPE_NONE || Filter == currStation->stationType)) + { + if(displayCount == selected) { + retStation = currStation; + break; + } + ++displayCount; + } + } } } @@ -746,12 +787,20 @@ static void BarUiAppendNewline (char *s, size_t maxlen) { void BarUiPrintStation (const BarSettings_t *settings, PianoStation_t *station) { char outstr[512]; - const char *vals[] = {station->name, station->id}; - - BarUiCustomFormat (outstr, sizeof (outstr), settings->npStationFormat, - "ni", vals); - BarUiAppendNewline (outstr, sizeof (outstr)); - BarUiMsg (settings, MSG_PLAYING, "%s", outstr); + char StationTypeStr[32]; + const char *vals[] = {StationTypeStr, station->name, station->id}; + strcpy(StationTypeStr,StationType2Str(station->stationType)); + StationTypeStr[0] = toupper(StationTypeStr[0]); + + switch(station->stationType) { + case PIANO_TYPE_STATION: + case PIANO_TYPE_PODCAST: + BarUiCustomFormat (outstr, sizeof (outstr), settings->npStationFormat, + "sni", vals); + BarUiAppendNewline (outstr, sizeof (outstr)); + BarUiMsg (settings, MSG_PLAYING, "%s", outstr); + break; + } } static const char *ratingToIcon (const BarSettings_t * const settings, @@ -785,8 +834,14 @@ void BarUiPrintSong (const BarSettings_t *settings, station != NULL ? station->name : "", song->detailUrl}; - BarUiCustomFormat (outstr, sizeof (outstr), settings->npSongFormat, - "talr@su", vals); + if(song->seedId != NULL && strncmp(song->seedId,"PC:",3) == 0) { + // "Song" is a podcast + strcpy(outstr,song->title); + } + else { + BarUiCustomFormat (outstr, sizeof (outstr), settings->npSongFormat, + "talr@su", vals); + } BarUiAppendNewline (outstr, sizeof (outstr)); BarUiMsg (settings, MSG_PLAYING, "%s", outstr); } @@ -887,6 +942,37 @@ static void BarUiEventcmdPrintSong (FILE * restrict stream, ); } +/* let user pick one station + * @param app handle + * @param stations that should be listed + * @param prompt string + * @param called if input was not a number + * @param auto-select if only one station remains after filtering + * @return pointer to selected station or NULL + */ +void BarUiSelectFilter(BarApp_t *app) +{ + char buf[100]; + const char *FilterTypes[] = { + "All","Stations","Podcasts","Playlists","Albums","Tracks",NULL + }; + + for(int i = 0; FilterTypes[i] != NULL; i++) { + BarUiMsg (&app->settings, MSG_LIST, "%i) %s\n", i,FilterTypes[i]); + if(i == PIANO_TYPE_PODCAST && !app->ph.user.IsSubscriber){ + // Must be a subscriber for Playlists, etc + break; + } + } + BarUiMsg (&app->settings, MSG_QUESTION, "%s", "Select filter: "); + + if (!BarReadlineStr (buf, sizeof (buf), &app->input, BAR_RL_DEFAULT) == 0 + && isnumeric (buf)) + { + app->Filter = atoi(buf); + } +} + /* Excute external event handler * @param settings containing the cmdline * @param event type diff --git a/src/ui.h b/src/ui.h index 33d2a76df..de51ebf72 100644 --- a/src/ui.h +++ b/src/ui.h @@ -55,4 +55,5 @@ bool BarUiPianoCall (BarApp_t * const, const PianoRequestType_t, void BarUiHistoryPrepend (BarApp_t *app, PianoSong_t *song); void BarUiCustomFormat (char *dest, size_t destSize, const char *format, const char *formatChars, const char **formatVals); +void BarUiSelectFilter(BarApp_t *app); diff --git a/src/ui_act.c b/src/ui_act.c index fa5c43bc6..740d3c142 100644 --- a/src/ui_act.c +++ b/src/ui_act.c @@ -34,6 +34,7 @@ THE SOFTWARE. #include "ui.h" #include "ui_readline.h" #include "ui_dispatch.h" +#include "debug_log.h" /* standard eventcmd call */ @@ -90,7 +91,7 @@ BarUiActCallback(BarUiActHelp) { BarUiMsg (&app->settings, MSG_NONE, "\r"); for (size_t i = 0; i < BAR_KS_COUNT; i++) { if (dispatchActions[i].helpText != NULL && - (context & dispatchActions[i].context) == dispatchActions[i].context && + BarUiContextMatch(context,dispatchActions[i].context,NULL) && app->settings.keys[i] != BAR_KS_DISABLED) { BarUiMsg (&app->settings, MSG_LIST, "%c %s\n", app->settings.keys[i], dispatchActions[i].helpText); @@ -225,12 +226,17 @@ BarUiActCallback(BarUiActAddSharedStation) { } static void drainPlaylist (BarApp_t * const app) { + app->stationStarted = false; BarUiDoSkipSong (&app->player); if (app->playlist != NULL) { /* drain playlist */ PianoDestroyPlaylist (PianoListNextP (app->playlist)); app->playlist->head.next = NULL; } + if(app->FullPlaylist != NULL) { + PianoDestroyPlaylist (app->FullPlaylist); + app->FullPlaylist = NULL; + } } /* delete current station @@ -238,23 +244,52 @@ static void drainPlaylist (BarApp_t * const app) { BarUiActCallback(BarUiActDeleteStation) { PianoReturn_t pRet; CURLcode wRet; - + bool bDeleted; + PianoRequestType_t requestType = 0; + char Temp[32]; + const char *StationTypeStr; assert (selStation != NULL); + StationTypeStr = StationType2Str(selStation->stationType); + + switch(selStation->stationType) { + case PIANO_TYPE_STATION: + requestType = PIANO_REQUEST_DELETE_STATION; + break; + + case PIANO_TYPE_PLAYLIST: + case PIANO_TYPE_ALBUM: + case PIANO_TYPE_TRACK: + requestType = PIANO_REQUEST_REMOVE_ITEM; + break; + + default: + BarUiMsg (&app->settings, MSG_ERR, "Delete not implemented for %ss.\n", + StationTypeStr); + break; + } - BarUiMsg (&app->settings, MSG_QUESTION, "Really delete \"%s\"? [yN] ", - selStation->name); - if (BarReadlineYesNo (false, &app->input)) { - BarUiMsg (&app->settings, MSG_INFO, "Deleting station... "); - if (BarUiActDefaultPianoCall (PIANO_REQUEST_DELETE_STATION, - selStation) && selStation == app->curStation) { - drainPlaylist (app); - app->nextStation = NULL; - /* XXX: usually we shoudn’t touch cur*, but DELETE_STATION destroys - * station struct */ - app->curStation = NULL; - selStation = NULL; + if(requestType) { + BarUiMsg (&app->settings, MSG_QUESTION, "Really delete the %s \"%s\"? [yN] ", + StationTypeStr,selStation->name); + if (BarReadlineYesNo (false, &app->input)) { + BarUiMsg (&app->settings, MSG_INFO, "Deleting %s... ",StationTypeStr); + if (BarUiActDefaultPianoCall (requestType,selStation) + && selStation == app->curStation) + { + drainPlaylist (app); + if(app->FullPlaylist != NULL) { + PianoDestroyPlaylist (app->FullPlaylist); + app->FullPlaylist = NULL; + } + app->nextStation = NULL; + /* XXX: usually we shoudn’t touch cur*, but DELETE_STATION destroys + * station struct */ + app->curStation = NULL; + selStation = NULL; + } + snprintf(Temp,sizeof(Temp),"%sdelete",StationTypeStr); + BarUiActDefaultEventcmd (Temp); } - BarUiActDefaultEventcmd ("stationdelete"); } } @@ -481,12 +516,18 @@ BarUiActCallback(BarUiActRenameStation) { /* play another station */ BarUiActCallback(BarUiActSelectStation) { + char prompt[32]; + snprintf(prompt,sizeof(prompt),"Select %s: ",StationType2Str(app->Filter)); PianoStation_t *newStation = BarUiSelectStation (app, app->ph.stations, - "Select station: ", NULL, app->settings.autoselect); + prompt, NULL, app->settings.autoselect); if (newStation != NULL) { app->nextStation = newStation; drainPlaylist (app); } + if(app->FullPlaylist != NULL) { + PianoDestroyPlaylist (app->FullPlaylist); + app->FullPlaylist = NULL; + } } /* ban song for 1 month @@ -927,6 +968,10 @@ BarUiActCallback(BarUiActManageStation) { PIANO_REQUEST_SET_STATION_MODE, &subReqDataSet)) { drainPlaylist (app); } + if(app->FullPlaylist != NULL) { + PianoDestroyPlaylist (app->FullPlaylist); + app->FullPlaylist = NULL; + } BarUiActDefaultEventcmd ("stationsetmode"); break; } @@ -938,3 +983,130 @@ BarUiActCallback(BarUiActManageStation) { PianoDestroyStationInfo (&reqData.info); } + +/* set station list filter + */ +BarUiActCallback(BarUiActFilter) { + BarUiSelectFilter(app); +} + +/* skip to specific track in a playlist or album */ +BarUiActCallback(BarUiActGotoSong) { + assert (selStation != NULL); + PianoRequestType_t requestType = 0; + PianoSong_t *song = NULL; + + switch(selStation->stationType) { + case PIANO_TYPE_ALBUM: { + song = app->FullPlaylist; + int i; + while(song != NULL) { + BarUiMsg (&app->settings, MSG_LIST, "%s\n", song->title); + song = (PianoSong_t *) song->head.next; + } + BarUiMsg (&app->settings, MSG_QUESTION, "Select track: "); + if (BarReadlineInt (&i, &app->input) == 0) { + return; + } + song = app->FullPlaylist; + while(song != NULL) { + int TrackNumber; + sscanf(song->title,"%d",&TrackNumber); + if(i == TrackNumber) { + LOG("Selected %s\n",song->title); + break; + } + song = (PianoSong_t *) song->head.next; + } + break; + } + + case PIANO_TYPE_PLAYLIST: { + song = app->FullPlaylist; + int i = 1; + while(song != NULL) { + BarUiMsg (&app->settings, MSG_LIST, "%d) %s\n", i ,song->title); + song = (PianoSong_t *) song->head.next; + i++; + } + BarUiMsg (&app->settings, MSG_QUESTION, "Select song: "); + if (BarReadlineInt (&i, &app->input) == 0) { + return; + } + + song = app->FullPlaylist; + int j = 1; + while(song != NULL) { + if(i == j++) { + break; + } + song = (PianoSong_t *) song->head.next; + } + break; + } + + case PIANO_TYPE_PODCAST: { + PianoReturn_t pRet; + CURLcode wRet; + PianoRequestDataGetEpisodes_t reqData; + int i = 1; + + reqData.station = selStation; + reqData.playList = NULL; + reqData.bGetAll = true; + + if (BarUiActDefaultPianoCall(PIANO_REQUEST_GET_EPISODES,&reqData) ) { + song = reqData.playList; + while(song != NULL) { + BarUiMsg (&app->settings, MSG_LIST, "%2d) %s\n", i ,song->title); + song = (PianoSong_t *) song->head.next; + i++; + } + BarUiMsg (&app->settings, MSG_QUESTION, "Select episode: "); + if (BarReadlineInt (&i, &app->input) != 0) { + song = reqData.playList; + int j = 1; + while(song != NULL) { + if(i == j++) { + break; + } + song = (PianoSong_t *) song->head.next; + } + if(song != NULL) { + PianoSong_t *NewSong = CopySong(app->playlist); + LOG("Selected '%s'\n",song->title); + + assert(NewSong->trackToken != NULL); + assert(NewSong->title != NULL); + free(NewSong->trackToken); + NewSong->trackToken = strdup(song->trackToken); + free(NewSong->title); + NewSong->title = strdup(song->title); + NewSong->fileGain = 0.0; + song = NewSong; + } + } + } + PianoDestroyPlaylist(reqData.playList); + break; + } + + case PIANO_TYPE_STATION: + case PIANO_TYPE_TRACK: + default: + BarUiMsg (&app->settings, MSG_ERR, "GoTo not implemented for %ss.\n", + StationType2Str(selStation->stationType)); + break; + } + + if(song != NULL) { + BarUiDoSkipSong (&app->player); + assert(app->playlist != NULL); + PianoSong_t *PlayTail = (PianoSong_t *) app->playlist->head.next; + + if (PlayTail != NULL) { + PianoDestroyPlaylist (PianoListNextP (PlayTail)); + } + app->playlist->head.next = (PianoListHead_t *) CopyPlaylist(song); + } +} diff --git a/src/ui_act.h b/src/ui_act.h index fb4457b08..b2c947d8c 100644 --- a/src/ui_act.h +++ b/src/ui_act.h @@ -61,4 +61,6 @@ BarUiActCallback(BarUiActVolUp); BarUiActCallback(BarUiActManageStation); BarUiActCallback(BarUiActVolReset); BarUiActCallback(BarUiActSettings); +BarUiActCallback(BarUiActFilter); +BarUiActCallback(BarUiActGotoSong); diff --git a/src/ui_dispatch.c b/src/ui_dispatch.c index 93c77c869..2c0e24a1b 100644 --- a/src/ui_dispatch.c +++ b/src/ui_dispatch.c @@ -26,6 +26,7 @@ THE SOFTWARE. #include "ui_dispatch.h" #include "settings.h" #include "ui.h" +#include "debug_log.h" /* handle global keyboard shortcuts * @return BAR_KS_* if action was performed or BAR_KS_COUNT on error/if no @@ -37,9 +38,27 @@ BarKeyShortcutId_t BarUiDispatch (BarApp_t *app, const char key, PianoStation_t assert (app != NULL); assert (sizeof (app->settings.keys) / sizeof (*app->settings.keys) == sizeof (dispatchActions) / sizeof (*dispatchActions)); + BarUiDispatchContext_t AllowedStationTypes = context & BAR_DC_ALL_STATION_TYPES; + BarUiDispatchContext_t TempContext; + BarKeyShortcutId_t Ret = BAR_KS_COUNT; if (selStation != NULL) { + const BarUiDispatchContext_t StationTypes[] = { + BAR_DC_UNDEFINED, + BAR_DC_STATION_TYPE_STATION, + BAR_DC_STATION_TYPE_PODCAST, + BAR_DC_STATION_TYPE_PLAYLIST, + BAR_DC_STATION_TYPE_ALBUM, + BAR_DC_STATION_TYPE_TRACK + }; context |= BAR_DC_STATION; + PianoStationType_t stationType = selStation->stationType; + if(stationType >= PIANO_TYPE_NONE && stationType < PIANO_TYPE_LAST) { + context |= StationTypes[stationType]; + } + else { + ELOG("Internal error: stationType 0x%x\n",stationType); + } } if (selSong != NULL) { context |= BAR_DC_SONG; @@ -47,25 +66,55 @@ BarKeyShortcutId_t BarUiDispatch (BarApp_t *app, const char key, PianoStation_t for (size_t i = 0; i < BAR_KS_COUNT; i++) { if (app->settings.keys[i] != BAR_KS_DISABLED && - app->settings.keys[i] == key) { - if ((dispatchActions[i].context & context) == dispatchActions[i].context) { - assert (dispatchActions[i].function != NULL); - - dispatchActions[i].function (app, selStation, selSong, - context); - return i; - } else if (verbose) { - if (dispatchActions[i].context & BAR_DC_SONG) { - BarUiMsg (&app->settings, MSG_ERR, "No song playing.\n"); - } else if (dispatchActions[i].context & BAR_DC_STATION) { - BarUiMsg (&app->settings, MSG_ERR, "No station selected.\n"); - } else { - assert (0); + app->settings.keys[i] == key) + { + const char *ErrMsg = NULL; + if(!BarUiContextMatch(context,dispatchActions[i].context,&ErrMsg)) { + if(verbose && ErrMsg != NULL) { + BarUiMsg (&app->settings, MSG_ERR, "%s",ErrMsg); } - return BAR_KS_COUNT; + break; } + assert (dispatchActions[i].function != NULL); + dispatchActions[i].function (app, selStation, selSong,context); + Ret = i; + break; } } - return BAR_KS_COUNT; + return Ret; +} + +bool BarUiContextMatch( + BarUiDispatchContext_t Have, + BarUiDispatchContext_t Need, + const char **ErrMsg) +{ + BarUiDispatchContext_t NeedStation = Need & BAR_DC_ALL_STATION_TYPES; + BarUiDispatchContext_t HaveStation = Have & BAR_DC_ALL_STATION_TYPES; + bool Ret = false; + + Need &= ~BAR_DC_ALL_STATION_TYPES; + Have &= ~BAR_DC_ALL_STATION_TYPES; + + do { + if(NeedStation != 0 && (NeedStation & HaveStation) == 0) { + // Station type reqirement not met + break; + } + if(Need != BAR_DC_UNDEFINED && (Need & Have) != Need) { + break; + if(ErrMsg != NULL) { + if((Need & BAR_DC_SONG) && !(Have & BAR_DC_SONG)) { + *ErrMsg = "No song playing.\n"; + } + else if((Need & BAR_DC_STATION) && !(Have & BAR_DC_STATION)) { + *ErrMsg = "No station selected.\n"; + } + } + } + Ret = true; + } while(false); + + return Ret; } diff --git a/src/ui_dispatch.h b/src/ui_dispatch.h index 72de887fe..5606a9095 100644 --- a/src/ui_dispatch.h +++ b/src/ui_dispatch.h @@ -29,8 +29,19 @@ typedef enum { BAR_DC_GLOBAL = 1, /* top-level action */ BAR_DC_STATION = 2, /* station selected */ BAR_DC_SONG = 4, /* song selected */ + BAR_DC_STATION_TYPE_STATION = 8, + BAR_DC_STATION_TYPE_PLAYLIST = 0x10, + BAR_DC_STATION_TYPE_PODCAST = 0x20, + BAR_DC_STATION_TYPE_ALBUM = 0x40, + BAR_DC_STATION_TYPE_TRACK = 0x80, } BarUiDispatchContext_t; +#define BAR_DC_ALL_STATION_TYPES (BAR_DC_STATION_TYPE_STATION \ + | BAR_DC_STATION_TYPE_PLAYLIST \ + | BAR_DC_STATION_TYPE_PODCAST \ + | BAR_DC_STATION_TYPE_ALBUM \ + | BAR_DC_STATION_TYPE_TRACK) + #include "settings.h" #include "main.h" @@ -50,16 +61,18 @@ typedef struct { /* see settings.h */ static const BarUiDispatchAction_t dispatchActions[BAR_KS_COUNT] = { {'?', BAR_DC_UNDEFINED, BarUiActHelp, NULL, "act_help"}, - {'+', BAR_DC_SONG, BarUiActLoveSong, "love song", - "act_songlove"}, - {'-', BAR_DC_SONG, BarUiActBanSong, "ban song", "act_songban"}, - {'a', BAR_DC_STATION, BarUiActAddMusic, "add music to station", - "act_stationaddmusic"}, + {'+', BAR_DC_STATION_TYPE_STATION | BAR_DC_SONG, + BarUiActLoveSong, "love song","act_songlove"}, + {'-', BAR_DC_STATION_TYPE_STATION | BAR_DC_SONG, + BarUiActBanSong, "ban song", "act_songban"}, + {'a', BAR_DC_STATION_TYPE_STATION | BAR_DC_STATION, + BarUiActAddMusic, "add music to station","act_stationaddmusic"}, {'c', BAR_DC_GLOBAL, BarUiActCreateStation, "create new station", "act_stationcreate"}, - {'d', BAR_DC_STATION, BarUiActDeleteStation, "delete station", - "act_stationdelete"}, - {'e', BAR_DC_SONG, BarUiActExplain, "explain why this song is played", + {'d', BAR_DC_ALL_STATION_TYPES | BAR_DC_STATION, + BarUiActDeleteStation, "delete station","act_stationdelete"}, + {'e', BAR_DC_STATION_TYPE_STATION | BAR_DC_SONG, + BarUiActExplain, "explain why this song is played", "act_songexplain"}, {'g', BAR_DC_GLOBAL, BarUiActStationFromGenre, "add genre station", "act_stationaddbygenre"}, @@ -73,16 +86,16 @@ static const BarUiDispatchAction_t dispatchActions[BAR_KS_COUNT] = { {'p', BAR_DC_GLOBAL | BAR_DC_STATION, BarUiActTogglePause, "pause/resume playback", "act_songpausetoggle"}, {'q', BAR_DC_GLOBAL, BarUiActQuit, "quit", "act_quit"}, - {'r', BAR_DC_STATION, BarUiActRenameStation, "rename station", - "act_stationrename"}, + {'r', BAR_DC_STATION_TYPE_STATION | BAR_DC_STATION, + BarUiActRenameStation, "rename station","act_stationrename"}, {'s', BAR_DC_GLOBAL, BarUiActSelectStation, "change station", "act_stationchange"}, - {'t', BAR_DC_SONG, BarUiActTempBanSong, "tired (ban song for 1 month)", - "act_songtired"}, + {'t', BAR_DC_STATION_TYPE_STATION | BAR_DC_SONG, + BarUiActTempBanSong, "tired (ban song for 1 month)", "act_songtired"}, {'u', BAR_DC_GLOBAL | BAR_DC_STATION, BarUiActPrintUpcoming, "upcoming songs", "act_upcoming"}, - {'x', BAR_DC_STATION, BarUiActSelectQuickMix, "select quickmix stations", - "act_stationselectquickmix"}, + {'x', BAR_DC_STATION_TYPE_STATION | BAR_DC_STATION, + BarUiActSelectQuickMix, "select quickmix stations","act_stationselectquickmix"}, {'$', BAR_DC_SONG, BarUiActDebug, NULL, "act_debug"}, {'b', BAR_DC_SONG, BarUiActBookmark, "bookmark song/artist", "act_bookmark"}, @@ -90,8 +103,8 @@ static const BarUiDispatchAction_t dispatchActions[BAR_KS_COUNT] = { "act_voldown"}, {')', BAR_DC_GLOBAL, BarUiActVolUp, "increase volume", "act_volup"}, - {'=', BAR_DC_STATION, BarUiActManageStation, "manage station seeds/feedback/mode", - "act_managestation"}, + {'=', BAR_DC_STATION_TYPE_STATION | BAR_DC_STATION, BarUiActManageStation, + "manage station seeds/feedback/mode","act_managestation"}, {' ', BAR_DC_GLOBAL | BAR_DC_STATION, BarUiActTogglePause, NULL, "act_songpausetoggle2"}, {'v', BAR_DC_SONG, BarUiActCreateStationFromSong, @@ -104,6 +117,10 @@ static const BarUiDispatchAction_t dispatchActions[BAR_KS_COUNT] = { "act_volreset"}, {'!', BAR_DC_GLOBAL, BarUiActSettings, "change settings", "act_settings"}, + {'f', BAR_DC_GLOBAL, BarUiActFilter, "filter station list settings", + "act_filter"}, + {'G', BAR_DC_STATION_TYPE_ALBUM | BAR_DC_STATION_TYPE_PLAYLIST | BAR_DC_STATION_TYPE_PODCAST | BAR_DC_SONG, + BarUiActGotoSong, "goto song", "act_gotosong"}, }; #include @@ -112,4 +129,5 @@ static const BarUiDispatchAction_t dispatchActions[BAR_KS_COUNT] = { BarKeyShortcutId_t BarUiDispatch (BarApp_t *, const char, PianoStation_t *, PianoSong_t *, const bool, BarUiDispatchContext_t); +bool BarUiContextMatch(BarUiDispatchContext_t Have,BarUiDispatchContext_t Need,const char **ErrMsg);