diff options
-rw-r--r-- | .github/workflows/build.yml | 10 | ||||
-rw-r--r-- | ChangeLog | 7 | ||||
-rw-r--r-- | INSTALL | 4 | ||||
-rw-r--r-- | README.rst | 14 | ||||
-rw-r--r-- | contrib/config-example | 1 | ||||
-rw-r--r-- | contrib/pianobar.1 | 14 | ||||
-rw-r--r-- | src/config.h | 2 | ||||
-rw-r--r-- | src/libpiano/response.c | 27 | ||||
-rw-r--r-- | src/player.c | 25 | ||||
-rw-r--r-- | src/player.h | 1 | ||||
-rw-r--r-- | src/ui.c | 107 | ||||
-rw-r--r-- | src/ui_act.c | 28 |
12 files changed, 171 insertions, 69 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b61ea6..536a7d9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,12 +9,14 @@ on: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: deps - run: sudo apt install libao-dev libavcodec-dev libavfilter-dev libavformat-dev libavutil-dev libcurl4-gnutls-dev libgcrypt20-dev libjson-c-dev libpth-dev pkg-config build-essential - - name: make + - name: Install dependencies + run: | + sudo apt-get update + sudo apt install libao-dev libavcodec-dev libavfilter-dev libavformat-dev libavutil-dev libcurl4-gnutls-dev libgcrypt20-dev libjson-c-dev libpth-dev pkg-config build-essential + - name: Build pianobar run: make @@ -1,3 +1,10 @@ +Release 2022.04.01 + +- Not a joke +- Fix compilation with ffmpeg 5.0 and replace deprecated function +- Improved retry handling with unreliable HTTP proxies +- Minor UI improvements + Release 2020.11.28 - Support changing station modes @@ -7,10 +7,10 @@ Dependencies - gmake - pthreads - libao -- libcurl +- libcurl>=7.32.0 - gcrypt[1] - json-c -- ffmpeg>=3.3 [2] +- ffmpeg>=5.1 [2] - UTF-8 console/locale [1] with blowfish cipher enabled @@ -16,11 +16,12 @@ Features - play and manage (create, add more music, delete, rename, ...) stations - rate songs and explain why they have been selected - upcoming songs/song history -- customize keybindings and text output +- customize keybindings and text output (see `configuration example`_) - remote control and eventcmd interface (send tracks to last.fm_, for example) - proxy support for listeners outside the USA .. _last.fm: https://www.last.fm +.. _configuration example: https://github.com/PromyLOPh/pianobar/blob/master/contrib/config-example Download -------- @@ -32,10 +33,11 @@ and \*BSD as well as a `native Windows port`_. .. _homebrew: http://brew.sh/ .. _native Windows Port: https://github.com/thedmd/pianobar-windows -The current pianobar release is 2020.11.28_ (sha256__, sign__). More recent and +The current pianobar release is 2022.04.01_ (sha256__, sign__). More recent and experimental code is available at GitHub_ and the local gitweb_. Older releases are available here: +- 2020.11.28_ (sha256__, sign__) - 2020.04.05_ (sha256__, sign__) - 2019.02.14_ (sha256__, sign__) - 2019.01.25_ (sha256__, sign__) @@ -65,12 +67,15 @@ are available here: - 2010.10.07_ (sha1__) - 2010.08.21_ (sha1__) +.. _2022.04.01: https://6xq.net/pianobar/pianobar-2022.04.01.tar.bz2 +__ https://6xq.net/pianobar/pianobar-2022.04.01.tar.bz2.sha256 +__ https://6xq.net/pianobar/pianobar-2022.04.01.tar.bz2.asc .. _2020.11.28: https://6xq.net/pianobar/pianobar-2020.11.28.tar.bz2 __ https://6xq.net/pianobar/pianobar-2020.11.28.tar.bz2.sha256 __ https://6xq.net/pianobar/pianobar-2020.11.28.tar.bz2.asc .. _snapshot: http://github.com/PromyLOPh/pianobar/tarball/master .. _GitHub: http://github.com/PromyLOPh/pianobar/ -.. _gitweb: https://6xq.net/pianobar/git/ +.. _gitweb: https://6xq.net/git/lars/pianobar.git/ .. _2020.04.05: https://6xq.net/pianobar/pianobar-2020.04.05.tar.bz2 __ https://6xq.net/pianobar/pianobar-2020.04.05.tar.bz2.sha256 __ https://6xq.net/pianobar/pianobar-2020.04.05.tar.bz2.asc @@ -180,10 +185,13 @@ pianobar.el_ Emacs interface for pianobar `pianobar-mediaplayer2`_ Control pianobar like any other media player through DBUS/MPRIS. +PianobarNowPlayable_ + Integrate pianobar with the Now Playing feature of macOS .. _control-pianobar: http://malabarba.github.io/control-pianobar/ .. _pianobar.el: https://github.com/agrif/pianobar.el .. _pianobar-mediaplayer2: https://github.com/ryanswilson59/pianobar-mediaplayer2 +.. _PianobarNowPlayable: https://github.com/iDom818/PianobarNowPlayable Clients +++++++ diff --git a/contrib/config-example b/contrib/config-example index 5f5dc2d..060fbcb 100644 --- a/contrib/config-example +++ b/contrib/config-example @@ -23,7 +23,6 @@ #act_stationaddbygenre = g #act_songinfo = i #act_addshared = j -#act_songmove = m #act_songnext = n #act_songpause = S #act_songpausetoggle = p diff --git a/contrib/pianobar.1 b/contrib/pianobar.1 index c5b82aa..887ae4a 100644 --- a/contrib/pianobar.1 +++ b/contrib/pianobar.1 @@ -103,10 +103,6 @@ beginning. Delete artist/song seeds or feedback. .TP -.B act_songmove = m -Move current song to another station - -.TP .B act_songnext = n Skip current song. @@ -449,13 +445,13 @@ can report certain "events" to an external application (see information like error code and description, was well as song information related to the current event, is supplied through stdin. -Currently supported events are: artistbookmark, songban, songbookmark, -songexplain, songfinish, songlove, songmove, songshelf, songstart, +Currently supported events are: artistbookmark, settingschange, settingsget, +songban, songbookmark, songexplain, songfinish, songlove, songshelf, songstart, stationaddgenre, stationaddmusic, stationaddshared, stationcreate, stationdelete, stationdeleteartistseed, stationdeletefeedback, -stationdeletesongseed, stationfetchinfo, stationfetchplaylist, -stationfetchgenre stationquickmixtoggle, stationrename, userlogin, -usergetstations +stationdeletesongseed, stationdeletestationseed, stationfetchgenre, +stationfetchinfo, stationfetchplaylist, stationgetmodes, stationquickmixtoggle, +stationrename, stationsetmode, usergetstations, userlogin An example script can be found in the contrib/ directory of .B pianobar's diff --git a/src/config.h b/src/config.h index ea8a6ec..f3d3d3e 100644 --- a/src/config.h +++ b/src/config.h @@ -3,7 +3,7 @@ /* package name */ #define PACKAGE "pianobar" -#define VERSION "2020.11.28-dev" +#define VERSION "2022.04.01-dev" /* glibc feature test macros, define _before_ including other files */ #define _POSIX_C_SOURCE 200809L diff --git a/src/libpiano/response.c b/src/libpiano/response.c index 1e79261..0be8872 100644 --- a/src/libpiano/response.c +++ b/src/libpiano/response.c @@ -206,7 +206,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { break; } - for (int i = 0; i < json_object_array_length (stations); i++) { + for (unsigned int i = 0; i < json_object_array_length (stations); i++) { PianoStation_t *tmpStation; json_object *s = json_object_array_get_idx (stations, i); @@ -229,7 +229,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { if (mix != NULL) { PianoStation_t *curStation = ph->stations; PianoListForeachP (curStation) { - for (int i = 0; i < json_object_array_length (mix); i++) { + for (unsigned int i = 0; i < json_object_array_length (mix); i++) { json_object *id = json_object_array_get_idx (mix, i); if (strcmp (json_object_get_string (id), curStation->id) == 0) { @@ -256,7 +256,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { } assert (items != NULL); - for (int i = 0; i < json_object_array_length (items); i++) { + for (unsigned int i = 0; i < json_object_array_length (items); i++) { json_object *s = json_object_array_get_idx (items, i); PianoSong_t *song; @@ -377,7 +377,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { /* get artists */ json_object *artists; if (json_object_object_get_ex (result, "artists", &artists)) { - for (int i = 0; i < json_object_array_length (artists); i++) { + for (unsigned int i = 0; i < json_object_array_length (artists); i++) { json_object *a = json_object_array_get_idx (artists, i); PianoArtist_t *artist; @@ -396,7 +396,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { /* get songs */ json_object *songs; if (json_object_object_get_ex (result, "songs", &songs)) { - for (int i = 0; i < json_object_array_length (songs); i++) { + for (unsigned int i = 0; i < json_object_array_length (songs); i++) { json_object *s = json_object_array_get_idx (songs, i); PianoSong_t *song; @@ -456,7 +456,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { /* get genre stations */ json_object *categories; if (json_object_object_get_ex (result, "categories", &categories)) { - for (int i = 0; i < json_object_array_length (categories); i++) { + for (unsigned int i = 0; i < json_object_array_length (categories); i++) { json_object *c = json_object_array_get_idx (categories, i); PianoGenreCategory_t *tmpGenreCategory; @@ -471,7 +471,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { /* get genre subnodes */ json_object *stations; if (json_object_object_get_ex (c, "stations", &stations)) { - for (int k = 0; + for (unsigned int k = 0; k < json_object_array_length (stations); k++) { json_object *s = json_object_array_get_idx (stations, k); @@ -520,12 +520,13 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { assert (reqData != NULL); json_object *explanations; - if (json_object_object_get_ex (result, "explanations", &explanations)) { + if (json_object_object_get_ex (result, "explanations", &explanations) && + json_object_array_length (explanations) > 0) { reqData->retExplain = malloc (strSize * sizeof (*reqData->retExplain)); strncpy (reqData->retExplain, "We're playing this track " "because it features ", strSize); - for (int i = 0; i < json_object_array_length (explanations); i++) { + for (unsigned int i = 0; i < json_object_array_length (explanations); i++) { json_object *e = json_object_array_get_idx (explanations, i); json_object *f; @@ -573,7 +574,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { /* songs */ json_object *songs; if (json_object_object_get_ex (music, "songs", &songs)) { - for (int i = 0; i < json_object_array_length (songs); i++) { + for (unsigned int i = 0; i < json_object_array_length (songs); i++) { json_object *s = json_object_array_get_idx (songs, i); PianoSong_t *seedSong; @@ -594,7 +595,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { /* artists */ json_object *artists; if (json_object_object_get_ex (music, "artists", &artists)) { - for (int i = 0; i < json_object_array_length (artists); i++) { + for (unsigned int i = 0; i < json_object_array_length (artists); i++) { json_object *a = json_object_array_get_idx (artists, i); PianoArtist_t *seedArtist; @@ -622,7 +623,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { continue; } assert (json_object_is_type (val, json_type_array)); - for (int i = 0; i < json_object_array_length (val); i++) { + for (unsigned int i = 0; i < json_object_array_length (val); i++) { json_object *s = json_object_array_get_idx (val, i); PianoSong_t *feedbackSong; @@ -665,7 +666,7 @@ PianoReturn_t PianoResponse (PianoHandle_t *ph, PianoRequest_t *req) { json_object *availableModes; if (json_object_object_get_ex (result, "availableModes", &availableModes)) { - for (int i = 0; i < json_object_array_length (availableModes); i++) { + for (unsigned int i = 0; i < json_object_array_length (availableModes); i++) { json_object *val = json_object_array_get_idx (availableModes, i); assert (json_object_is_type (val, json_type_object)); diff --git a/src/player.c b/src/player.c index 875f473..753d490 100644 --- a/src/player.c +++ b/src/player.c @@ -235,7 +235,7 @@ static bool openStream (player_t * const player) { softfail ("avcodec_parameters_to_context"); } - AVCodec * const decoder = avcodec_find_decoder (cp->codec_id); + const AVCodec * const decoder = avcodec_find_decoder (cp->codec_id); if (decoder == NULL) { softfail ("find_decoder"); } @@ -282,11 +282,13 @@ static bool openFilter (player_t * const player) { /* abuffer */ AVRational time_base = player->st->time_base; + char channelLayout[128]; + av_channel_layout_describe(&player->cctx->ch_layout, channelLayout, sizeof(channelLayout)); snprintf (strbuf, sizeof (strbuf), - "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%"PRIx64, + "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=%s", time_base.num, time_base.den, cp->sample_rate, av_get_sample_fmt_name (player->cctx->sample_fmt), - cp->channel_layout); + channelLayout); if ((ret = avfilter_graph_create_filter (&player->fabuf, avfilter_get_by_name ("abuffer"), "source", strbuf, NULL, player->fgraph)) < 0) { @@ -340,7 +342,7 @@ static bool openDevice (player_t * const player) { memset (&aoFmt, 0, sizeof (aoFmt)); aoFmt.bits = av_get_bytes_per_sample (avformat) * 8; assert (aoFmt.bits > 0); - aoFmt.channels = cp->channels; + aoFmt.channels = cp->ch_layout.nb_channels; aoFmt.rate = getSampleRate (player); aoFmt.byte_format = AO_FMT_NATIVE; @@ -431,8 +433,12 @@ static int play (player_t * const player) { } else if (ret < 0) { /* error, abort */ /* mark the EOF, so that BarAoPlayThread can quit*/ - debugPrint (DEBUG_AUDIO, "av_read_frame failed with code %i, sending " - "NULL frame\n", ret); + char error[AV_ERROR_MAX_STRING_SIZE]; + if (av_strerror(ret, error, sizeof(error)) < 0) { + strncpy (error, "(unknown)", sizeof(error)-1); + } + debugPrint (DEBUG_AUDIO, "av_read_frame failed with code %i (%s), " + "sending NULL frame\n", ret, error); pthread_mutex_lock (&player->aoplayLock); const int rt = av_buffersrc_add_frame (player->fabuf, NULL); assert (rt == 0); @@ -533,7 +539,9 @@ void *BarPlayerThread (void *data) { if (openFilter (player) && openDevice (player)) { changeMode (player, PLAYER_PLAYING); BarPlayerSetVolume (player); - retry = play (player) == AVERROR_INVALIDDATA && + const int ret = play (player); + retry = (ret == AVERROR_INVALIDDATA || + ret == -ECONNRESET) && !player->interrupted; } else { /* filter missing or audio device busy */ @@ -583,8 +591,7 @@ void *BarAoPlayThread (void *data) { } pthread_mutex_unlock (&player->aoplayLock); - const int numChannels = av_get_channel_layout_nb_channels ( - filteredFrame->channel_layout); + const int numChannels = filteredFrame->ch_layout.nb_channels; const int bps = av_get_bytes_per_sample (filteredFrame->format); ao_play (player->aoDev, (char *) filteredFrame->data[0], filteredFrame->nb_samples * numChannels * bps); diff --git a/src/player.h b/src/player.h index 3179785..2e44aed 100644 --- a/src/player.h +++ b/src/player.h @@ -34,6 +34,7 @@ THE SOFTWARE. #include <ao/ao.h> #include <libavformat/avformat.h> #include <libavfilter/avfilter.h> +#include <libavcodec/avcodec.h> #include <piano.h> #include "settings.h" @@ -161,8 +161,8 @@ static size_t httpFetchCb (char *ptr, size_t size, size_t nmemb, /* libcurl progress callback. aborts the current request if user pressed ^C */ -int progressCb (void * const data, double dltotal, double dlnow, - double ultotal, double ulnow) { +int progressCb (void * const data, curl_off_t dltotal, curl_off_t dlnow, + curl_off_t ultotal, curl_off_t ulnow) { const sig_atomic_t lint = *((sig_atomic_t *) data); if (lint) { return 1; @@ -171,6 +171,27 @@ int progressCb (void * const data, double dltotal, double dlnow, } } +/* Error codes from libcurl, which may be temporary and should be retried. + */ +static bool temporaryCurlError (const CURLcode code) { + switch (code) { + case CURLE_COULDNT_RESOLVE_PROXY: + case CURLE_COULDNT_RESOLVE_HOST: + case CURLE_COULDNT_CONNECT: + case CURLE_WEIRD_SERVER_REPLY: + case CURLE_READ_ERROR: + case CURLE_OPERATION_TIMEDOUT: + case CURLE_SSL_CONNECT_ERROR: + case CURLE_GOT_NOTHING: + case CURLE_SEND_ERROR: + case CURLE_RECV_ERROR: + return true; + + default: + return false; + } +} + #define setAndCheck(k,v) \ httpret = curl_easy_setopt (http, k, v); \ assert (httpret == CURLE_OK); @@ -203,8 +224,8 @@ static CURLcode BarPianoHttpRequest (CURL * const http, setAndCheck (CURLOPT_POSTFIELDS, req->postData); setAndCheck (CURLOPT_WRITEFUNCTION, httpFetchCb); setAndCheck (CURLOPT_WRITEDATA, &buffer); - setAndCheck (CURLOPT_PROGRESSFUNCTION, progressCb); - setAndCheck (CURLOPT_PROGRESSDATA, &lint); + setAndCheck (CURLOPT_XFERINFOFUNCTION, progressCb); + setAndCheck (CURLOPT_XFERINFODATA, &lint); setAndCheck (CURLOPT_NOPROGRESS, 0); setAndCheck (CURLOPT_POST, 1); setAndCheck (CURLOPT_TIMEOUT, settings->timeout); @@ -248,7 +269,7 @@ static CURLcode BarPianoHttpRequest (CURL * const http, do { httpret = curl_easy_perform (http); ++retry; - if (httpret == CURLE_OPERATION_TIMEDOUT) { + if (temporaryCurlError (httpret)) { free (buffer.data); buffer.data = NULL; buffer.pos = 0; @@ -823,6 +844,49 @@ size_t BarUiListSongs (const BarApp_t * const app, return i; } +enum { + NO_DURATION = 0, +}; +#define NO_POSTFIX "" + +/* Print song information to the eventcmd stream + * @param Event command stream. + * @param Song information. + * @param Printed key name postfix, use NO_POSTFIX to print bare keys. + * @param Override song length from song parameter, use NO_DURATION if unavailable. + */ +static void BarUiEventcmdPrintSong (FILE * restrict stream, + const PianoSong_t * const song, const char * const postfix, + const unsigned int songDuration) { + assert (song != NULL); + assert (stream != NULL); + assert (postfix != NULL); + + fprintf (stream, + "artist%s=%s\n" + "title%s=%s\n" + "album%s=%s\n" + "coverArt%s=%s\n" + "rating%s=%i\n" + "detailUrl%s=%s\n" + "songDuration%s=%u\n", + postfix, + song->artist, + postfix, + song->title, + postfix, + song->album, + postfix, + song->coverArt, + postfix, + song->rating, + postfix, + song->detailUrl, + postfix, + songDuration == NO_DURATION ? song->length : songDuration + ); +} + /* Excute external event handler * @param settings containing the cmdline * @param event type @@ -879,36 +943,37 @@ void BarUiStartEventCmd (const BarSettings_t *settings, const char *type, pthread_mutex_unlock (&player->lock); fprintf (pipeWriteFd, - "artist=%s\n" - "title=%s\n" - "album=%s\n" - "coverArt=%s\n" "stationName=%s\n" "songStationName=%s\n" "pRet=%i\n" "pRetStr=%s\n" "wRet=%i\n" "wRetStr=%s\n" - "songDuration=%u\n" - "songPlayed=%u\n" - "rating=%i\n" - "detailUrl=%s\n", - curSong == NULL ? "" : curSong->artist, - curSong == NULL ? "" : curSong->title, - curSong == NULL ? "" : curSong->album, - curSong == NULL ? "" : curSong->coverArt, + "songPlayed=%u\n", curStation == NULL ? "" : curStation->name, songStation == NULL ? "" : songStation->name, pRet, PianoErrorToStr (pRet), wRet, curl_easy_strerror (wRet), - songDuration, - songPlayed, - curSong == NULL ? PIANO_RATE_NONE : curSong->rating, - curSong == NULL ? "" : curSong->detailUrl + songPlayed ); + if (curSong != NULL) { + BarUiEventcmdPrintSong (pipeWriteFd, curSong, NO_POSTFIX, songDuration); + } + + const PianoSong_t *nextSong = PianoListNextP (curSong); + if (nextSong != NULL) { + unsigned int i = 0; + PianoListForeachP (nextSong) { + char postfix[16]; + snprintf (postfix, sizeof(postfix)-1, "Next%i", i); + BarUiEventcmdPrintSong (pipeWriteFd, nextSong, postfix, NO_DURATION); + i++; + } + } + if (stations != NULL) { /* send station list */ PianoStation_t **sortedStations = NULL; diff --git a/src/ui_act.c b/src/ui_act.c index 43d5b6e..fa5c43b 100644 --- a/src/ui_act.c +++ b/src/ui_act.c @@ -271,8 +271,12 @@ BarUiActCallback(BarUiActExplain) { BarUiMsg (&app->settings, MSG_INFO, "Receiving explanation... "); if (BarUiActDefaultPianoCall (PIANO_REQUEST_EXPLAIN, &reqData)) { - BarUiMsg (&app->settings, MSG_INFO, "%s\n", reqData.retExplain); - free (reqData.retExplain); + if (reqData.retExplain == NULL) { + BarUiMsg (&app->settings, MSG_ERR, "No explanation provided.\n"); + } else { + BarUiMsg (&app->settings, MSG_INFO, "%s\n", reqData.retExplain); + free (reqData.retExplain); + } } BarUiActDefaultEventcmd ("songexplain"); } @@ -785,7 +789,12 @@ BarUiActCallback(BarUiActManageStation) { } /* enable submenus depending on data availability */ - strcpy (question, "Delete "); + if (reqData.info.artistSeeds != NULL || + reqData.info.songSeeds != NULL || + reqData.info.stationSeeds != NULL || + reqData.info.feedback != NULL) { + strcpy (question, "Delete "); + } if (reqData.info.artistSeeds != NULL) { strcat (question, "[a]rtist"); *allowedPos++ = 'a'; @@ -814,15 +823,22 @@ BarUiActCallback(BarUiActManageStation) { strcat (question, "[f]eedback"); *allowedPos++ = 'f'; } - /* station mode is always available */ if (allowedPos != allowedActions) { strcat (question, "? "); } - strcat (question, "Manage [m]ode? "); - *allowedPos++ = 'm'; + /* station mode is not available for QuickMix. */ + if (!selStation->isQuickMix) { + strcat (question, "Manage [m]ode? "); + *allowedPos++ = 'm'; + } *allowedPos = '\0'; + if (allowedPos == allowedActions) { + BarUiMsg (&app->settings, MSG_INFO, "No actions available.\n"); + return; + } + assert (strlen (question) < sizeof (question) / sizeof (*question)); BarUiMsg (&app->settings, MSG_QUESTION, "%s", question); |