diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..13c89733 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,79 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + name: build ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-22.04, ubuntu-24.04] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install build dependencies + env: + PACKAGES: > + ccache + libboost-dev + libboost-filesystem-dev + libboost-locale-dev + libboost-program-options-dev + libboost-regex-dev + libboost-thread-dev + libcurl4-gnutls-dev + libfftw3-dev + libmpdclient-dev + libncurses-dev + libreadline-dev + libtag1-dev + zlib1g-dev + run: | + sudo apt update -qq + sudo apt install -y --no-install-recommends ${{ env.PACKAGES }} + + - name: Set up ccache + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/.ccache + key: ccache-${{ matrix.os }}-${{ github.run_id }} + restore-keys: ccache-${{ matrix.os }}- + + - name: Run autoreconf + run: | + autoreconf -fiv + + - name: Build ncmpcpp (light) + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache/light + run: | + CC='ccache gcc' CXX='ccache g++' ./configure \ + --disable-outputs \ + --disable-visualizer \ + --disable-clock \ + --without-fftw \ + --without-taglib \ + --with-lto=$(nproc) + make -j$(nproc) || exit 1 + + - name: Build ncmpcpp (full) + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache/full + run: | + CC='ccache gcc' CXX='ccache g++' ./configure \ + --enable-outputs \ + --enable-visualizer \ + --enable-clock \ + --with-fftw \ + --with-taglib \ + --with-lto=$(nproc) + make -j$(nproc) || exit 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c8884d1..1d5f7563 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -# ncmpcpp-0.10 (????-??-??) +# ncmpcpp-0.10.1 (2024-10-24) +* Fix compilation with `libc++`. +* Remove `autogen.sh` in favour of `autoreconf`. +* Do not crash when trying to reverse an empty playlist. + +# ncmpcpp-0.10 (2024-09-03) * Add the configuration option `mpd_password`. * Separate chunks of lyrics with a double newline. * Fix separator between albums with the same name, to check for album artist @@ -19,6 +24,7 @@ * Add `visualizer_spectrum_smooth_look_legacy_chars` option (enabled by default) for potentially improved bottom part of the spectrum visualizer in terminals with transparent background. +* Add support for fetching lyrics from tags. # ncmpcpp-0.9.2 (2021-01-24) * Revert suppression of output of all external commands as that makes e.g album diff --git a/INSTALL b/INSTALL index cf1d5c6c..bb8c71ba 100644 --- a/INSTALL +++ b/INSTALL @@ -30,7 +30,7 @@ The simplest way to compile this package is: For the next two commands, `csh' users will need to prefix them with `sh '. - 2. Run `./autogen.sh' to generate the `configure' script. + 2. Run `autoreconf -fiv' to generate the `configure' script. 3. Run `./configure' to configure the package for your system. This will take a while. While running, it prints some messages diff --git a/README.md b/README.md index 2cc9b8ad..bcd14e9f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,16 @@ ## ncmpcpp – featureful ncurses based MPD client inspired by ncmpc +### Project status + +The project is officially in maintenance mode. I (Andrzej Rybczak) still use it +daily, but it's feature complete for me and there is very limited time I have +for tending to the issue tracker and open pull requests. + +No new, substantial features should be expected (at least from me). However, if +there are any serious bugs or the project outright stops compiling because of +new, incompatible versions of dependencies, it will be fixed. + ### Main features: * tag editor * playlist editor @@ -34,7 +44,7 @@ The simplest way to compile this package is: For the next two commands, `csh` users will need to prefix them with `sh `. - 2. Run `./autogen.sh` to generate the `configure` script. + 2. Run `autoreconf -fiv` to generate the `configure` script. 3. Run `./configure` to configure the package for your system. This will take a while. While running, it prints some messages diff --git a/autogen.sh b/autogen.sh deleted file mode 100755 index 48c9878b..00000000 --- a/autogen.sh +++ /dev/null @@ -1,158 +0,0 @@ -#!/bin/sh -# Run this to set up the build system: configure, makefiles, etc. -# (at one point this was based on the version in enlightenment's cvs) - -package="ncmpcpp" - -olddir="`pwd`" -srcdir="`dirname $0`" -test -z "$srcdir" && srcdir=. -cd "$srcdir" -DIE= -AM_VERSIONGREP="sed -e s/.*[^0-9\.]\([0-9]\.[0-9]\).*/\1/" -AC_VERSIONGREP="sed -e s/.*[^0-9\.]\([0-9]\.[0-9][0-9]\).*/\1/" -VERSIONMKINT="sed -e s/[^0-9]//" -if test -n "$AM_FORCE_VERSION" -then - AM_VERSIONS="$AM_FORCE_VERSION" -else - AM_VERSIONS='1.6 1.7 1.8 1.9' -fi -if test -n "$AC_FORCE_VERSION" -then - AC_VERSIONS="$AC_FORCE_VERSION" -else - AC_VERSIONS='2.58 2.59' -fi - -versioned_bins () -{ - bin="$1" - needed_int=`echo $VERNEEDED | $VERSIONMKINT` - for i in $VERSIONS - do - i_int=`echo $i | $VERSIONMKINT` - if test $i_int -ge $needed_int - then - echo $bin-$i $bin$i $bin-$i_int $bin$i_int - fi - done - echo $bin -} - -for c in autoconf autoheader automake aclocal -do - uc=`echo $c | tr a-z A-Z` - eval "val=`echo '$'$uc`" - if test -n "$val" - then - echo "$uc=$val in environment, will not attempt to auto-detect" - continue - fi - - case "$c" in - autoconf|autoheader) - VERNEEDED=`fgrep AC_PREREQ configure.ac | $AC_VERSIONGREP` - VERSIONS="$AC_VERSIONS" - pkg=autoconf - ;; - automake|aclocal) - VERNEEDED=`fgrep AUTOMAKE_OPTIONS Makefile.am | $AM_VERSIONGREP` - VERSIONS="$AM_VERSIONS" - pkg=automake - ;; - esac - printf "checking for $c ... " - for x in `versioned_bins $c`; do - ($x --version < /dev/null > /dev/null 2>&1) > /dev/null 2>&1 - if test $? -eq 0 - then - echo $x - eval $uc=$x - break - fi - done - eval "val=`echo '$'$uc`" - if test -z "$val" - then - if test $c = $pkg - then - DIE="$DIE $c=$VERNEEDED" - else - DIE="$DIE $c($pkg)=$VERNEEDED" - fi - fi -done - -if test -n "$LIBTOOLIZE" -then - echo "LIBTOOLIZE=$LIBTOOLIZE in environment," \ - "will not attempt to auto-detect" -else - printf "checking for libtoolize ... " - for x in libtoolize glibtoolize - do - ($x --version < /dev/null > /dev/null 2>&1) > /dev/null 2>&1 - if test $? -eq 0 - then - echo $x - LIBTOOLIZE=$x - break - fi - done -fi - -if test -z "$LIBTOOLIZE" -then - DIE="$DIE libtoolize(libtool)" -fi - -if test -n "$DIE" -then - echo "You must have the following installed to compile $package:" - for i in $DIE - do - printf ' ' - echo $i | sed -e 's/(/ (from /' -e 's/=\(.*\)/ (>= \1)/' - done - echo "Download the appropriate package(s) for your system," - echo "or get the source from one of the GNU ftp sites" - echo "listed in http://www.gnu.org/order/ftp.html" - exit 1 -fi - -echo "Generating configuration files for $package, please wait...." - -ACLOCAL_FLAGS="$ACLOCAL_FLAGS" - -# /usr/share/aclocal is most likely included by default, already... -ac_local_paths=' -/usr/local/share/aclocal -/sw/share/aclocal -/usr/pkg/share/aclocal -/opt/share/aclocal -/usr/gnu/share/aclocal -' - -for i in $ac_local_paths; do - if test -d "$i"; then - ACLOCAL_FLAGS="$ACLOCAL_FLAGS -I $i" - # we probably only want one of these... - break - fi -done - -echo " $ACLOCAL $ACLOCAL_FLAGS" -$ACLOCAL $ACLOCAL_FLAGS || exit 1 - -echo " $AUTOHEADER" -$AUTOHEADER || exit 1 - -echo " $LIBTOOLIZE --automake" -$LIBTOOLIZE --automake || exit 1 - -echo " $AUTOMAKE --add-missing $AUTOMAKE_FLAGS" -$AUTOMAKE --add-missing $AUTOMAKE_FLAGS || exit 1 - -echo " $AUTOCONF" -$AUTOCONF || exit 1 diff --git a/configure.ac b/configure.ac index ffe1bd10..fbc8805d 100644 --- a/configure.ac +++ b/configure.ac @@ -1,4 +1,4 @@ -AC_INIT([ncmpcpp],[0.10_dev]) +AC_INIT([ncmpcpp],[0.10.1]) AC_CONFIG_SRCDIR([configure.ac]) AC_CONFIG_HEADERS(config.h) AM_INIT_AUTOMAKE([subdir-objects]) @@ -142,7 +142,7 @@ LIBS="$LIBS $BOOST_REGEX_LIBS" BOOST_THREAD AC_SUBST(BOOST_THREAD_LDFLAGS) AC_SUBST(BOOST_THREAD_LIBS) -LDFLAGS+="$LDFLAGS $BOOST_THREAD_LDFLAGS" +LDFLAGS="$LDFLAGS $BOOST_THREAD_LDFLAGS" LIBS="$LIBS $BOOST_THREAD_LIBS" # icu diff --git a/doc/config b/doc/config index cc6d2913..791f2084 100644 --- a/doc/config +++ b/doc/config @@ -422,7 +422,7 @@ # #cyclic_scrolling = no # -#lyrics_fetchers = genius, tekstowo, plyrics, justsomelyrics, jahlyrics, zeneszoveg, internet +#lyrics_fetchers = tags, genius, tekstowo, plyrics, justsomelyrics, jahlyrics, zeneszoveg, internet # #follow_now_playing_lyrics = no # diff --git a/src/actions.cpp b/src/actions.cpp index b5d2a823..ed5afd41 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -2007,7 +2007,10 @@ bool ReversePlaylist::canBeRun() return false; m_begin = myPlaylist->main().begin(); m_end = myPlaylist->main().end(); - return findSelectedRangeAndPrintInfoIfNot(m_begin, m_end); + if (m_begin == m_end) + return false; + else + return findSelectedRangeAndPrintInfoIfNot(m_begin, m_end); } void ReversePlaylist::run() diff --git a/src/configuration.cpp b/src/configuration.cpp index c1e26913..455bce5e 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -171,7 +171,7 @@ bool configure(int argc, char **argv) << fetcher->name() << " : " << std::flush; - auto result = fetcher->fetch(std::get<1>(data), std::get<2>(data)); + auto result = fetcher->fetch(std::get<1>(data), std::get<2>(data), {}); std::cout << (result.first ? "ok" : "failed") << "\n"; } diff --git a/src/helpers/song_iterator_maker.h b/src/helpers/song_iterator_maker.h index 4c0f96ea..25b1b941 100644 --- a/src/helpers/song_iterator_maker.h +++ b/src/helpers/song_iterator_maker.h @@ -46,8 +46,8 @@ SongIterator makeSongIterator(IteratorT it) > Extractor; static_assert( std::is_convertible< - typename std::result_of::type, - SongProperties & + std::invoke_result_t, + SongProperties & >::value, "invalid result type of SongPropertiesExtractor"); return SongIterator(boost::make_transform_iterator(it, Extractor{})); } @@ -60,8 +60,8 @@ ConstSongIterator makeConstSongIterator(ConstIteratorT it) > Extractor; static_assert( std::is_convertible< - typename std::result_of::type, - const SongProperties & + std::invoke_result_t, + const SongProperties & >::value, "invalid result type of SongPropertiesExtractor"); return ConstSongIterator(boost::make_transform_iterator(it, Extractor{})); } diff --git a/src/lyrics_fetcher.cpp b/src/lyrics_fetcher.cpp index 8a7d2f51..c27e6702 100644 --- a/src/lyrics_fetcher.cpp +++ b/src/lyrics_fetcher.cpp @@ -19,7 +19,6 @@ ***************************************************************************/ #include "config.h" -#include "curl_handle.h" #include #include @@ -29,8 +28,15 @@ #include #include +#ifdef HAVE_TAGLIB_H +#include +#include +#endif // HAVE_TAGLIB_H + #include "charset.h" +#include "curl_handle.h" #include "lyrics_fetcher.h" +#include "settings.h" #include "utility/html.h" #include "utility/string.h" @@ -52,6 +58,10 @@ std::istream &operator>>(std::istream &is, LyricsFetcher_ &fetcher) fetcher = std::make_unique(); else if (s == "internet") fetcher = std::make_unique(); +#ifdef HAVE_TAGLIB_H + else if (s == "tags") + fetcher = std::make_unique(); +#endif // HAVE_TAGLIB_H else is.setstate(std::ios::failbit); return is; @@ -60,7 +70,8 @@ std::istream &operator>>(std::istream &is, LyricsFetcher_ &fetcher) const char LyricsFetcher::msgNotFound[] = "Not found"; LyricsFetcher::Result LyricsFetcher::fetch(const std::string &artist, - const std::string &title) + const std::string &title, + [[maybe_unused]] const MPD::Song &song) { Result result; result.first = false; @@ -131,7 +142,7 @@ void LyricsFetcher::postProcess(std::string &data) const stripHtmlTags(data); // Remove indentation from each line and collapse multiple newlines into one. std::vector lines; - boost::split(lines, data, boost::is_any_of("\n")); + boost::split(lines, data, boost::is_any_of("\r\n")); for (auto &line : lines) boost::trim(line); auto last = std::unique( @@ -146,7 +157,8 @@ void LyricsFetcher::postProcess(std::string &data) const /**********************************************************************/ LyricsFetcher::Result GoogleLyricsFetcher::fetch(const std::string &artist, - const std::string &title) + const std::string &title, + const MPD::Song &song) { Result result; result.first = false; @@ -188,7 +200,7 @@ LyricsFetcher::Result GoogleLyricsFetcher::fetch(const std::string &artist, data = unescapeHtmlUtf8(urls[0]); URL = data.c_str(); - return LyricsFetcher::fetch("", ""); + return LyricsFetcher::fetch("", "", song); } bool GoogleLyricsFetcher::isURLOk(const std::string &url) @@ -199,9 +211,10 @@ bool GoogleLyricsFetcher::isURLOk(const std::string &url) /**********************************************************************/ LyricsFetcher::Result InternetLyricsFetcher::fetch(const std::string &artist, - const std::string &title) + const std::string &title, + const MPD::Song &song) { - GoogleLyricsFetcher::fetch(artist, title); + GoogleLyricsFetcher::fetch(artist, title, song); LyricsFetcher::Result result; result.first = false; result.second = "The following site may contain lyrics for this song: "; @@ -214,3 +227,42 @@ bool InternetLyricsFetcher::isURLOk(const std::string &url) URL = url; return false; } + +#ifdef HAVE_TAGLIB_H +LyricsFetcher::Result TagsLyricsFetcher::fetch([[maybe_unused]] const std::string &artist, + [[maybe_unused]] const std::string &title, + const MPD::Song &song) +{ + LyricsFetcher::Result result; + result.first = false; + + std::string path; + if (song.isFromDatabase()) + path += Config.mpd_music_dir; + path += song.getURI(); + + TagLib::FileRef f(path.c_str()); + if (f.isNull()) + { + result.second = "Could not open file"; + return result; + } + + TagLib::PropertyMap properties = f.file()->properties(); + + if (properties.contains("LYRICS")) + { + result.first = true; + result.second = properties["LYRICS"].toString("\n\n").to8Bit(true); + } + else if (properties.contains("UNSYNCEDLYRICS")) + { + result.first = true; + result.second = properties["UNSYNCEDLYRICS"].toString("\n\n").to8Bit(true); + } + else + result.second = "No lyrics in tags"; + + return result; +} +#endif // HAVE_TAGLIB_H diff --git a/src/lyrics_fetcher.h b/src/lyrics_fetcher.h index f4cfc0b2..dc1b3f48 100644 --- a/src/lyrics_fetcher.h +++ b/src/lyrics_fetcher.h @@ -26,6 +26,8 @@ #include #include +#include "song.h" + struct LyricsFetcher { typedef std::pair Result; @@ -33,7 +35,7 @@ struct LyricsFetcher virtual ~LyricsFetcher() { } virtual const char *name() const = 0; - virtual Result fetch(const std::string &artist, const std::string &title); + virtual Result fetch(const std::string &artist, const std::string &title, const MPD::Song &song); protected: virtual const char *urlTemplate() const = 0; @@ -57,7 +59,7 @@ std::istream &operator>>(std::istream &is, LyricsFetcher_ &fetcher); struct GoogleLyricsFetcher : public LyricsFetcher { - virtual Result fetch(const std::string &artist, const std::string &title); + virtual Result fetch(const std::string &artist, const std::string &title, const MPD::Song &song); protected: virtual const char *urlTemplate() const { return URL; } @@ -120,7 +122,7 @@ protected: struct InternetLyricsFetcher : public GoogleLyricsFetcher { virtual const char *name() const override { return "the Internet"; } - virtual Result fetch(const std::string &artist, const std::string &title) override; + virtual Result fetch(const std::string &artist, const std::string &title, const MPD::Song &song) override; protected: virtual const char *siteKeyword() const override { return nullptr; } @@ -132,4 +134,16 @@ private: std::string URL; }; +#ifdef HAVE_TAGLIB_H +struct TagsLyricsFetcher : public LyricsFetcher +{ + virtual const char *name() const override { return "tags"; } + virtual Result fetch(const std::string &artist, const std::string &title, const MPD::Song &song) override; + +protected: + virtual const char *urlTemplate() const override { return ""; } + virtual const char *regex() const override { return ""; } +}; +#endif // HAVE_TAGLIB_H + #endif // NCMPCPP_LYRICS_FETCHER_H diff --git a/src/screens/lyrics.cpp b/src/screens/lyrics.cpp index 5ca42869..70ea26cb 100644 --- a/src/screens/lyrics.cpp +++ b/src/screens/lyrics.cpp @@ -151,7 +151,7 @@ boost::optional downloadLyrics( << NC::Format::NoBold << "... "; } } - auto result_ = fetcher_->fetch(s_artist, s_title); + auto result_ = fetcher_->fetch(s_artist, s_title, s); if (result_.first == false) { if (shared_buffer) diff --git a/src/settings.cpp b/src/settings.cpp index d3c94b73..ddde73e0 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -462,6 +462,9 @@ bool Configuration::read(const std::vector &config_paths, bool igno p.add("header_text_scrolling", &header_text_scrolling, "yes", yes_no); p.add("cyclic_scrolling", &use_cyclic_scrolling, "no", yes_no); p.add("lyrics_fetchers", nullptr, +#ifdef HAVE_TAGLIB_H + "tags, " +#endif "genius, tekstowo, plyrics, justsomelyrics, jahlyrics, zeneszoveg, internet", [this](std::string v) { lyrics_fetchers = list_of(v, [](std::string s) { LyricsFetcher_ fetcher;