From 417d5f893c8d07557bbce28140e087ed402882ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cicho=C5=84?= Date: Thu, 10 Dec 2015 14:15:06 +0100 Subject: [PATCH 01/25] Add missing new line after password prompt. --- src/main.c | 637 ++++++++++++++++++++++++++++------------------------- 1 file changed, 342 insertions(+), 295 deletions(-) diff --git a/src/main.c b/src/main.c index f0172da9..eca47056 100644 --- a/src/main.c +++ b/src/main.c @@ -1,6 +1,6 @@ /* Copyright (c) 2008-2013 - Lars-Dominik Braun + Lars-Dominik Braun Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -34,313 +34,356 @@ THE SOFTWARE. /* authenticate user */ -static bool BarMainLoginUser (BarApp_t *app) { - PianoReturn_t pRet; - PianoRequestDataLogin_t reqData; - bool ret; - - reqData.user = app->settings.username; - reqData.password = app->settings.password; - reqData.step = 0; - - BarUiMsg (&app->settings, MSG_INFO, "Login... "); - ret = BarUiPianoCall (app, PIANO_REQUEST_LOGIN, &reqData, &pRet); - BarUiStartEventCmd (&app->settings, "userlogin", NULL, NULL, &app->player, - NULL, pRet); - - return ret; +static bool BarMainLoginUser(BarApp_t *app) +{ + PianoReturn_t pRet; + PianoRequestDataLogin_t reqData; + bool ret; + + reqData.user = app->settings.username; + reqData.password = app->settings.password; + reqData.step = 0; + + BarUiMsg(&app->settings, MSG_INFO, "Login... "); + ret = BarUiPianoCall(app, PIANO_REQUEST_LOGIN, &reqData, &pRet); + BarUiStartEventCmd(&app->settings, "userlogin", NULL, NULL, &app->player, + NULL, pRet); + + return ret; } /* ask for username/password if none were provided in settings */ -static bool BarMainGetLoginCredentials (BarSettings_t *settings, - BarReadline_t rl) { - bool usernameFromConfig = true; - - if (settings->username == NULL) { - char nameBuf[100]; - - BarUiMsg (settings, MSG_QUESTION, "Email: "); - BarReadlineStr (nameBuf, sizeof (nameBuf), rl, BAR_RL_DEFAULT); - settings->username = strdup (nameBuf); - usernameFromConfig = false; - } - - if (settings->password == NULL) { - char passBuf[100]; - - if (usernameFromConfig) { - BarUiMsg (settings, MSG_QUESTION, "Email: %s\n", settings->username); - } - - if (settings->passwordCmd == NULL) { - BarUiMsg (settings, MSG_QUESTION, "Password: "); - BarReadlineStr (passBuf, sizeof (passBuf), rl, BAR_RL_NOECHO); - /* write missing newline */ - BarConsolePuts(""); - settings->password = strdup (passBuf); - } else { - //pid_t chld; - //int pipeFd[2]; - - //BarUiMsg (settings, MSG_INFO, "Requesting password from external helper... "); - - //if (pipe (pipeFd) == -1) { - // BarUiMsg (settings, MSG_NONE, "Error: %s\n", strerror (errno)); - // return false; - //} - - //chld = fork (); - //if (chld == 0) { - // /* child */ - // close (pipeFd[0]); - // dup2 (pipeFd[1], fileno (stdout)); - // execl ("/bin/sh", "/bin/sh", "-c", settings->passwordCmd, (char *) NULL); - // BarUiMsg (settings, MSG_NONE, "Error: %s\n", strerror (errno)); - // close (pipeFd[1]); - // exit (1); - //} else if (chld == -1) { - // BarUiMsg (settings, MSG_NONE, "Error: %s\n", strerror (errno)); - // return false; - //} else { - // /* parent */ - // int status; - - // close (pipeFd[1]); - // memset (passBuf, 0, sizeof (passBuf)); - // read (pipeFd[0], passBuf, sizeof (passBuf)-1); - // close (pipeFd[0]); - - // /* drop trailing newlines */ - // ssize_t len = strlen (passBuf)-1; - // while (len >= 0 && passBuf[len] == '\n') { - // passBuf[len] = '\0'; - // --len; - // } - - // waitpid (chld, &status, 0); - // if (WEXITSTATUS (status) == 0) { - // settings->password = strdup (passBuf); - // BarUiMsg (settings, MSG_NONE, "Ok.\n"); - // } else { - // BarUiMsg (settings, MSG_NONE, "Error: Exit status %i.\n", WEXITSTATUS (status)); - // return false; - // } - //} - return false; - } /* end else passwordCmd */ - } - - return true; +static bool BarMainGetLoginCredentials(BarSettings_t *settings, + BarReadline_t rl) +{ + bool usernameFromConfig = true; + + if (settings->username == NULL) + { + char nameBuf[100]; + + BarUiMsg(settings, MSG_QUESTION, "Email: "); + BarReadlineStr(nameBuf, sizeof(nameBuf), rl, BAR_RL_DEFAULT); + settings->username = strdup(nameBuf); + usernameFromConfig = false; + } + + if (settings->password == NULL) + { + char passBuf[100]; + + if (usernameFromConfig) + { + BarUiMsg(settings, MSG_QUESTION, "Email: %s\n", settings->username); + } + + if (settings->passwordCmd == NULL) + { + BarUiMsg(settings, MSG_QUESTION, "Password: "); + BarReadlineStr(passBuf, sizeof(passBuf), rl, BAR_RL_NOECHO); + /* write missing newline */ + BarConsolePutc('\n'); + settings->password = strdup(passBuf); + } + else + { + //pid_t chld; + //int pipeFd[2]; + + //BarUiMsg (settings, MSG_INFO, "Requesting password from external helper... "); + + //if (pipe (pipeFd) == -1) { + // BarUiMsg (settings, MSG_NONE, "Error: %s\n", strerror (errno)); + // return false; + //} + + //chld = fork (); + //if (chld == 0) { + // /* child */ + // close (pipeFd[0]); + // dup2 (pipeFd[1], fileno (stdout)); + // execl ("/bin/sh", "/bin/sh", "-c", settings->passwordCmd, (char *) NULL); + // BarUiMsg (settings, MSG_NONE, "Error: %s\n", strerror (errno)); + // close (pipeFd[1]); + // exit (1); + //} else if (chld == -1) { + // BarUiMsg (settings, MSG_NONE, "Error: %s\n", strerror (errno)); + // return false; + //} else { + // /* parent */ + // int status; + + // close (pipeFd[1]); + // memset (passBuf, 0, sizeof (passBuf)); + // read (pipeFd[0], passBuf, sizeof (passBuf)-1); + // close (pipeFd[0]); + + // /* drop trailing newlines */ + // ssize_t len = strlen (passBuf)-1; + // while (len >= 0 && passBuf[len] == '\n') { + // passBuf[len] = '\0'; + // --len; + // } + + // waitpid (chld, &status, 0); + // if (WEXITSTATUS (status) == 0) { + // settings->password = strdup (passBuf); + // BarUiMsg (settings, MSG_NONE, "Ok.\n"); + // } else { + // BarUiMsg (settings, MSG_NONE, "Error: Exit status %i.\n", WEXITSTATUS (status)); + // return false; + // } + //} + return false; + } /* end else passwordCmd */ + } + + return true; } /* get station list */ -static bool BarMainGetStations (BarApp_t *app) { - PianoReturn_t pRet; - bool ret; - - BarUiMsg (&app->settings, MSG_INFO, "Get stations... "); - ret = BarUiPianoCall (app, PIANO_REQUEST_GET_STATIONS, NULL, &pRet); - BarUiStartEventCmd (&app->settings, "usergetstations", NULL, NULL, &app->player, - app->ph.stations, pRet); - return ret; +static bool BarMainGetStations(BarApp_t *app) +{ + PianoReturn_t pRet; + bool ret; + + BarUiMsg(&app->settings, MSG_INFO, "Get stations... "); + ret = BarUiPianoCall(app, PIANO_REQUEST_GET_STATIONS, NULL, &pRet); + BarUiStartEventCmd(&app->settings, "usergetstations", NULL, NULL, &app->player, + app->ph.stations, pRet); + return ret; } /* get initial station from autostart setting or user rl */ -static void BarMainGetInitialStation (BarApp_t *app) { - /* try to get autostart station */ - if (app->settings.autostartStation != NULL) { - app->curStation = PianoFindStationById (app->ph.stations, - app->settings.autostartStation); - if (app->curStation == NULL) { - BarUiMsg (&app->settings, MSG_ERR, - "Error: Autostart station not found.\n"); - } - } - /* no autostart? ask the user */ - if (app->curStation == NULL) { - app->curStation = BarUiSelectStation (app, app->ph.stations, - "Select station: ", NULL, app->settings.autoselect); - } - if (app->curStation != NULL) { - BarUiPrintStation (&app->settings, app->curStation); - } +static void BarMainGetInitialStation(BarApp_t *app) +{ + /* try to get autostart station */ + if (app->settings.autostartStation != NULL) + { + app->curStation = PianoFindStationById(app->ph.stations, + app->settings.autostartStation); + if (app->curStation == NULL) + { + BarUiMsg(&app->settings, MSG_ERR, + "Error: Autostart station not found.\n"); + } + } + /* no autostart? ask the user */ + if (app->curStation == NULL) + { + app->curStation = BarUiSelectStation(app, app->ph.stations, + "Select station: ", NULL, app->settings.autoselect); + } + if (app->curStation != NULL) + { + BarUiPrintStation(&app->settings, app->curStation); + } } /* wait for user rl */ -static void BarMainHandleUserInput (BarApp_t *app) { - char buf[2]; - if (BarReadline (buf, sizeof (buf), NULL, app->rl, - BAR_RL_FULLRETURN | BAR_RL_NOECHO, 1) > 0) { - BarUiDispatch (app, buf[0], app->curStation, app->playlist, true, - BAR_DC_GLOBAL); - } +static void BarMainHandleUserInput(BarApp_t *app) +{ + char buf[2]; + if (BarReadline(buf, sizeof(buf), NULL, app->rl, + BAR_RL_FULLRETURN | BAR_RL_NOECHO, 1) > 0) + { + BarUiDispatch(app, buf[0], app->curStation, app->playlist, true, + BAR_DC_GLOBAL); + } } /* fetch new playlist */ -static void BarMainGetPlaylist (BarApp_t *app) { - PianoReturn_t pRet; - PianoRequestDataGetPlaylist_t reqData; - reqData.station = app->curStation; - reqData.quality = app->settings.audioQuality; - - BarUiMsg (&app->settings, MSG_INFO, "Receiving new playlist... "); - if (!BarUiPianoCall (app, PIANO_REQUEST_GET_PLAYLIST, - &reqData, &pRet)) { - app->curStation = NULL; - } else { - app->playlist = reqData.retPlaylist; - if (app->playlist == NULL) { - BarUiMsg (&app->settings, MSG_INFO, "No tracks left.\n"); - app->curStation = NULL; - } - } - BarUiStartEventCmd (&app->settings, "stationfetchplaylist", - app->curStation, app->playlist, &app->player, app->ph.stations, - pRet); +static void BarMainGetPlaylist(BarApp_t *app) +{ + PianoReturn_t pRet; + PianoRequestDataGetPlaylist_t reqData; + reqData.station = app->curStation; + reqData.quality = app->settings.audioQuality; + + BarUiMsg(&app->settings, MSG_INFO, "Receiving new playlist... "); + if (!BarUiPianoCall(app, PIANO_REQUEST_GET_PLAYLIST, + &reqData, &pRet)) + { + app->curStation = NULL; + } + else + { + app->playlist = reqData.retPlaylist; + if (app->playlist == NULL) + { + BarUiMsg(&app->settings, MSG_INFO, "No tracks left.\n"); + app->curStation = NULL; + } + } + BarUiStartEventCmd(&app->settings, "stationfetchplaylist", + app->curStation, app->playlist, &app->player, app->ph.stations, + pRet); } /* start new player thread */ -static void BarMainStartPlayback (BarApp_t *app) { - assert (app != NULL); - - const PianoSong_t * const curSong = app->playlist; - assert (curSong != NULL); - - BarUiPrintSong (&app->settings, curSong, app->curStation->isQuickMix ? - PianoFindStationById (app->ph.stations, - curSong->stationId) : NULL); - - static const char httpPrefix[] = "http://"; - /* avoid playing local files */ - if (curSong->audioUrl == NULL || - strncmp (curSong->audioUrl, httpPrefix, strlen (httpPrefix)) != 0) { - BarUiMsg (&app->settings, MSG_ERR, "Invalid song url.\n"); - } else { - BarPlayer2SetGain(app->player, curSong->fileGain); - BarPlayer2Open(app->player, curSong->audioUrl); - - /* throw event */ - BarUiStartEventCmd (&app->settings, "songstart", - app->curStation, curSong, &app->player, app->ph.stations, - PIANO_RET_OK); - - if (!BarPlayer2Play(app->player)) - ++app->playerErrors; - else - app->playerErrors = 0; - } +static void BarMainStartPlayback(BarApp_t *app) +{ + assert(app != NULL); + + const PianoSong_t * const curSong = app->playlist; + assert(curSong != NULL); + + BarUiPrintSong(&app->settings, curSong, app->curStation->isQuickMix ? + PianoFindStationById(app->ph.stations, + curSong->stationId) : NULL); + + static const char httpPrefix[] = "http://"; + /* avoid playing local files */ + if (curSong->audioUrl == NULL || + strncmp(curSong->audioUrl, httpPrefix, strlen(httpPrefix)) != 0) + { + BarUiMsg(&app->settings, MSG_ERR, "Invalid song url.\n"); + } + else + { + BarPlayer2SetGain(app->player, curSong->fileGain); + BarPlayer2Open(app->player, curSong->audioUrl); + + /* throw event */ + BarUiStartEventCmd(&app->settings, "songstart", + app->curStation, curSong, &app->player, app->ph.stations, + PIANO_RET_OK); + + if (!BarPlayer2Play(app->player)) + ++app->playerErrors; + else + app->playerErrors = 0; + } } /* player is done, clean up */ -static void BarMainPlayerCleanup (BarApp_t *app) { - BarUiStartEventCmd (&app->settings, "songfinish", app->curStation, - app->playlist, &app->player, app->ph.stations, PIANO_RET_OK); +static void BarMainPlayerCleanup(BarApp_t *app) +{ + BarUiStartEventCmd(&app->settings, "songfinish", app->curStation, + app->playlist, &app->player, app->ph.stations, PIANO_RET_OK); - BarPlayer2Finish(app->player); + BarPlayer2Finish(app->player); - BarConsoleSetTitle (TITLE); + BarConsoleSetTitle(TITLE); - if (app->playerErrors >= app->settings.maxPlayerErrors) { - /* don't continue playback if thread reports too many error */ - app->curStation = NULL; - app->playerErrors = 0; - } + if (app->playerErrors >= app->settings.maxPlayerErrors) + { + /* don't continue playback if thread reports too many error */ + app->curStation = NULL; + app->playerErrors = 0; + } } /* print song duration */ -static void BarMainPrintTime (BarApp_t *app) { - double songPlayed, songDuration, songRemaining; - char sign; - - songDuration = BarPlayer2GetDuration(app->player); - songPlayed = BarPlayer2GetTime(app->player); - - if (songPlayed <= songDuration) { - songRemaining = songDuration - songPlayed; - sign = '-'; - } else { - /* longer than expected */ - songRemaining = songPlayed - songDuration; - sign = '+'; - } - BarUiMsg (&app->settings, MSG_TIME, "%c%02u:%02u/%02u:%02u\r", - sign, (int)songRemaining / 60, (int)songRemaining % 60, - (int)songDuration / 60, (int)songDuration % 60); +static void BarMainPrintTime(BarApp_t *app) +{ + double songPlayed, songDuration, songRemaining; + char sign; + + songDuration = BarPlayer2GetDuration(app->player); + songPlayed = BarPlayer2GetTime(app->player); + + if (songPlayed <= songDuration) + { + songRemaining = songDuration - songPlayed; + sign = '-'; + } + else + { + /* longer than expected */ + songRemaining = songPlayed - songDuration; + sign = '+'; + } + BarUiMsg(&app->settings, MSG_TIME, "%c%02u:%02u/%02u:%02u\r", + sign, (int)songRemaining / 60, (int)songRemaining % 60, + (int)songDuration / 60, (int)songDuration % 60); } /* main loop */ -static void BarMainLoop (BarApp_t *app) { - if (!BarMainGetLoginCredentials (&app->settings, app->rl)) { - return; - } - - if (!BarMainLoginUser (app)) { - return; - } - - if (!BarMainGetStations (app)) { - return; - } - - BarMainGetInitialStation (app); - - while (!app->doQuit) { - /* song finished playing, clean up things/scrobble song */ - if (BarPlayer2IsStopped(app->player)) { - BarMainPlayerCleanup (app); - } - - /* check whether player finished playing and start playing new - * song */ - if (BarPlayer2IsFinished(app->player) && app->curStation != NULL) { - /* what's next? */ - if (app->playlist != NULL) { - PianoSong_t *histsong = app->playlist; - app->playlist = PianoListNextP (app->playlist); - histsong->head.next = NULL; - BarUiHistoryPrepend (app, histsong); - } - if (app->playlist == NULL) { - BarMainGetPlaylist (app); - } - /* song ready to play */ - if (app->playlist != NULL) { - BarMainStartPlayback (app); - } - } - - BarMainHandleUserInput (app); - - /* show time */ - if (BarPlayer2IsPlaying(app->player) || BarPlayer2IsPaused(app->player)) { - BarMainPrintTime (app); - } - } +static void BarMainLoop(BarApp_t *app) +{ + if (!BarMainGetLoginCredentials(&app->settings, app->rl)) + { + return; + } + + if (!BarMainLoginUser(app)) + { + return; + } + + if (!BarMainGetStations(app)) + { + return; + } + + BarMainGetInitialStation(app); + + while (!app->doQuit) + { + /* song finished playing, clean up things/scrobble song */ + if (BarPlayer2IsStopped(app->player)) + { + BarMainPlayerCleanup(app); + } + + /* check whether player finished playing and start playing new + * song */ + if (BarPlayer2IsFinished(app->player) && app->curStation != NULL) + { + /* what's next? */ + if (app->playlist != NULL) + { + PianoSong_t *histsong = app->playlist; + app->playlist = PianoListNextP(app->playlist); + histsong->head.next = NULL; + BarUiHistoryPrepend(app, histsong); + } + if (app->playlist == NULL) + { + BarMainGetPlaylist(app); + } + /* song ready to play */ + if (app->playlist != NULL) + { + BarMainStartPlayback(app); + } + } + + BarMainHandleUserInput(app); + + /* show time */ + if (BarPlayer2IsPlaying(app->player) || BarPlayer2IsPaused(app->player)) + { + BarMainPrintTime(app); + } + } } -int main (int argc, char **argv) { - static BarApp_t app; +int main(int argc, char **argv) +{ + static BarApp_t app; - memset (&app, 0, sizeof (app)); + memset(&app, 0, sizeof(app)); - BarConsoleInit (); + BarConsoleInit(); - BarConsoleSetTitle (TITLE); + BarConsoleSetTitle(TITLE); - /* init some things */ - BarSettingsInit (&app.settings); - BarSettingsRead (&app.settings); + /* init some things */ + BarSettingsInit(&app.settings); + BarSettingsRead(&app.settings); - if (!BarPlayer2Init (&app.player, app.settings.player)) + if (!BarPlayer2Init(&app.player, app.settings.player)) { if (app.settings.player) BarUiMsg(&app.settings, MSG_ERR, "Player \"%s\" initialization failed.", app.settings.player); @@ -349,47 +392,51 @@ int main (int argc, char **argv) { return 0; } - PianoReturn_t pret; - if ((pret = PianoInit (&app.ph, app.settings.partnerUser, - app.settings.partnerPassword, app.settings.device, - app.settings.inkey, app.settings.outkey)) != PIANO_RET_OK) { - BarUiMsg (&app.settings, MSG_ERR, "Initialization failed:" - " %s\n", PianoErrorToStr (pret)); - return 0; - } + PianoReturn_t pret; + if ((pret = PianoInit(&app.ph, app.settings.partnerUser, + app.settings.partnerPassword, app.settings.device, + app.settings.inkey, app.settings.outkey)) != PIANO_RET_OK) + { + BarUiMsg(&app.settings, MSG_ERR, "Initialization failed:" + " %s\n", PianoErrorToStr(pret)); + return 0; + } - BarUiMsg (&app.settings, MSG_NONE, - "Welcome to " PACKAGE " (" VERSION ")!\n"); - if (app.settings.keys[BAR_KS_HELP] == BAR_KS_DISABLED) { - BarUiMsg (&app.settings, MSG_NONE, "\n"); - } else { - BarUiMsg (&app.settings, MSG_NONE, - "Press %c for a list of commands.\n", - app.settings.keys[BAR_KS_HELP]); - } + BarUiMsg(&app.settings, MSG_NONE, + "Welcome to " PACKAGE " (" VERSION ")!\n"); + if (app.settings.keys[BAR_KS_HELP] == BAR_KS_DISABLED) + { + BarUiMsg(&app.settings, MSG_NONE, "\n"); + } + else + { + BarUiMsg(&app.settings, MSG_NONE, + "Press %c for a list of commands.\n", + app.settings.keys[BAR_KS_HELP]); + } - HttpInit(&app.http2, app.settings.rpcHost, app.settings.rpcTlsPort); - if (app.settings.controlProxy) - HttpSetProxy(app.http2, app.settings.controlProxy); + HttpInit(&app.http2, app.settings.rpcHost, app.settings.rpcTlsPort); + if (app.settings.controlProxy) + HttpSetProxy(app.http2, app.settings.controlProxy); - BarReadlineInit (&app.rl); + BarReadlineInit(&app.rl); - BarMainLoop (&app); + BarMainLoop(&app); - BarReadlineDestroy (app.rl); + BarReadlineDestroy(app.rl); - /* write statefile */ - BarSettingsWrite (app.curStation, &app.settings); + /* write statefile */ + BarSettingsWrite(app.curStation, &app.settings); - PianoDestroy (&app.ph); - PianoDestroyPlaylist (app.songHistory); - PianoDestroyPlaylist (app.playlist); - HttpDestroy (app.http2); - BarPlayer2Destroy (app.player); - BarSettingsDestroy (&app.settings); - BarConsoleDestroy (); + PianoDestroy(&app.ph); + PianoDestroyPlaylist(app.songHistory); + PianoDestroyPlaylist(app.playlist); + HttpDestroy(app.http2); + BarPlayer2Destroy(app.player); + BarSettingsDestroy(&app.settings); + BarConsoleDestroy(); - return 0; + return 0; } From 897f032bef66968bf8f14c03b71df3c2cd7a2499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cicho=C5=84?= Date: Thu, 25 Aug 2016 00:15:04 +0200 Subject: [PATCH 02/25] Update README.md --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d1afcd8..1dc78621 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,16 @@ pianobar is a console client for the personalized web radio [Pandora] ###Source Code -The source code can be downloaded at [github.com](http://github.com/PromyLOPh/pianobar/) +Original source code can be downloaded at [github.com](http://github.com/PromyLOPh/pianobar/) or [6xq.net](http://6xq.net/projects/pianobar/). +Windows port source code is available at this repository ([pianobar-windows](https://github.com/thedmd/pianobar-windows)). + +###Building + +Checkout [pianobar-windows-build](https://github.com/thedmd/pianobar-windows-build) where +you will find configured solution for Visual Studio 2015. + ###Download/Installation There are community provided packages available for most Linux distributions (see your distribution’s package manager), Mac OS X ([MacPorts](http://trac.macports.org/browser/trunk/dports/audio/pianobar/Portfile) or [homebrew](http://brew.sh/)) and *BSD as well as a [native Windows port](https://github.com/thedmd/pianobar-windows). From cf5b9193a9f6480c9f98f8fd8cb6e3a0a4e068e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cicho=C5=84?= Date: Thu, 25 Aug 2016 10:34:31 +0200 Subject: [PATCH 03/25] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 1dc78621..5322440a 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ pianobar is a console client for the personalized web radio [Pandora] * last.fm scrobbling support (external application) * Proxy support for listeners outside the USA. +###Binary + +Prebuild binary is available at [pianobar-windows-binaries](https://github.com/thedmd/pianobar-windows-binaries) repository. + ###Source Code Original source code can be downloaded at [github.com](http://github.com/PromyLOPh/pianobar/) From c07462d745f24bc46e8a18dd4eabf989cc8694a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cicho=C5=84?= Date: Thu, 25 Aug 2016 10:35:15 +0200 Subject: [PATCH 04/25] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 5322440a..ae1d7391 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,3 @@ Windows port source code is available at this repository ([pianobar-windows](htt Checkout [pianobar-windows-build](https://github.com/thedmd/pianobar-windows-build) where you will find configured solution for Visual Studio 2015. - -###Download/Installation - -There are community provided packages available for most Linux distributions (see your distribution’s package manager), Mac OS X ([MacPorts](http://trac.macports.org/browser/trunk/dports/audio/pianobar/Portfile) or [homebrew](http://brew.sh/)) and *BSD as well as a [native Windows port](https://github.com/thedmd/pianobar-windows). From efa50e960dab5d6134994bf81c18e5397f2ae924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cicho=C5=84?= Date: Thu, 25 Aug 2016 10:35:49 +0200 Subject: [PATCH 05/25] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ae1d7391..16aee6c9 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,11 @@ Prebuild binary is available at [pianobar-windows-binaries](https://github.com/t ###Source Code +Windows port source code is available at this repository ([pianobar-windows](https://github.com/thedmd/pianobar-windows)). + Original source code can be downloaded at [github.com](http://github.com/PromyLOPh/pianobar/) or [6xq.net](http://6xq.net/projects/pianobar/). -Windows port source code is available at this repository ([pianobar-windows](https://github.com/thedmd/pianobar-windows)). - ###Building Checkout [pianobar-windows-build](https://github.com/thedmd/pianobar-windows-build) where From ba183a09135c03763a4790cdafac2e43d8dbccd0 Mon Sep 17 00:00:00 2001 From: Lars-Dominik Braun Date: Thu, 11 Feb 2016 19:50:55 +0100 Subject: [PATCH 06/25] Add default value for rpcTlsPort Passing NULL to printf and the result to curl may or may not work. YMMV. Fixes #574. # Conflicts: # src/ui.c --- src/settings.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings.c b/src/settings.c index 0765bc84..6ba2e91d 100644 --- a/src/settings.c +++ b/src/settings.c @@ -180,7 +180,7 @@ void BarSettingsRead (BarSettings_t *settings) { settings->titleFormat = strdup (TITLE " - \"%t\" by \"%a\" on \"%l\"%r%@%s"); settings->player = NULL; settings->rpcHost = strdup (PIANO_RPC_HOST); - settings->rpcTlsPort = NULL; + settings->rpcTlsPort = strdup ("443"); settings->partnerUser = strdup ("android"); settings->partnerPassword = strdup ("AC7IBG09A3DTSYM4R41UJWL07VLN8JI7"); settings->device = strdup ("android-generic"); From 7c4b615a38a49e1bbe9c2315fed09920aa238b65 Mon Sep 17 00:00:00 2001 From: Lars-Dominik Braun Date: Sun, 21 Feb 2016 10:45:42 +0100 Subject: [PATCH 07/25] Makefile: Provide a way to disable silent rules Fixes #571 # Conflicts: # Makefile --- Makefile | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 78f1d435..00336603 100644 --- a/Makefile +++ b/Makefile @@ -84,26 +84,34 @@ ALL_LDFLAGS:=${LDFLAGS} -lao -lpthread -lm \ ${LIBAV_LDFLAGS} ${LIBGNUTLS_LDFLAGS} \ ${LIBGCRYPT_LDFLAGS} ${LIBJSONC_LDFLAGS} ${LIBCURL_LDFLAGS} +# Be verbose if V=1 (gnu autotools’ --disable-silent-rules) +SILENTCMD:=@ +SILENTECHO:=@echo +ifeq (${V},1) + SILENTCMD:= + SILENTECHO:=@true +endif + # build pianobar ifeq (${DYNLINK},1) pianobar: ${PIANOBAR_OBJ} ${PIANOBAR_HDR} libpiano.so.0 - @echo " LINK $@" - @${CC} -o $@ ${PIANOBAR_OBJ} -L. -lpiano ${ALL_LDFLAGS} + ${SILENTECHO} " LINK $@" + ${SILENTCMD}${CC} -o $@ ${PIANOBAR_OBJ} -L. -lpiano ${ALL_LDFLAGS} else pianobar: ${PIANOBAR_OBJ} ${PIANOBAR_HDR} ${LIBPIANO_OBJ} - @echo " LINK $@" - @${CC} -o $@ ${PIANOBAR_OBJ} ${LIBPIANO_OBJ} ${ALL_LDFLAGS} + ${SILENTECHO} " LINK $@" + ${SILENTCMD}${CC} -o $@ ${PIANOBAR_OBJ} ${LIBPIANO_OBJ} ${ALL_LDFLAGS} endif # build shared and static libpiano libpiano.so.0: ${LIBPIANO_RELOBJ} ${LIBPIANO_HDR} ${LIBPIANO_OBJ} - @echo " LINK $@" - @${CC} -shared -Wl,-soname,libpiano.so.0 -o libpiano.so.0.0.0 \ + ${SILENTECHO} " LINK $@" + ${SILENTCMD}${CC} -shared -Wl,-soname,libpiano.so.0 -o libpiano.so.0.0.0 \ ${LIBPIANO_RELOBJ} ${ALL_LDFLAGS} - @ln -fs libpiano.so.0.0.0 libpiano.so.0 - @ln -fs libpiano.so.0 libpiano.so - @echo " AR libpiano.a" - @${AR} rcs libpiano.a ${LIBPIANO_OBJ} + ${SILENTCMD}ln -fs libpiano.so.0.0.0 libpiano.so.0 + ${SILENTCMD}ln -fs libpiano.so.0 libpiano.so + ${SILENTECHO} " AR libpiano.a" + ${SILENTCMD}${AR} rcs libpiano.a ${LIBPIANO_OBJ} -include $(PIANOBAR_SRC:.c=.d) @@ -111,17 +119,17 @@ libpiano.so.0: ${LIBPIANO_RELOBJ} ${LIBPIANO_HDR} ${LIBPIANO_OBJ} # build standard object files %.o: %.c - @echo " CC $<" - @${CC} -c -o $@ ${ALL_CFLAGS} -MMD -MF $*.d -MP $< + ${SILENTECHO} " CC $<" + ${SILENTCMD}${CC} -c -o $@ ${ALL_CFLAGS} -MMD -MF $*.d -MP $< # create position independent code (for shared libraries) %.lo: %.c - @echo " CC $< (PIC)" - @${CC} -c -fPIC -o $@ ${ALL_CFLAGS} -MMD -MF $*.d -MP $< + ${SILENTECHO} " CC $< (PIC)" + ${SILENTCMD}${CC} -c -fPIC -o $@ ${ALL_CFLAGS} -MMD -MF $*.d -MP $< clean: - @echo " CLEAN" - @${RM} ${PIANOBAR_OBJ} ${LIBPIANO_OBJ} \ + ${SILENTECHO} " CLEAN" + ${SILENTCMD}${RM} ${PIANOBAR_OBJ} ${LIBPIANO_OBJ} \ ${LIBPIANO_RELOBJ} pianobar libpiano.so* \ libpiano.a $(PIANOBAR_SRC:.c=.d) $(LIBPIANO_SRC:.c=.d) From 817ff6a87020a55d7628d600e2ee09b947ee2f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cicho=C5=84?= Date: Wed, 17 May 2017 04:07:07 +0200 Subject: [PATCH 08/25] Show status code text if no other source of error is available. --- src/http/http.c | 21 +++++++++- src/main.c | 100 ++++++++++++++++++++++-------------------------- src/main.h | 4 +- src/ui.c | 3 +- src/ui_act.c | 18 +++++---- 5 files changed, 81 insertions(+), 65 deletions(-) diff --git a/src/http/http.c b/src/http/http.c index dd64ad19..609317f4 100644 --- a/src/http/http.c +++ b/src/http/http.c @@ -44,6 +44,7 @@ static wchar_t* HttpToWideString(const char* string, int size); static bool HttpCreateConnection (http_t http); static void HttpCloseConnection (http_t http); static void HttpSetLastError (http_t http, const char* message); +static void HttpSetLastErrorW (http_t http, const wchar_t* message); static void HttpSetLastErrorFromWinHttp (http_t http); static char* HttpFormatWinApiError (DWORD errorCode, HINSTANCE module); static char* HttpFormatWinHttpError (DWORD errorCode); @@ -118,11 +119,19 @@ static void HttpCloseConnection (http_t http) { static void HttpSetLastError (http_t http, const char* message) { free(http->error); http->error = NULL; - + if (message) http->error = strdup(message); } +static void HttpSetLastErrorW (http_t http, const wchar_t* message) { + free(http->error); + http->error = NULL; + + if (message) + http->error = HttpToString(message, wcslen(message)); +} + static void HttpSetLastErrorFromWinHttp (http_t http) { free(http->error); http->error = NULL; @@ -396,6 +405,13 @@ bool HttpRequest(http_t http, PianoRequest_t * const request) { } if (succeeded && statusCode == 407) { + wchar_t statusText[256] = { 0 }; + DWORD statusTextSize = sizeof(statusText) - 1; + WinHttpQueryHeaders(handle, + WINHTTP_QUERY_STATUS_TEXT, + WINHTTP_HEADER_NAME_BY_INDEX, + statusText, &statusTextSize, WINHTTP_NO_HEADER_INDEX); + HttpSetLastErrorW (http, statusText); requestSent = false; retry = true; } @@ -467,6 +483,9 @@ bool HttpRequest(http_t http, PianoRequest_t * const request) { HttpSetLastError (http, "Maximum retries count exceeded"); } + if (retryLimit == 0) + goto done; + complete = true; HttpSetLastError (http, NULL); diff --git a/src/main.c b/src/main.c index eca47056..6e99ada8 100644 --- a/src/main.c +++ b/src/main.c @@ -158,29 +158,21 @@ static bool BarMainGetStations(BarApp_t *app) /* get initial station from autostart setting or user rl */ -static void BarMainGetInitialStation(BarApp_t *app) -{ - /* try to get autostart station */ - if (app->settings.autostartStation != NULL) - { - app->curStation = PianoFindStationById(app->ph.stations, - app->settings.autostartStation); - if (app->curStation == NULL) - { - BarUiMsg(&app->settings, MSG_ERR, - "Error: Autostart station not found.\n"); - } - } - /* no autostart? ask the user */ - if (app->curStation == NULL) - { - app->curStation = BarUiSelectStation(app, app->ph.stations, - "Select station: ", NULL, app->settings.autoselect); - } - if (app->curStation != NULL) - { - BarUiPrintStation(&app->settings, app->curStation); - } +static void BarMainGetInitialStation (BarApp_t *app) { + /* try to get autostart station */ + if (app->settings.autostartStation != NULL) { + app->nextStation = PianoFindStationById (app->ph.stations, + app->settings.autostartStation); + if (app->nextStation == NULL) { + BarUiMsg (&app->settings, MSG_ERR, + "Error: Autostart station not found.\n"); + } + } + /* no autostart? ask the user */ + if (app->nextStation == NULL) { + app->nextStation = BarUiSelectStation (app, app->ph.stations, + "Select station: ", NULL, app->settings.autoselect); + } } /* wait for user rl @@ -198,31 +190,27 @@ static void BarMainHandleUserInput(BarApp_t *app) /* fetch new playlist */ -static void BarMainGetPlaylist(BarApp_t *app) -{ - PianoReturn_t pRet; - PianoRequestDataGetPlaylist_t reqData; - reqData.station = app->curStation; - reqData.quality = app->settings.audioQuality; - - BarUiMsg(&app->settings, MSG_INFO, "Receiving new playlist... "); - if (!BarUiPianoCall(app, PIANO_REQUEST_GET_PLAYLIST, - &reqData, &pRet)) - { - app->curStation = NULL; - } - else - { - app->playlist = reqData.retPlaylist; - if (app->playlist == NULL) - { - BarUiMsg(&app->settings, MSG_INFO, "No tracks left.\n"); - app->curStation = NULL; - } - } - BarUiStartEventCmd(&app->settings, "stationfetchplaylist", - app->curStation, app->playlist, &app->player, app->ph.stations, - pRet); +static void BarMainGetPlaylist (BarApp_t *app) { + PianoReturn_t pRet; + PianoRequestDataGetPlaylist_t reqData; + reqData.station = app->nextStation; + reqData.quality = app->settings.audioQuality; + + BarUiMsg (&app->settings, MSG_INFO, "Receiving new playlist... "); + if (!BarUiPianoCall (app, PIANO_REQUEST_GET_PLAYLIST, + &reqData, &pRet)) { + 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; + BarUiStartEventCmd (&app->settings, "stationfetchplaylist", + app->curStation, app->playlist, &app->player, app->ph.stations, + pRet); } /* start new player thread @@ -276,7 +264,7 @@ static void BarMainPlayerCleanup(BarApp_t *app) if (app->playerErrors >= app->settings.maxPlayerErrors) { /* don't continue playback if thread reports too many error */ - app->curStation = NULL; + app->nextStation = NULL; app->playerErrors = 0; } } @@ -338,7 +326,7 @@ static void BarMainLoop(BarApp_t *app) /* check whether player finished playing and start playing new * song */ - if (BarPlayer2IsFinished(app->player) && app->curStation != NULL) + if (BarPlayer2IsFinished(app->player) && app->nextStation != NULL) { /* what's next? */ if (app->playlist != NULL) @@ -348,10 +336,14 @@ static void BarMainLoop(BarApp_t *app) histsong->head.next = NULL; BarUiHistoryPrepend(app, histsong); } - if (app->playlist == NULL) - { - BarMainGetPlaylist(app); - } + if (app->playlist == NULL && app->nextStation != NULL && !app->doQuit) + { + if (app->nextStation != app->curStation) + { + BarUiPrintStation (&app->settings, app->nextStation); + } + BarMainGetPlaylist (app); + } /* song ready to play */ if (app->playlist != NULL) { diff --git a/src/main.h b/src/main.h index 6c34d717..e5e988dd 100644 --- a/src/main.h +++ b/src/main.h @@ -42,7 +42,9 @@ typedef struct { /* first item is current song */ PianoSong_t *playlist; PianoSong_t *songHistory; - PianoStation_t *curStation; + /* station of current song and station used to fetch songs from if playlist + * is empty */ + PianoStation_t *curStation, *nextStation; char doQuit; BarReadline_t rl; unsigned int playerErrors; diff --git a/src/ui.c b/src/ui.c index 8efc7aac..88ad4d7e 100644 --- a/src/ui.c +++ b/src/ui.c @@ -715,7 +715,8 @@ void BarUiStartEventCmd (const BarSettings_t *settings, const char *type, // pipeWriteFd = fdopen (pipeFd[1], "w"); - // if (curSong != NULL && stations != NULL && curStation->isQuickMix) { + // if (curSong != NULL && stations != NULL && curStation != NULL && + // curStation->isQuickMix) { // songStation = PianoFindStationById (stations, curSong->stationId); // } diff --git a/src/ui_act.c b/src/ui_act.c index 4206a332..cb1d7a8d 100644 --- a/src/ui_act.c +++ b/src/ui_act.c @@ -224,10 +224,14 @@ BarUiActCallback(BarUiActDeleteStation) { if (BarUiActDefaultPianoCall (PIANO_REQUEST_DELETE_STATION, selStation) && selStation == app->curStation) { BarUiDoSkipSong (app->player); - PianoDestroyPlaylist (PianoListNextP (app->playlist)); - app->playlist->head.next = NULL; - BarUiHistoryPrepend (app, app->playlist); - app->playlist = NULL; + if (app->playlist != NULL) { + /* drain playlist */ + PianoDestroyPlaylist (PianoListNextP (app->playlist)); + app->playlist->head.next = NULL; + } + app->nextStation = NULL; + /* XXX: usually we shoudn’t touch cur*, but DELETE_STATION destroys + * station struct */ app->curStation = NULL; } BarUiActDefaultEventcmd ("stationdelete"); @@ -446,14 +450,12 @@ BarUiActCallback(BarUiActSelectStation) { PianoStation_t *newStation = BarUiSelectStation (app, app->ph.stations, "Select station: ", NULL, app->settings.autoselect); if (newStation != NULL) { - app->curStation = newStation; - BarUiPrintStation (&app->settings, app->curStation); + app->nextStation = newStation; BarUiDoSkipSong (app->player); if (app->playlist != NULL) { + /* drain playlist */ PianoDestroyPlaylist (PianoListNextP (app->playlist)); app->playlist->head.next = NULL; - BarUiHistoryPrepend (app, app->playlist); - app->playlist = NULL; } } } From 06192d9c879cf70171139597b8f722a41fb88a37 Mon Sep 17 00:00:00 2001 From: blmpl Date: Sat, 15 Oct 2016 10:10:52 +0200 Subject: [PATCH 09/25] Support binding to a specific network interface Closes #597. # Conflicts: # src/ui.c --- contrib/config-example | 1 + contrib/pianobar.1 | 11 +++++++++++ src/settings.c | 3 +++ src/settings.h | 1 + 4 files changed, 16 insertions(+) diff --git a/contrib/config-example b/contrib/config-example index 899ee394..070314b3 100644 --- a/contrib/config-example +++ b/contrib/config-example @@ -10,6 +10,7 @@ # Proxy (for those who are not living in the USA) #control_proxy = http://127.0.0.1:9090/ +#bind_to = if!tun0 # Keybindings #act_help = ? diff --git a/contrib/pianobar.1 b/contrib/pianobar.1 index ee09c410..910af9a3 100644 --- a/contrib/pianobar.1 +++ b/contrib/pianobar.1 @@ -209,6 +209,17 @@ required to validate Pandora’s SSL certificate. Non-american users need a proxy to use pandora.com. Only the xmlrpc interface will use this proxy. The music is streamed directly. +.TP +.B bind_to = {if!tunX,host!x.x.x.x,..} +This sets the interface name to use as outgoing network interface. The name can +be an interface name, an IP address, or a host name. (from CURLOPT_INTERFACE) + +It can be used as a replacement for +.B control_proxy +in conjunction with OpenVPN's +option +.B route-nopull. + .TP .B decrypt_password = R=U!LH$O2B# diff --git a/src/settings.c b/src/settings.c index 6ba2e91d..36245ded 100644 --- a/src/settings.c +++ b/src/settings.c @@ -119,6 +119,7 @@ void BarSettingsInit (BarSettings_t *settings) { void BarSettingsDestroy (BarSettings_t *settings) { free (settings->controlProxy); free (settings->proxy); + free (settings->bindTo); free (settings->username); free (settings->password); free (settings->passwordCmd); @@ -236,6 +237,8 @@ void BarSettingsRead (BarSettings_t *settings) { settings->controlProxy = strdup (val); } else if (streq ("proxy", key)) { settings->proxy = strdup (val); + } else if (streq ("bind_to", key)) { + settings->bindTo = strdup (val); } else if (streq ("user", key)) { settings->username = strdup (val); } else if (streq ("password", key)) { diff --git a/src/settings.h b/src/settings.h index 78d0e457..e6067f3b 100644 --- a/src/settings.h +++ b/src/settings.h @@ -93,6 +93,7 @@ typedef struct { char *password, *passwordCmd; char *controlProxy; /* non-american listeners need this */ char *proxy; + char *bindTo; char *autostartStation; char *eventCmd; char *loveIcon; From 926764b76434c0919759ecd5b26cc4d3bc92712b Mon Sep 17 00:00:00 2001 From: Lars-Dominik Braun Date: Sat, 29 Oct 2016 15:57:20 +0200 Subject: [PATCH 10/25] Replace getline() with fgets() Mac OS X 10.6 compatibility, fixes #572. # Conflicts: # src/settings.c --- src/settings.c | 65 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/src/settings.c b/src/settings.c index 36245ded..b15053d3 100644 --- a/src/settings.c +++ b/src/settings.c @@ -27,11 +27,13 @@ THE SOFTWARE. #include "settings.h" #include "config.h" +#include "ui.h" #include "ui_dispatch.h" #include #include #include #include +#include #define PACKAGE_CONFIG PACKAGE ".cfg" #define PACKAGE_STATE PACKAGE ".state" @@ -214,8 +216,9 @@ void BarSettingsRead (BarSettings_t *settings) { /* read config files */ for (size_t j = 0; j < sizeof (configfiles) / sizeof (*configfiles); j++) { static const char *formatMsgPrefix = "format_msg_"; - char key[256], val[256]; FILE *configfd; + char line[512]; + size_t lineNum = 0; char * const path = BarGetXdgConfigDir (configfiles[j]); assert (path != NULL); @@ -225,14 +228,64 @@ void BarSettingsRead (BarSettings_t *settings) { } while (1) { - char lwhite, rwhite; - int scanRet = fscanf (configfd, "%255s%c=%c%255[^\n]", key, &lwhite, &rwhite, val); - if (scanRet == EOF) { + ++lineNum; + char * const ret = fgets (line, sizeof (line), configfd); + if (ret == NULL) { + /* EOF or error */ break; - } else if (scanRet != 4 || lwhite != ' ' || rwhite != ' ') { - /* invalid config line */ + } + if (strchr (line, '\n') == NULL && !feof (configfd)) { + BarUiMsg (settings, MSG_INFO, "Line %s:%zu too long, " + "ignoring\n", path, lineNum); + continue; + } + /* parse lines that match "^\s*(.*?)\s?=\s?(.*)$". Windows and Unix + * line terminators are supported. */ + char *key = line; + + /* skip leading spaces */ + while (isspace ((unsigned char) key[0])) { + ++key; + } + + /* skip comments */ + if (key[0] == '#') { continue; } + + /* search for delimiter and split key-value pair */ + char *val = strchr (line, '='); + if (val == NULL) { + /* no warning for empty lines */ + if (key[0] != '\0') { + BarUiMsg (settings, MSG_INFO, + "Invalid line at %s:%zu\n", path, lineNum); + } + /* invalid line */ + continue; + } + *val = '\0'; + ++val; + + /* drop spaces at the end */ + char *keyend = &key[strlen (key)-1]; + while (keyend >= key && isspace ((unsigned char) *keyend)) { + *keyend = '\0'; + --keyend; + } + + /* strip at most one space, legacy cruft, required for values with + * leading spaces like love_icon */ + if (isspace ((unsigned char) val[0])) { + ++val; + } + /* drop trailing cr/lf */ + char *valend = &val[strlen (val)-1]; + while (valend >= val && (*valend == '\r' || *valend == '\n')) { + *valend = '\0'; + --valend; + } + if (streq ("control_proxy", key)) { settings->controlProxy = strdup (val); } else if (streq ("proxy", key)) { From 8576aa5186a2cdd4f4bf44ec34c80e90d04de969 Mon Sep 17 00:00:00 2001 From: Lars-Dominik Braun Date: Fri, 2 Dec 2016 15:03:10 +0100 Subject: [PATCH 11/25] Exit when no email/password was entered or ^C was pressed Fixes #600. # Conflicts: # src/main.c --- src/main.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main.c b/src/main.c index 6e99ada8..3e6e85f3 100644 --- a/src/main.c +++ b/src/main.c @@ -64,7 +64,8 @@ static bool BarMainGetLoginCredentials(BarSettings_t *settings, char nameBuf[100]; BarUiMsg(settings, MSG_QUESTION, "Email: "); - BarReadlineStr(nameBuf, sizeof(nameBuf), rl, BAR_RL_DEFAULT); + if (BarReadlineStr(nameBuf, sizeof(nameBuf), rl, BAR_RL_DEFAULT) == 0) + return false; settings->username = strdup(nameBuf); usernameFromConfig = false; } @@ -78,18 +79,20 @@ static bool BarMainGetLoginCredentials(BarSettings_t *settings, BarUiMsg(settings, MSG_QUESTION, "Email: %s\n", settings->username); } - if (settings->passwordCmd == NULL) - { - BarUiMsg(settings, MSG_QUESTION, "Password: "); - BarReadlineStr(passBuf, sizeof(passBuf), rl, BAR_RL_NOECHO); + if (settings->passwordCmd == NULL) { + BarUiMsg (settings, MSG_QUESTION, "Password: "); + if (BarReadlineStr (passBuf, sizeof (passBuf), rl, BAR_RL_NOECHO) == 0) { + BarConsolePutc('\n'); + return false; + } /* write missing newline */ BarConsolePutc('\n'); - settings->password = strdup(passBuf); - } + settings->password = strdup (passBuf); + } else { - //pid_t chld; - //int pipeFd[2]; + //pid_t chld; + //int pipeFd[2]; //BarUiMsg (settings, MSG_INFO, "Requesting password from external helper... "); From f3ccc0628f3340e1f77eb38084ece2521db9d737 Mon Sep 17 00:00:00 2001 From: Felix Wong Date: Mon, 12 Dec 2016 01:29:06 -0800 Subject: [PATCH 12/25] add uninstall into makefile --- Makefile | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 00336603..f1dea034 100644 --- a/Makefile +++ b/Makefile @@ -154,4 +154,13 @@ install-libpiano: install -d ${DESTDIR}${INCDIR}/ install -m644 src/libpiano/piano.h ${DESTDIR}${INCDIR}/ -.PHONY: install install-libpiano test debug all +uninstall: + $(RM) ${DESTDIR}/${BINDIR}/pianobar \ + ${DESTDIR}/${MANDIR}/man1/pianobar.1 \ + ${DESTDIR}/${LIBDIR}/libpiano.so.0.0.0 \ + ${DESTDIR}/${LIBDIR}/libpiano.so.0 \ + ${DESTDIR}/${LIBDIR}/libpiano.so \ + ${DESTDIR}/${LIBDIR}/libpiano.a \ + ${DESTDIR}/${INCDIR}/piano.h + +.PHONY: install install-libpiano uninstall test debug all From c934c373e16acc7b7db6a374b1047649a0875dc3 Mon Sep 17 00:00:00 2001 From: Sean Greenslade Date: Tue, 14 Mar 2017 19:36:24 -0700 Subject: [PATCH 13/25] Added gain_mul setting to soften effect of replaygain. --- contrib/pianobar.1 | 6 ++++++ src/main.c | 3 ++- src/settings.c | 3 +++ src/settings.h | 1 + 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/contrib/pianobar.1 b/contrib/pianobar.1 index 910af9a3..0619735d 100644 --- a/contrib/pianobar.1 +++ b/contrib/pianobar.1 @@ -366,6 +366,12 @@ Your pandora.com username. .B volume = 0 Initial volume correction in dB. Usually between -30 and +5. +.TP +.B gain_mul = 1.0 +Pandora sends a ReplayGain value with every song. This sets a multiplier so that the gain adjustment can be +reduced. 0.0 means no gain adjustment, 1.0 means full gain adjustment, values inbetween reduce the magnitude +of gain adjustment. + .SH REMOTE CONTROL .B pianobar can be controlled through a fifo. You have to create it yourself by executing diff --git a/src/main.c b/src/main.c index 3e6e85f3..4f4214ab 100644 --- a/src/main.c +++ b/src/main.c @@ -31,6 +31,7 @@ THE SOFTWARE. #include "ui.h" #include "ui_dispatch.h" #include "ui_readline.h" +#include "settings.h" /* authenticate user */ @@ -238,7 +239,7 @@ static void BarMainStartPlayback(BarApp_t *app) } else { - BarPlayer2SetGain(app->player, curSong->fileGain); + BarPlayer2SetGain(app->player, curSong->fileGain * app->settings.gainMul); BarPlayer2Open(app->player, curSong->audioUrl); /* throw event */ diff --git a/src/settings.c b/src/settings.c index b15053d3..4f0ef8c9 100644 --- a/src/settings.c +++ b/src/settings.c @@ -172,6 +172,7 @@ void BarSettingsRead (BarSettings_t *settings) { settings->autoselect = true; settings->history = 5; settings->volume = 0; + settings->gainMul = 1.0; settings->maxPlayerErrors = 5; settings->sortOrder = BAR_SORT_NAME_AZ; settings->loveIcon = strdup (" <3"); @@ -378,6 +379,8 @@ void BarSettingsRead (BarSettings_t *settings) { settings->atIcon = strdup (val); } else if (streq ("volume", key)) { settings->volume = atoi (val); + } else if (streq ("gain_mul", key)) { + settings->gainMul = (float)atof (val); } else if (streq ("format_nowplaying_song", key)) { free (settings->npSongFormat); settings->npSongFormat = strdup (val); diff --git a/src/settings.h b/src/settings.h index e6067f3b..f8225ce0 100644 --- a/src/settings.h +++ b/src/settings.h @@ -87,6 +87,7 @@ typedef struct { bool autoselect; unsigned int history, maxPlayerErrors; int volume; + float gainMul; BarStationSorting_t sortOrder; PianoAudioQuality_t audioQuality; char *username; From e23931a2509850686150ce12845c70be1cf3f735 Mon Sep 17 00:00:00 2001 From: Lars-Dominik Braun Date: Fri, 17 Mar 2017 11:59:29 +0100 Subject: [PATCH 14/25] Sort manpage, remove non-existing tls_fingerprint --- contrib/config-example | 1 + contrib/pianobar.1 | 38 +++++++++++++++++--------------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/contrib/config-example b/contrib/config-example index 070314b3..0a695262 100644 --- a/contrib/config-example +++ b/contrib/config-example @@ -50,6 +50,7 @@ #ban_icon = [-] #volume = 0 #ca_bundle = /etc/ssl/certs/ca-certificates.crt +#gain_mul = 1.0 # Format strings #format_nowplaying_song = %t by %a on %l%r%@%s diff --git a/contrib/pianobar.1 b/contrib/pianobar.1 index 0619735d..bf385f6f 100644 --- a/contrib/pianobar.1 +++ b/contrib/pianobar.1 @@ -199,16 +199,6 @@ or the key you defined in .B ban_icon = Date: Sat, 25 Mar 2017 11:52:42 +0100 Subject: [PATCH 15/25] piano: Replace deprecated json_object_object_get # Conflicts: # src/libpiano/response.c --- src/libpiano/response.c | 179 +++++++++++++++++++++++----------------- 1 file changed, 105 insertions(+), 74 deletions(-) diff --git a/src/libpiano/response.c b/src/libpiano/response.c index 8b6ed526..e37824e1 100644 --- a/src/libpiano/response.c +++ b/src/libpiano/response.c @@ -1,5 +1,5 @@ /* -Copyright (c) 2008-2013 +Copyright (c) 2008-2017 Lars-Dominik Braun Permission is hereby granted, free of charge, to any person obtaining a copy @@ -34,16 +34,34 @@ THE SOFTWARE. #include "crypt.h" static char *PianoJsonStrdup (json_object *j, const char *key) { - return strdup (json_object_get_string (json_object_object_get (j, key))); + assert (j != NULL); + assert (key != NULL); + + json_object *v; + if (json_object_object_get_ex (j, key, &v)) { + return strdup (json_object_get_string (v)); + } else { + return NULL; + } +} + +static bool getBoolDefault (json_object * const j, const char * const key, const bool def) { + assert (j != NULL); + assert (key != NULL); + + json_object *v; + if (json_object_object_get_ex (j, key, &v)) { + return json_object_get_boolean (v); + } else { + return def; + } } static void PianoJsonParseStation (json_object *j, PianoStation_t *s) { s->name = PianoJsonStrdup (j, "stationName"); s->id = PianoJsonStrdup (j, "stationToken"); - s->isCreator = !json_object_get_boolean (json_object_object_get (j, - "isShared")); - s->isQuickMix = json_object_get_boolean (json_object_object_get (j, - "isQuickMix")); + s->isCreator = !getBoolDefault (j, "isShared", !false); + s->isQuickMix = getBoolDefault (j, "isQuickMix", false); } /* concat strings @@ -77,23 +95,22 @@ static void PianoStrpcat (char * restrict dest, const char * restrict src, */ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { PianoReturn_t ret = PIANO_RET_OK; - json_object *j, *result, *status; assert (ph != NULL); assert (req != NULL); - j = json_tokener_parse (req->responseData); + json_object * const j = json_tokener_parse (req->responseData); - status = json_object_object_get (j, "stat"); - if (status == NULL) { - json_object_put (j); - return PIANO_RET_INVALID_RESPONSE; + json_object *status; + if (!json_object_object_get_ex (j, "stat", &status)) { + ret = PIANO_RET_INVALID_RESPONSE; + goto cleanup; } /* error handling */ if (strcmp (json_object_get_string (status), "ok") != 0) { - json_object *code = json_object_object_get (j, "code"); - if (code == NULL) { + json_object *code; + if (!json_object_object_get_ex (j, "code", &code)) { ret = PIANO_RET_INVALID_RESPONSE; } else { ret = json_object_get_int (code)+PIANO_RET_OFFSET; @@ -110,11 +127,12 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { } } - json_object_put (j); - return ret; + goto cleanup; } - result = json_object_object_get (j, "result"); + json_object *result = NULL; + /* missing for some request types */ + json_object_object_get_ex (j, "result", &result); switch (req->type) { case PIANO_REQUEST_LOGIN: { @@ -127,8 +145,14 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { switch (reqData->step) { case 0: { /* decrypt timestamp */ - const char * const cryptedTimestamp = json_object_get_string ( - json_object_object_get (result, "syncTime")); + json_object *jsonTimestamp; + if (!json_object_object_get_ex (result, "syncTime", &jsonTimestamp)) { + ret = PIANO_RET_INVALID_RESPONSE; + break; + } + assert (jsonTimestamp != NULL); + const char * const cryptedTimestamp = json_object_get_string (jsonTimestamp); + assert (cryptedTimestamp != NULL); const time_t realTimestamp = time (NULL); char *decryptedTimestamp = NULL; size_t decryptedSize; @@ -148,8 +172,12 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { /* get auth token */ ph->partner.authToken = PianoJsonStrdup (result, "partnerAuthToken"); - ph->partner.id = json_object_get_int ( - json_object_object_get (result, "partnerId")); + json_object *partnerId; + if (!json_object_object_get_ex (result, "partnerId", &partnerId)) { + ret = PIANO_RET_INVALID_RESPONSE; + break; + } + ph->partner.id = json_object_get_int (partnerId); ++reqData->step; break; } @@ -172,8 +200,11 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { /* get stations */ assert (req->responseData != NULL); - json_object *stations = json_object_object_get (result, - "stations"), *mix = NULL; + json_object *stations, *mix = NULL; + + if (!json_object_object_get_ex (result, "stations", &stations)) { + break; + } for (int i = 0; i < json_object_array_length (stations); i++) { PianoStation_t *tmpStation; @@ -187,7 +218,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { if (tmpStation->isQuickMix) { /* fix flags on other stations later */ - mix = json_object_object_get (s, "quickMixStationIds"); + json_object_object_get_ex (s, "quickMixStationIds", &mix); } /* start new linked list or append */ @@ -219,7 +250,10 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { assert (reqData != NULL); assert (reqData->quality != PIANO_AQ_UNKNOWN); - json_object *items = json_object_object_get (result, "items"); + 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++) { @@ -230,7 +264,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { return PIANO_RET_OUT_OF_MEMORY; } - if (json_object_object_get (s, "artistName") == NULL) { + if (!json_object_object_get_ex (s, "artistName", NULL)) { free (song); continue; } @@ -240,15 +274,15 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { "highQuality"}; assert (reqData->quality < sizeof (qualityMap)/sizeof (*qualityMap)); static const char *formatMap[] = {"", "aacplus", "mp3"}; - json_object *map = json_object_object_get (s, "audioUrlMap"); - assert (map != NULL); - if (map != NULL) { - map = json_object_object_get (map, qualityMap[reqData->quality]); - - if (map != NULL) { - const char *encoding = json_object_get_string ( - json_object_object_get (map, "encoding")); + json_object *umap; + if (json_object_object_get_ex (s, "audioUrlMap", &umap)) { + assert (umap != NULL); + json_object *jsonEncoding, *qmap; + if (json_object_object_get_ex (umap, qualityMap[reqData->quality], &qmap) && + json_object_object_get_ex (qmap, "encoding", &jsonEncoding)) { + assert (qmap != 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) { @@ -256,7 +290,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { break; } } - song->audioUrl = PianoJsonStrdup (map, "audioUrl"); + song->audioUrl = PianoJsonStrdup (qmap, "audioUrl"); } else { /* requested quality is not available */ ret = PIANO_RET_QUALITY_UNAVAILABLE; @@ -266,6 +300,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { } } + json_object *v; song->artist = PianoJsonStrdup (s, "artistName"); song->album = PianoJsonStrdup (s, "albumName"); song->title = PianoJsonStrdup (s, "songName"); @@ -273,12 +308,12 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { song->stationId = PianoJsonStrdup (s, "stationId"); song->coverArt = PianoJsonStrdup (s, "albumArtUrl"); song->detailUrl = PianoJsonStrdup (s, "songDetailUrl"); - song->fileGain = (float)json_object_get_double ( - json_object_object_get (s, "trackGain")); - song->length = json_object_get_int ( - json_object_object_get (s, "trackLength")); - switch (json_object_get_int (json_object_object_get (s, - "songRating"))) { + song->fileGain = json_object_object_get_ex (s, "trackGain", &v) ? + (float)json_object_get_double (v) : 0.0f; + song->length = json_object_object_get_ex (s, "trackLength", &v) ? + json_object_get_int (v) : 0; + switch (json_object_object_get_ex (s, "songRating", &v) ? + json_object_get_int (v) : 0) { case 1: song->rating = PIANO_RATE_LOVE; break; @@ -340,8 +375,8 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { memset (searchResult, 0, sizeof (*searchResult)); /* get artists */ - json_object *artists = json_object_object_get (result, "artists"); - if (artists != NULL) { + json_object *artists; + if (json_object_object_get_ex (result, "artists", &artists)) { for (int i = 0; i < json_object_array_length (artists); i++) { json_object *a = json_object_array_get_idx (artists, i); PianoArtist_t *artist; @@ -359,8 +394,8 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { } /* get songs */ - json_object *songs = json_object_object_get (result, "songs"); - if (songs != NULL) { + json_object *songs; + if (json_object_object_get_ex (result, "songs", &songs)) { for (int i = 0; i < json_object_array_length (songs); i++) { json_object *s = json_object_array_get_idx (songs, i); PianoSong_t *song; @@ -414,8 +449,8 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { case PIANO_REQUEST_GET_GENRE_STATIONS: { /* get genre stations */ - json_object *categories = json_object_object_get (result, "categories"); - if (categories != NULL) { + json_object *categories; + if (json_object_object_get_ex (result, "categories", &categories)) { for (int i = 0; i < json_object_array_length (categories); i++) { json_object *c = json_object_array_get_idx (categories, i); PianoGenreCategory_t *tmpGenreCategory; @@ -429,9 +464,8 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { "categoryName"); /* get genre subnodes */ - json_object *stations = json_object_object_get (c, - "stations"); - if (stations != NULL) { + json_object *stations; + if (json_object_object_get_ex (c, "stations", &stations)) { for (int k = 0; k < json_object_array_length (stations); k++) { json_object *s = @@ -480,9 +514,8 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { assert (reqData != NULL); - json_object *explanations = json_object_object_get (result, - "explanations"); - if (explanations != NULL) { + json_object *explanations; + if (json_object_object_get_ex (result, "explanations", &explanations)) { reqData->retExplain = malloc (strSize * sizeof (*reqData->retExplain)); strncpy (reqData->retExplain, "We're playing this track " @@ -490,9 +523,11 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { for (int i = 0; i < json_object_array_length (explanations); i++) { json_object *e = json_object_array_get_idx (explanations, i); - const char *s = json_object_get_string ( - json_object_object_get (e, "focusTraitName")); - + json_object *f; + if (!json_object_object_get_ex (e, "focusTraitName", &f)) { + continue; + } + const char *s = json_object_get_string (f); PianoStrpcat (reqData->retExplain, s, strSize); if (i < json_object_array_length (explanations)-2) { PianoStrpcat (reqData->retExplain, ", ", strSize); @@ -511,8 +546,8 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { assert (settings != NULL); - settings->explicitContentFilter = json_object_get_boolean ( - json_object_object_get (result, "isExplicitContentFilterEnabled")); + settings->explicitContentFilter = getBoolDefault (result, + "isExplicitContentFilterEnabled", false); settings->username = PianoJsonStrdup (result, "username"); break; } @@ -528,11 +563,11 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { assert (info != NULL); /* parse music seeds */ - json_object *music = json_object_object_get (result, "music"); - if (music != NULL) { + json_object *music; + if (json_object_object_get_ex (result, "music", &music)) { /* songs */ - json_object *songs = json_object_object_get (music, "songs"); - if (songs != NULL) { + json_object *songs; + if (json_object_object_get_ex (music, "songs", &songs)) { for (int i = 0; i < json_object_array_length (songs); i++) { json_object *s = json_object_array_get_idx (songs, i); PianoSong_t *seedSong; @@ -552,9 +587,8 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { } /* artists */ - json_object *artists = json_object_object_get (music, - "artists"); - if (artists != NULL) { + json_object *artists; + if (json_object_object_get_ex (music, "artists", &artists)) { for (int i = 0; i < json_object_array_length (artists); i++) { json_object *a = json_object_array_get_idx (artists, i); PianoArtist_t *seedArtist; @@ -574,14 +608,12 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { } /* parse feedback */ - json_object *feedback = json_object_object_get (result, - "feedback"); - if (feedback != NULL) { + json_object *feedback; + if (json_object_object_get_ex (result, "feedback", &feedback)) { static const char * const keys[] = {"thumbsUp", "thumbsDown"}; for (size_t i = 0; i < sizeof (keys)/sizeof (*keys); i++) { - json_object * const val = json_object_object_get (feedback, - keys[i]); - if (val == NULL) { + json_object *val; + if (!json_object_object_get_ex (feedback, keys[i], &val)) { continue; } assert (json_object_is_type (val, json_type_array)); @@ -599,9 +631,8 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { "artistName"); feedbackSong->feedbackId = PianoJsonStrdup (s, "feedbackId"); - feedbackSong->rating = json_object_get_boolean ( - json_object_object_get (s, "isPositive")) ? - PIANO_RATE_LOVE : PIANO_RATE_BAN; + feedbackSong->rating = getBoolDefault (s, "isPositive", + false) ? PIANO_RATE_LOVE : PIANO_RATE_BAN; info->feedback = PianoListAppendP (info->feedback, feedbackSong); From 21b167532b07f631b107968837dcfdf93025239b Mon Sep 17 00:00:00 2001 From: Lars-Dominik Braun Date: Sat, 25 Mar 2017 12:30:33 +0100 Subject: [PATCH 16/25] Switch to #pragma once # Conflicts: # src/http/http.h # src/player.h --- src/config.h | 4 +--- src/console.h | 4 +--- src/http/http.h | 5 +---- src/libpiano/crypt.h | 4 +--- src/libpiano/piano.h | 4 +--- src/libpiano/piano_private.h | 4 +--- src/main.h | 5 +---- src/player/backends/media_foundation.h | 7 +------ src/player/backends/utility/com_ptr.h | 4 ---- src/player/backends/utility/optional.h | 5 ----- src/player/player2.h | 4 ---- src/player/player2_private.h | 3 --- src/settings.h | 6 +++--- src/ui.h | 4 +--- src/ui_act.h | 4 +--- src/ui_dispatch.h | 5 +---- src/ui_readline.h | 5 +---- src/ui_types.h | 4 +--- 18 files changed, 16 insertions(+), 65 deletions(-) diff --git a/src/config.h b/src/config.h index c4085428..8679001a 100644 --- a/src/config.h +++ b/src/config.h @@ -1,5 +1,4 @@ -#ifndef SRC_CONFIG_H_S6A1C09K -#define SRC_CONFIG_H_S6A1C09K +# pragma once /* package name */ #define PACKAGE "pianobar" @@ -25,4 +24,3 @@ #define CURL_STATICLIB -#endif /* SRC_CONFIG_H_S6A1C09K */ diff --git a/src/console.h b/src/console.h index de5d951f..627e77b2 100644 --- a/src/console.h +++ b/src/console.h @@ -21,8 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef SRC_CONSOLE_H_WY8F3MNH -#define SRC_CONSOLE_H_WY8F3MNH +# pragma once #include "config.h" #include @@ -49,4 +48,3 @@ void BarConsolePuts(const char* c); void BarConsolePrint(const char* format, ...); void BarConsolePrintV(const char* format, va_list args); -#endif /* SRC_CONSOLE_H_WY8F3MNH */ diff --git a/src/http/http.h b/src/http/http.h index a321cd3f..5ea617f8 100644 --- a/src/http/http.h +++ b/src/http/http.h @@ -21,8 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef SRC_HTTP_H_CN979RE9 -#define SRC_HTTP_H_CN979RE9 +#pragma once #include "config.h" @@ -42,5 +41,3 @@ bool HttpSetProxy(http_t, const char*); bool HttpRequest (http_t, PianoRequest_t * const); const char* HttpGetError (http_t); -#endif /* SRC_HTTP_H_CN979RE9 */ - diff --git a/src/libpiano/crypt.h b/src/libpiano/crypt.h index 890b8c1d..30d2294c 100644 --- a/src/libpiano/crypt.h +++ b/src/libpiano/crypt.h @@ -21,8 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef SRC_LIBPIANO_CRYPT_H_O832IVGK -#define SRC_LIBPIANO_CRYPT_H_O832IVGK +#pragma once #include "piano.h" @@ -34,4 +33,3 @@ char *PianoDecryptString (PianoCipher_t, const char * const, size_t * const); char *PianoEncryptString (PianoCipher_t, const char *); -#endif /* SRC_LIBPIANO_CRYPT_H_O832IVGK */ diff --git a/src/libpiano/piano.h b/src/libpiano/piano.h index ca33626f..2dbf6079 100644 --- a/src/libpiano/piano.h +++ b/src/libpiano/piano.h @@ -21,8 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef SRC_LIBPIANO_PIANO_H_MFBT13PN -#define SRC_LIBPIANO_PIANO_H_MFBT13PN +#pragma once #include "../config.h" @@ -366,4 +365,3 @@ PianoStation_t *PianoFindStationById (PianoStation_t * const, const char * const); const char *PianoErrorToStr (PianoReturn_t); -#endif /* SRC_LIBPIANO_PIANO_H_MFBT13PN */ diff --git a/src/libpiano/piano_private.h b/src/libpiano/piano_private.h index bc0cc0ee..ffc14c82 100644 --- a/src/libpiano/piano_private.h +++ b/src/libpiano/piano_private.h @@ -21,12 +21,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef SRC_LIBPIANO_PIANO_PRIVATE_H_C35CDGGL -#define SRC_LIBPIANO_PIANO_PRIVATE_H_C35CDGGL +#pragma once #include "piano.h" void PianoDestroyStation (PianoStation_t *station); void PianoDestroyUserInfo (PianoUserInfo_t *user); -#endif /* SRC_LIBPIANO_PIANO_PRIVATE_H_C35CDGGL */ diff --git a/src/main.h b/src/main.h index e5e988dd..192ffdb2 100644 --- a/src/main.h +++ b/src/main.h @@ -21,8 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef SRC_MAIN_H_4ZGSCG6X -#define SRC_MAIN_H_4ZGSCG6X +# pragma once //#include @@ -50,5 +49,3 @@ typedef struct { unsigned int playerErrors; } BarApp_t; -#endif /* SRC_MAIN_H_4ZGSCG6X */ - diff --git a/src/player/backends/media_foundation.h b/src/player/backends/media_foundation.h index cf10122f..6ad4966e 100644 --- a/src/player/backends/media_foundation.h +++ b/src/player/backends/media_foundation.h @@ -1,5 +1,3 @@ -# ifndef __TD__BASIC_MEDIA_PLAYER_H__ -# define __TD__BASIC_MEDIA_PLAYER_H__ # pragma once # include @@ -98,7 +96,7 @@ class MediaPlayer: com_ptr m_SimpleAudioVolume; com_ptr m_PresentationClock; com_ptr m_StreamAudioVolume; - + optional m_SetMasterVolume; mutable float m_MasterVolume; mutable float m_ReplayGain; @@ -110,6 +108,3 @@ class MediaPlayer: State m_State; HANDLE m_CloseEvent; }; - - -# endif // __TD__BASIC_MEDIA_PLAYER_H__ \ No newline at end of file diff --git a/src/player/backends/utility/com_ptr.h b/src/player/backends/utility/com_ptr.h index 23534b2b..519caefe 100644 --- a/src/player/backends/utility/com_ptr.h +++ b/src/player/backends/utility/com_ptr.h @@ -1,5 +1,3 @@ -# ifndef __TD__COM_PTR_H__ -# define __TD__COM_PTR_H__ # pragma once template @@ -144,5 +142,3 @@ template inline bool operator<=(const com_ptr& a, const com_ptr< template inline bool operator>(const com_ptr& a, const com_ptr& b) { return std::greater()(a.get(), b.get()); } template inline bool operator>=(const com_ptr& a, const com_ptr& b) { return std::greater_equal()(a.get(), b.get()); } template void swap(com_ptr & lhs, com_ptr & rhs) { lhs.swap(rhs); } - -# endif // __TD__COM_PTR_H__ \ No newline at end of file diff --git a/src/player/backends/utility/optional.h b/src/player/backends/utility/optional.h index 9ee0b71b..dcdc59a4 100644 --- a/src/player/backends/utility/optional.h +++ b/src/player/backends/utility/optional.h @@ -1,8 +1,6 @@ //------------------------------------------------------------------------------ // //------------------------------------------------------------------------------ -# ifndef __TD__OPTIONAL_H__ -# define __TD__OPTIONAL_H__ # pragma once # include @@ -512,6 +510,3 @@ inline bool operator>=(const T& v, const optional& opt) using namespace std; return static_cast(opt) ? greater_equal(v, *opt) : true; } - - -# endif // __TD__OPTIONAL_H__ diff --git a/src/player/player2.h b/src/player/player2.h index 710b35d7..3ac29c21 100644 --- a/src/player/player2.h +++ b/src/player/player2.h @@ -21,8 +21,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef __PIANOBAR_PLAYER2_H__ -#define __PIANOBAR_PLAYER2_H__ #pragma once #include "config.h" @@ -49,5 +47,3 @@ bool BarPlayer2IsPaused(player2_t player); bool BarPlayer2IsStopped(player2_t player); bool BarPlayer2IsFinished(player2_t player); -#endif /* __PIANOBAR_PLAYER2_H__ */ - diff --git a/src/player/player2_private.h b/src/player/player2_private.h index 757a07e4..b76eb8b7 100644 --- a/src/player/player2_private.h +++ b/src/player/player2_private.h @@ -1,5 +1,3 @@ -#ifndef __PIANOBAR_PLAYER2_PRIVATE_H__ -#define __PIANOBAR_PLAYER2_PRIVATE_H__ #pragma once #include "config.h" @@ -32,4 +30,3 @@ typedef struct _player2_iface extern player2_iface player2_direct_show; extern player2_iface player2_windows_media_foundation; -#endif /* __PIANOBAR_PLAYER2_PRIVATE_H__ */ diff --git a/src/settings.h b/src/settings.h index f8225ce0..0edb544f 100644 --- a/src/settings.h +++ b/src/settings.h @@ -21,8 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef SRC_SETTINGS_H_IPL0ON9L -#define SRC_SETTINGS_H_IPL0ON9L +#pragma once #include @@ -83,6 +82,8 @@ typedef struct { char *postfix; } BarMsgFormatStr_t; +#include "ui_types.h" + typedef struct { bool autoselect; unsigned int history, maxPlayerErrors; @@ -116,4 +117,3 @@ void BarSettingsDestroy (BarSettings_t *); void BarSettingsRead (BarSettings_t *); void BarSettingsWrite (PianoStation_t *, BarSettings_t *); -#endif /* SRC_SETTINGS_H_IPL0ON9L */ diff --git a/src/ui.h b/src/ui.h index 705bbab1..126f6bb6 100644 --- a/src/ui.h +++ b/src/ui.h @@ -21,8 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef SRC_UI_H_46P20TS0 -#define SRC_UI_H_46P20TS0 +#pragma once #include @@ -54,4 +53,3 @@ int BarUiPianoCall (BarApp_t * const, PianoRequestType_t, void *, PianoReturn_t *); void BarUiHistoryPrepend (BarApp_t *app, PianoSong_t *song); -#endif /* SRC_UI_H_46P20TS0 */ diff --git a/src/ui_act.h b/src/ui_act.h index 676fbb40..fb4457b0 100644 --- a/src/ui_act.h +++ b/src/ui_act.h @@ -21,8 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef SRC_UI_ACT_H_1FEFTC06 -#define SRC_UI_ACT_H_1FEFTC06 +#pragma once #include @@ -63,4 +62,3 @@ BarUiActCallback(BarUiActManageStation); BarUiActCallback(BarUiActVolReset); BarUiActCallback(BarUiActSettings); -#endif /* SRC_UI_ACT_H_1FEFTC06 */ diff --git a/src/ui_dispatch.h b/src/ui_dispatch.h index 24b68bb3..7e34393c 100644 --- a/src/ui_dispatch.h +++ b/src/ui_dispatch.h @@ -21,8 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef SRC_UI_DISPATCH_H_PV2JON1Z -#define SRC_UI_DISPATCH_H_PV2JON1Z +#pragma once /* bit-mask */ typedef enum { @@ -114,5 +113,3 @@ static const BarUiDispatchAction_t dispatchActions[BAR_KS_COUNT] = { BarKeyShortcutId_t BarUiDispatch (BarApp_t *, const char, PianoStation_t *, PianoSong_t *, const bool, BarUiDispatchContext_t); -#endif /* SRC_UI_DISPATCH_H_PV2JON1Z */ - diff --git a/src/ui_readline.h b/src/ui_readline.h index 9343e1a8..83214b94 100644 --- a/src/ui_readline.h +++ b/src/ui_readline.h @@ -21,8 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef SRC_UI_READLINE_H_IFRX74VM -#define SRC_UI_READLINE_H_IFRX74VM +#pragma once #include "config.h" @@ -46,5 +45,3 @@ size_t BarReadlineStr (char *, const size_t, size_t BarReadlineInt (int *, BarReadline_t); bool BarReadlineYesNo (bool, BarReadline_t); -#endif /* SRC_UI_READLINE_H_IFRX74VM */ - diff --git a/src/ui_types.h b/src/ui_types.h index aab4199e..ecd32d4b 100644 --- a/src/ui_types.h +++ b/src/ui_types.h @@ -21,8 +21,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef SRC_UI_TYPES_H_2HR75RII -#define SRC_UI_TYPES_H_2HR75RII +# pragma once typedef enum { MSG_NONE = 0, @@ -36,4 +35,3 @@ typedef enum { MSG_COUNT = 8, /* invalid type */ } BarUiMsg_t; -#endif /* SRC_UI_TYPES_H_2HR75RII */ From b732d8c429ff77f825cb5de93e139672effd2619 Mon Sep 17 00:00:00 2001 From: Lars-Dominik Braun Date: Wed, 26 Apr 2017 14:47:25 +0200 Subject: [PATCH 17/25] Fix use-after-free when deleting station Eventcmd uses both, selStation and selSong. Fixes #617. --- src/ui_act.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ui_act.c b/src/ui_act.c index cb1d7a8d..a036f198 100644 --- a/src/ui_act.c +++ b/src/ui_act.c @@ -228,11 +228,13 @@ BarUiActCallback(BarUiActDeleteStation) { /* drain playlist */ PianoDestroyPlaylist (PianoListNextP (app->playlist)); app->playlist->head.next = NULL; + selSong = NULL; } app->nextStation = NULL; /* XXX: usually we shoudn’t touch cur*, but DELETE_STATION destroys * station struct */ app->curStation = NULL; + selStation = NULL; } BarUiActDefaultEventcmd ("stationdelete"); } From 573613e834a0538a3ce8b950cbad7cd11e180723 Mon Sep 17 00:00:00 2001 From: Lars-Dominik Braun Date: Wed, 26 Apr 2017 14:49:41 +0200 Subject: [PATCH 18/25] contrib: Use $XDG_HOME_CONFIG Fixes #618. --- contrib/eventcmd-examples/eventcmd.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/eventcmd-examples/eventcmd.sh b/contrib/eventcmd-examples/eventcmd.sh index 7e2872a3..f07cb7d7 100755 --- a/contrib/eventcmd-examples/eventcmd.sh +++ b/contrib/eventcmd-examples/eventcmd.sh @@ -11,7 +11,7 @@ case "$1" in # songstart) # echo 'naughty.notify({title = "pianobar", text = "Now playing: ' "$title" ' by ' "$artist" '"})' | awesome-client - -# echo "$title -- $artist" > $HOME/.config/pianobar/nowplaying +# echo "$title -- $artist" > ${XDG_HOME_CONFIG:-${HOME}/.config}/pianobar/nowplaying # if [ "$rating" -eq 1 ] # then From aa2d42f83607d7eae63cc7d860b919e60e1defd8 Mon Sep 17 00:00:00 2001 From: Richard Hartmann Date: Wed, 26 Apr 2017 15:00:57 +0200 Subject: [PATCH 19/25] eventcmd.sh: Guard against spaces in path --- contrib/eventcmd-examples/eventcmd.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/eventcmd-examples/eventcmd.sh b/contrib/eventcmd-examples/eventcmd.sh index f07cb7d7..233b52ef 100755 --- a/contrib/eventcmd-examples/eventcmd.sh +++ b/contrib/eventcmd-examples/eventcmd.sh @@ -11,7 +11,7 @@ case "$1" in # songstart) # echo 'naughty.notify({title = "pianobar", text = "Now playing: ' "$title" ' by ' "$artist" '"})' | awesome-client - -# echo "$title -- $artist" > ${XDG_HOME_CONFIG:-${HOME}/.config}/pianobar/nowplaying +# echo "$title -- $artist" > "${XDG_HOME_CONFIG:-${HOME}/.config}/pianobar/nowplaying" # if [ "$rating" -eq 1 ] # then From e1eed89e03c0aadbd6c7781ce680768a16be7c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cicho=C5=84?= Date: Thu, 18 May 2017 18:16:20 +0200 Subject: [PATCH 20/25] Handle escaped authority url components. pianobar-windows-binaries/#3 Authority username and password now can be escaped to handle special characters. --- src/http/http.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/http/http.c b/src/http/http.c index 609317f4..4576d04d 100644 --- a/src/http/http.c +++ b/src/http/http.c @@ -250,6 +250,32 @@ bool HttpSetAutoProxy (http_t http, const char* url) { return false; } +static void HttpUrlDecodeInplace (wchar_t* url) +{ + wchar_t* input = url; + wchar_t* output = url; + size_t size = wcslen (url); + while (size > 0) { + if (input[0] == '%' && iswxdigit(input[1]) && iswxdigit(input[2])) { + wchar_t hex[3]; + hex[0] = input[1]; + hex[1] = input[2]; + hex[2] = 0; + *output++ = (wchar_t)(wcstol (hex, NULL, 16)); + input += 3; + size -= 3; + } + else { + *output++ = *input++; + --size; + } + } + + if (output < input) { + *output = '\0'; + } +} + bool HttpSetProxy (http_t http, const char* url) { URL_COMPONENTS urlComponents; wchar_t* wideUrl = NULL; @@ -267,10 +293,12 @@ bool HttpSetProxy (http_t http, const char* url) { if (urlComponents.lpszUserName && urlComponents.dwUserNameLength > 0) { wideUsername = wcsdup(urlComponents.lpszUserName); wideUsername[urlComponents.dwUserNameLength] = 0; + HttpUrlDecodeInplace (wideUsername); } if (urlComponents.lpszPassword && urlComponents.dwPasswordLength > 0) { widePassword = wcsdup(urlComponents.lpszPassword); widePassword[urlComponents.dwPasswordLength] = 0; + HttpUrlDecodeInplace (widePassword); } } From a05da504f94ca481e9e6f88c38185cbd18b3efd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cicho=C5=84?= Date: Wed, 17 May 2017 00:22:27 +0200 Subject: [PATCH 21/25] Add release content. --- pianobar.cfg.example | 31 +++++++++++++++++++++++++++++++ release/README.md | 34 ++++++++++++++++++++++++++++++++++ release/pianobar.cfg.example | 31 +++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 pianobar.cfg.example create mode 100644 release/README.md create mode 100644 release/pianobar.cfg.example diff --git a/pianobar.cfg.example b/pianobar.cfg.example new file mode 100644 index 00000000..ed86e88b --- /dev/null +++ b/pianobar.cfg.example @@ -0,0 +1,31 @@ +# Uncomment the control_proxy line if you would like to set up a proxy connection for pianobar +#control_proxy = http://:@: + +user = +password = + +#width = 220 +#height = 60 + +#------------------------------------------------------------------------------- +# Uncomment these if you're using GlobalPandora.com + +#rpc_host = internal-tuner.pandora.com +#partner_user = pandora one +#partner_password = TVCKIBGS9AO9TSYLNNFUML0743LH82D +#device = D01 +#encrypt_password = 2%3WCL*JU$MP]4 +#decrypt_password = U#IO$RZPAB%VX2 +#tls_fingerprint = B0A1EB460B1B6F33A1B6CB500C6523CB2E6EC946 + + +# Messages with colors using terminal escape codes +format_nowplaying_song = "%t" by "%a" on "%l"%r%@%s +format_nowplaying_station = Station "%n" (%i) +format_list_song = %i) %a - %t%r +format_msg_info = (i) %s +format_msg_nowplaying = |> %s +format_msg_time = # %s +format_msg_err = /!\ %s +format_msg_question = [?] %s +format_msg_debug = %s \ No newline at end of file diff --git a/release/README.md b/release/README.md new file mode 100644 index 00000000..f98315b9 --- /dev/null +++ b/release/README.md @@ -0,0 +1,34 @@ +pianobar for Windows - portable binaries +======== + +![pianobar](https://github.com/thedmd/pianobar-windows-binaries/blob/master/screenshots/pianobar.png) + +pianobar is a console client for the personalized web radio pandora +(http://www.pandora.com). Source code of the original project can be found at +at http://github.com/PromyLOPh/pianobar/ or http://6xq.net/projects/pianobar/ + +This project contains binaries for Windows build using Microsoft +Visual Studio 2015. + +json-c, vtparse are used to prepare this distributtion. + +Source code of this binary can be found at: +https://github.com/thedmd/pianobar-windows-build + + +CONFIGURATION + +Pianobar use configuration file. Under Windows this file is named pianobar.cfg +and should be placed next to pianobar.exe. +On reporitory there is an example configuration file, you may copy or rename it. +Then edit it and fill marked fields relevant to you and remove remaining. + +Note that non-US users have to have configuration file with control proxy +details set. + + +GLOBALPANDORA.COM + +If you're behind proxy please look into pianobar.cfg.example and copy necessary +settings to your own configuration file. This are necessary, because +globalpandora.com does not support server used in by pianobar. diff --git a/release/pianobar.cfg.example b/release/pianobar.cfg.example new file mode 100644 index 00000000..ed86e88b --- /dev/null +++ b/release/pianobar.cfg.example @@ -0,0 +1,31 @@ +# Uncomment the control_proxy line if you would like to set up a proxy connection for pianobar +#control_proxy = http://:@: + +user = +password = + +#width = 220 +#height = 60 + +#------------------------------------------------------------------------------- +# Uncomment these if you're using GlobalPandora.com + +#rpc_host = internal-tuner.pandora.com +#partner_user = pandora one +#partner_password = TVCKIBGS9AO9TSYLNNFUML0743LH82D +#device = D01 +#encrypt_password = 2%3WCL*JU$MP]4 +#decrypt_password = U#IO$RZPAB%VX2 +#tls_fingerprint = B0A1EB460B1B6F33A1B6CB500C6523CB2E6EC946 + + +# Messages with colors using terminal escape codes +format_nowplaying_song = "%t" by "%a" on "%l"%r%@%s +format_nowplaying_station = Station "%n" (%i) +format_list_song = %i) %a - %t%r +format_msg_info = (i) %s +format_msg_nowplaying = |> %s +format_msg_time = # %s +format_msg_err = /!\ %s +format_msg_question = [?] %s +format_msg_debug = %s \ No newline at end of file From ff34cb46e2002978fa6a41eefaca12484fe6de12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cicho=C5=84?= Date: Wed, 17 May 2017 00:22:51 +0200 Subject: [PATCH 22/25] Add screenshot. --- screenshots/pianobar.png | Bin 0 -> 46647 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 screenshots/pianobar.png diff --git a/screenshots/pianobar.png b/screenshots/pianobar.png new file mode 100644 index 0000000000000000000000000000000000000000..61096ab602574cf6a963fb686c76e4e071343bad GIT binary patch literal 46647 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 z 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 literal 0 HcmV?d00001 From 6615f72c66fba59ffe8b924189d0febab0697923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cicho=C5=84?= Date: Wed, 17 May 2017 01:52:12 +0200 Subject: [PATCH 23/25] Add AppVeyor configuration. --- appveyor.yml | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..c4f645a9 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,79 @@ +version: '{branch}-{build}' +configuration: Release +platform: +- x86 +- x64 +clone_script: +- ps: >- + # Clone build environment + + git clone -q --branch=master https://github.com/thedmd/pianobar-windows-build.git $env:appveyor_build_folder + + + # Clone project itself + + if(-not $env:appveyor_pull_request_number) { + git clone -q --branch=$env:appveyor_repo_branch https://github.com/$env:appveyor_repo_name.git $env:appveyor_build_folder\pianobar\src + cd $env:appveyor_build_folder\pianobar\src; git checkout -qf $env:appveyor_repo_commit + } else { + git clone -q https://github.com/$env:appveyor_repo_name.git $env:appveyor_build_folder\pianobar\src + cd $env:appveyor_build_folder\pianobar\src; git fetch -q origin +refs/pull/$env:appveyor_pull_request_number/merge: + cd $env:appveyor_build_folder\pianobar\src; git checkout -qf FETCH_HEAD + } +build: + verbosity: minimal +after_build: +- ps: >- + $artifactName = "pianobar" + + if([System.Convert]::ToBoolean($env:appveyor_repo_tag)) + + { + $artifactName = "$artifactName-$env:appveyor_repo_tag_name" + } + + else + + { + $branchName = $env:appveyor_build_version -replace "/", "-" + $artifactName = "$artifactName-$branchName" + } + + + function Package + + { + [cmdletbinding()] + Param([string]$BinaryDir, [string]$ArtifactName, [string]$OutputDir, [string]$Suffix) + Process + { + New-Item -ItemType directory $OutputDir\release-$Suffix + Copy-Item $env:appveyor_build_folder\pianobar\src\release\* $OutputDir\release-$Suffix + Copy-Item $BinaryDir\*.exe $OutputDir\release-$Suffix + 7z a $OutputDir\$ArtifactName-$Suffix.zip $OutputDir\release-$Suffix\* + } + } + + + if(Test-Path -Path $env:appveyor_build_folder\build\Win32) + + { + Package -BinaryDir $env:appveyor_build_folder\build\Win32 -ArtifactName $artifactName -Suffix x86 -OutputDir $env:appveyor_build_folder\build + } + + + if(Test-Path -Path $env:appveyor_build_folder\build\x64) + + { + Package -BinaryDir $env:appveyor_build_folder\build\x64 -ArtifactName $artifactName -Suffix x64 -OutputDir $env:appveyor_build_folder\build + } +artifacts: +- path: build\*.zip +deploy: +- provider: GitHub + auth_token: + secure: bXlXe4mzmi9lpGSfWMvWf01I05hyCuYZAVrlM5ZUad86QfyYvO6EeKTPaCpbjdyp + draft: true + force_update: true + on: + branch: /\d\d\d\d\.\d\d.\d\d.*/ \ No newline at end of file From 7b7c9efa8f29fe5667c3e630ccc9599ca27d1371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cicho=C5=84?= Date: Wed, 17 May 2017 02:33:53 +0200 Subject: [PATCH 24/25] Update README.md --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 16aee6c9..20b2638b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ -#pianobar - pianobar is a console client for the personalized web radio [Pandora] -([http://www.pandora.com](http://www.pandora.com)). +([http://www.pandora.com](http://www.pandora.com)) ported to Windows. + +![pianobar](https://github.com/thedmd/pianobar-windows/blob/feature/appveyor/screenshots/pianobar.png) + +# Releases + +Releases can be found at [GitHub Release page](https://github.com/thedmd/pianobar-windows/releases). + +[![Build status](https://ci.appveyor.com/api/projects/status/6n5qa9bs7aiy8e52?svg=true)](https://ci.appveyor.com/project/thedmd/pianobar-windows) -###Features +### Features * Play and manage (create, add more music, delete, rename, ...) your stations. * Rate played songs and let pandora explain why they have been selected. @@ -12,18 +18,14 @@ pianobar is a console client for the personalized web radio [Pandora] * last.fm scrobbling support (external application) * Proxy support for listeners outside the USA. -###Binary - -Prebuild binary is available at [pianobar-windows-binaries](https://github.com/thedmd/pianobar-windows-binaries) repository. - -###Source Code - -Windows port source code is available at this repository ([pianobar-windows](https://github.com/thedmd/pianobar-windows)). +### Source Code Original source code can be downloaded at [github.com](http://github.com/PromyLOPh/pianobar/) or [6xq.net](http://6xq.net/projects/pianobar/). -###Building +### Building Checkout [pianobar-windows-build](https://github.com/thedmd/pianobar-windows-build) where you will find configured solution for Visual Studio 2015. + +This repository is linked by GitHub submodule. From 593b2d6f18eb21309b82f7cad4a56f7bb1218180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cicho=C5=84?= Date: Thu, 18 May 2017 18:25:05 +0200 Subject: [PATCH 25/25] Bump version to 2017.05.18 --- src/config.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.h b/src/config.h index 8679001a..42a0b112 100644 --- a/src/config.h +++ b/src/config.h @@ -3,7 +3,7 @@ /* package name */ #define PACKAGE "pianobar" -#define VERSION "2015.12.10" +#define VERSION "2017.05.18" #define TITLE "Pianobar"