Compare commits

...

52 Commits

Author SHA1 Message Date
Jacopo De Simoi 8d9612c45a Use private area Unicode for spectrum visualizer 9 months ago
Jacopo De Simoi cb3e1f6791 Add playing-prefix 9 months ago
Andrzej Rybczak e8f1c4bd48 Add support for hexadecimal HTML escape codes 1 year ago
Andrzej Rybczak 2f9a65e177 Update lyrics fetchers 1 year ago
Andrzej Rybczak 3e6d992b93
Release 0.10.1 (#617) 1 year ago
Andrzej Rybczak 4fdafc5320 Do not crash when trying to reverse an empty playlist 1 year ago
Andrzej Rybczak 440e9a5768 Remove autogen.sh in favour of autoreconf 1 year ago
Andrzej Rybczak 4ac9a0f0dd
Add CI (#610) 1 year ago
Andrzej Rybczak 92b478ef31 Fix compilation with libc++ 1 year ago
fkobi 72d990fe25
fix stray + in LDFLAGS assignment (#608) 2 years ago
Andrzej Rybczak f9d0fb608c Put project status in the README 2 years ago
Andrzej Rybczak 1d7d5152c6 Release 0.10 2 years ago
Andrzej Rybczak 9c27eec846 Add support for fetching lyrics from tags 2 years ago
Andrzej Rybczak 510e28c65a Remove link to project page, it's deprecated 2 years ago
Andrzej Rybczak 37aea91be1 Don't throw when window doesn't fit, ncurses can deal with it 2 years ago
Andrzej Rybczak 377994daca Remove Larson from assignes in issue templates 2 years ago
Andrzej Rybczak 33145940c8 curl is not optional 2 years ago
Andrzej Rybczak cd9ae93c3e Fix boost::regex ICU support 2 years ago
Andrzej Rybczak ba484cff1e Use C++20 2 years ago
Andrzej Rybczak 603735d80e Clear warnings 2 years ago
o0nd7ots 5238d0f6ed
Update README.md (#602) 2 years ago
Will Huie 7078ed7f55
Tweaks to frequency spectrum and sound ellipse in visualizer (#464) 2 years ago
Andrzej Rybczak f51968b98b Rename the new option, slightly reword the description, add changelog 2 years ago
TilCreator f83428a665
Improve specrum visulaizer for transparent backgrounds (#467) 2 years ago
Andrzej Rybczak 1c77138f51 Update changelog 2 years ago
Andrzej Rybczak 838f600b57 Fix compilation with taglib 1.11 2 years ago
Andrzej Rybczak 68daf44032 Don't treat unknown lyrics fetchers as errors 2 years ago
Andrzej Rybczak 985bc09b5c Update lyrics fetchers 2 years ago
Andrzej Rybczak b2c8ca91c4 Don't treat empty strings as numbers 2 years ago
Andrzej Rybczak 6f8d12da8c If both strings are numbers, compare them as such 2 years ago
Andrzej Rybczak af7550a69f Adjust the new regex to require 2 minute digits 2 years ago
djvs 8ecbb8b49b
Expose filepath in columns view (#530) 2 years ago
James Le Cuirot b218d90193
Check for taglib with pkg-config before trying taglib-config (#558) 2 years ago
gardockt 95bca25cb6
Tag editor: only write files with modified tags (#536) 2 years ago
George Abbott 8746ded880
Added hh:mm:ss to g command (#550) 2 years ago
Alex Balgavy f76dd085b2
Fix crash when background-fetching lyrics for streams (#561) 2 years ago
Denys Tynok e87c104dd6
fix: update obsolete macros in autoconf (#582) 2 years ago
Andrzej Rybczak afb150138d Change visibility of orig_termios 2 years ago
Drew Marino 51ebe9e52d
Fixed bug related to readline.h (#592) 2 years ago
Andrzej Rybczak 9c396d7ef0 Fix removal of XiphComments 2 years ago
Andrzej Rybczak 81e5cf58b4 extras: fix compilation with taglib 2.0 2 years ago
Andrzej Rybczak dc46f7a49b Fix compilation with taglib 2.0 2 years ago
Larson Carter 9f44edf0b1
Merge pull request #560 from zaidhaan/fix-mpd-block 3 years ago
Zaidhaan Hussain d96e3e514d Fix MPD blocking when server error occurs by handling directly without blocking further execution by throwing 3 years ago
Robert Kaspar 417d7172e5
Read Performer from TPE4, not TPE3 (#497) 4 years ago
leg7 727ca95ca5
Increase the framerate limit for the visualizer. (#490) 4 years ago
squigz 26ee24a938
Add italic text support (#527) 4 years ago
Jeffrey Paul 02754534fd
update homepage link to https (#529) 4 years ago
Andrzej Rybczak 40e4f98ee8 Support sockets without a socket inode 5 years ago
Andrzej Rybczak b023c737e3 Remove options deprecated in 0.9 5 years ago
Andrzej Rybczak 70ca94f739 Curl: stop setting user agent to ncmpcpp 5 years ago
Andrzej Rybczak df1c4d7d7d Fix genius lyrics fetcher 5 years ago
  1. 2
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 2
      .github/ISSUE_TEMPLATE/feature_request.md
  3. 79
      .github/workflows/build.yml
  4. 28
      CHANGELOG.md
  5. 2
      INSTALL
  6. 31
      README.md
  7. 158
      autogen.sh
  8. 52
      configure.ac
  9. 17
      doc/config
  10. 8
      doc/ncmpcpp.1
  11. 2
      extras/artist_to_albumartist.cpp
  12. 28
      src/actions.cpp
  13. 8
      src/configuration.cpp
  14. 1
      src/curl_handle.cpp
  15. 6
      src/curses/formatted_color.cpp
  16. 32
      src/curses/window.cpp
  17. 10
      src/curses/window.h
  18. 31
      src/display.cpp
  19. 4
      src/format.cpp
  20. 29
      src/gcc.h
  21. 8
      src/helpers/song_iterator_maker.h
  22. 1
      src/interfaces.h
  23. 98
      src/lyrics_fetcher.cpp
  24. 62
      src/lyrics_fetcher.h
  25. 2
      src/mpdpp.cpp
  26. 8
      src/mpdpp.h
  27. 5
      src/ncmpcpp.cpp
  28. 28
      src/screens/lyrics.cpp
  29. 28
      src/screens/media_library.cpp
  30. 5
      src/screens/playlist_editor.cpp
  31. 15
      src/screens/tag_editor.cpp
  32. 105
      src/screens/visualizer.cpp
  33. 8
      src/screens/visualizer.h
  34. 61
      src/settings.cpp
  35. 7
      src/settings.h
  36. 6
      src/song.cpp
  37. 1
      src/statusbar.h
  38. 19
      src/tags.cpp
  39. 8
      src/title.cpp
  40. 13
      src/utility/comparators.cpp
  41. 7
      src/utility/conversion.h
  42. 15
      src/utility/html.cpp
  43. 1
      src/utility/string.h
  44. 2
      src/utility/type_conversions.cpp

@ -3,7 +3,7 @@ name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees: larson-carter
assignees:
---

@ -3,7 +3,7 @@ name: Feature request
about: Suggest an idea for this project
title: "[FEATURE]"
labels: enhancement
assignees: larson-carter
assignees:
---

@ -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

@ -1,8 +1,34 @@
# ncmpcpp-0.10 (????-??-??)
# ncmpcpp-0.10.2 (2025-??-??)
* Update lyrics fetchers.
* Add support for hexadecimal HTML escape codes.
# 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
instead of artist.
* Column widths are now configurable via `media_library_column_width_ratio_two`,
`media_library_column_width_ratio_three` and
`playlist_editor_column_width_ratio`.
* Removed deprecated `visualizer_fifo_path` and `visualizer_sync_interval`
options.
* Added support for italic text.
* Framerate limit for visualizer was removed.
* Performer tag is now read from the `TPE4` frame instead of `TPE3`.
* Potential crash when fetching lyrics for streams in the background was fixed.
* Seeking now accepts the `hh:mm:ss` format.
* Tag editor now only writes to files with modified tags.
* Column view can now display full filepaths.
* Updated the list of working lyrics fetchers.
* 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

@ -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

@ -1,11 +1,18 @@
# NCurses Music Player Client (Plus Plus)
Project page - http://rybczak.net/ncmpcpp/
## ncmpcpp – featureful ncurses based MPD client inspired by ncmpc
### Main features:
### 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
* easy to use search engine
@ -18,19 +25,18 @@ Project page - http://rybczak.net/ncmpcpp/
…and a lot more minor functions.
### Dependencies:
* boost library [https://www.boost.org/]
* ncurses library [http://www.gnu.org/software/ncurses/ncurses.html]
* readline library [https://tiswww.case.edu/php/chet/readline/rltop.html]
* curl library (optional, required for fetching lyrics and last.fm data) [https://curl.haxx.se/]
* fftw library (optional, required for frequency spectrum music visualization mode) [http://www.fftw.org/]
* tag library (optional, required for tag editing) [https://taglib.org/]
* [boost](https://www.boost.org/)
* [ncurses](https://invisible-island.net/ncurses/announce.html)
* [readline](https://tiswww.case.edu/php/chet/readline/rltop.html)
* [curl](https://curl.se), for fetching lyrics and last.fm data
#### Optional libraries
* [fftw](http://www.fftw.org), for frequency spectrum music visualization mode
* [taglib](https://taglib.org/), for tag editing
### Known issues:
* No full support for handling encodings other than UTF-8.
### Installation:
The simplest way to compile this package is:
1. `cd` to the directory containing the package's source code.
@ -38,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
@ -55,7 +61,6 @@ The simplest way to compile this package is:
Detailed intallation instructions can be found in the `INSTALL` file.
### Optional features:
Optional features can be enable by specifying them during configure. For
example, to enable visualizer run `./configure --enable-visualizer`.

@ -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

@ -1,15 +1,15 @@
AC_INIT([ncmpcpp], [0.10_dev])
AC_INIT([ncmpcpp],[0.10.2_dev])
AC_CONFIG_SRCDIR([configure.ac])
AC_CONFIG_HEADERS(config.h)
AM_INIT_AUTOMAKE([subdir-objects])
AC_CONFIG_MACRO_DIR([m4])
AC_PREREQ(2.59)
AC_PREREQ([2.71])
AC_LANG_CPLUSPLUS
AC_LANG([C++])
AC_PROG_CXX
AC_PROG_LIBTOOL
LT_INIT
AC_ARG_ENABLE(outputs, AS_HELP_STRING([--enable-outputs], [Enable outputs screen @<:@default=no@:>@]), [outputs=$enableval], [outputs=no])
AC_ARG_ENABLE(visualizer, AS_HELP_STRING([--enable-visualizer], [Enable music visualizer screen @<:@default=no@:>@]), [visualizer=$enableval], [visualizer=no])
@ -70,15 +70,15 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ ]])],
)
CXXFLAGS="$old_CXXFLAGS $fast_math"
# -std=c++14
AC_MSG_CHECKING([whether compiler supports -std=c++14])
# -std=c++20
AC_MSG_CHECKING([whether compiler supports -std=c++20])
old_CXXFLAGS="$CXXFLAGS"
CXXFLAGS="-std=c++14"
CXXFLAGS="-std=c++20"
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ ]])],
AC_MSG_RESULT([yes])
std_cpp14="-std=c++14",
std_cpp14="-std=c++20",
AC_MSG_RESULT([no])
AC_MSG_ERROR([[Your compiler doesn't seem to support C++14, please upgrade (GCC >= 5)]])
AC_MSG_ERROR([[Your compiler doesn't seem to support C++20, please upgrade]])
)
CXXFLAGS="$old_CXXFLAGS $std_cpp14"
@ -90,6 +90,9 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([], [[auto f = [](auto n) { return n*n; }; f(
AC_MSG_ERROR([[Your compiler doesn't seem to support generic lambda expressions, please upgrade (GCC >= 5)]])
)
# warnings
CXXFLAGS="$CXXFLAGS -Wall -Wextra -Wshadow -Wimplicit-fallthrough"
# boost
BOOST_REQUIRE([1.60])
AC_SUBST(BOOST_CPPFLAGS)
@ -139,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
@ -149,7 +152,7 @@ PKG_CHECK_MODULES([ICU], [icu-i18n icu-uc], [
old_LIBS="$LIBS"
AC_SUBST(ICU_CFLAGS)
AC_SUBST(ICU_LIBS)
CPPFLAGS="$CPPFLAGS $ICU_CFLAGS -DU_USING_ICU_NAMESPACE=0"
CPPFLAGS="$CPPFLAGS $ICU_CFLAGS"
LIBS="$LIBS $ICU_LIBS"
AC_MSG_CHECKING([whether boost.regex was compiled with ICU support])
AC_LINK_IFELSE([AC_LANG_PROGRAM([
@ -257,19 +260,30 @@ PKG_CHECK_MODULES([libcurl], [libcurl], [
# taglib
if test "$taglib" != "no" ; then
AC_PATH_PROG(TAGLIB_CONFIG, taglib-config)
if test "$TAGLIB_CONFIG" != "" ; then
CPPFLAGS="$CPPFLAGS `$TAGLIB_CONFIG --cflags`"
LIBS="$LIBS `$TAGLIB_CONFIG --libs`"
PKG_CHECK_MODULES([taglib], [taglib], [
AC_SUBST(taglib_CFLAGS)
AC_SUBST(taglib_LIBS)
], [
AC_PATH_PROG([TAGLIB_CONFIG], [taglib-config])
if test "$TAGLIB_CONFIG" != ""; then
taglib_CFLAGS=`$TAGLIB_CONFIG --cflags`
taglib_LIBS=`$TAGLIB_CONFIG --libs`
else
if test "$taglib" = "yes" ; then
AC_MSG_ERROR([could not find taglib.pc or taglib-config executable])
fi
fi
])
if test "$TAGLIB_CONFIG$taglib_LIBS" != "" ; then
CPPFLAGS="$CPPFLAGS $taglib_CFLAGS"
LIBS="$LIBS $taglib_LIBS"
AC_CHECK_HEADERS([taglib.h], ,
if test "$taglib" = "yes" ; then
AC_MSG_ERROR([missing taglib.h header])
fi
)
else
if test "$taglib" = "yes" ; then
AC_MSG_ERROR([taglib-config executable is missing])
fi
fi
fi

@ -125,6 +125,14 @@
#
#visualizer_spectrum_smooth_look = yes
#
## Use unicode block characters from "symbols for legacy computing". This
## improves the smooth look on transparent terminals by using special unicode
## chars instead of reversing the background and foreground color on the bottom
## edge of the spectrum. If it leads to a garbled output on the bottom edge of
## the spectrum, you can either change the font or disable this option.
#
#visualizer_spectrum_smooth_look_legacy_chars = yes
#
## A value between 1 and 5 inclusive. Specifying a larger value makes the
## visualizer look at a larger slice of time, which results in less jumpy
## visualizer output.
@ -141,6 +149,11 @@
#
#visualizer_spectrum_hz_max = 20000
#
## Use log scaling for the frequency spectrum axes
#
#visualizer_spectrum_log_scale_x = yes
#visualizer_spectrum_log_scale_y = yes
#
##### system encoding #####
##
## ncmpcpp should detect your charset encoding but if it failed to do so, you
@ -172,6 +185,7 @@
##
## %l - length
## %f - filename
## %F - full filepath
## %D - directory
## %a - artist
## %A - album artist
@ -215,6 +229,7 @@
## - 9 - end of current color
## - b - bold text
## - u - underline text
## - i - italic text
## - r - reverse colors
## - a - use alternative character set
##
@ -407,7 +422,7 @@
#
#cyclic_scrolling = no
#
#lyrics_fetchers = azlyrics, genius, musixmatch, sing365, metrolyrics, justsomelyrics, jahlyrics, plyrics, tekstowo, zeneszoveg, internet
#lyrics_fetchers = tags, genius, tekstowo, plyrics, justsomelyrics, jahlyrics, zeneszoveg, internet
#
#follow_now_playing_lyrics = no
#

@ -114,6 +114,13 @@ Automatically scale visualizer size.
.B visualizer_spectrum_smooth_look = yes/no
For spectrum visualizer, use unicode block characters for a smoother, more continuous look. This will override the visualizer_look option. With transparent terminals and visualizer_in_stereo set, artifacts may be visible on the bottom half of the visualization.
.TP
.B visualizer_spectrum_smooth_look_legacy_chars = yes/no
Use unicode block characters from "symbols for legacy computing". This improves
the smooth look on transparent terminals by using special unicode chars instead
of reversing the background and foreground color on the bottom edge of the
spectrum. If it leads to a garbled output on the bottom edge of the spectrum,
you can either change the font or disable this option.
.TP
.B visualizer_spectrum_dft_size = NUMBER
For spectrum visualizer, a value between 1 and 5 inclusive. Specifying a larger value makes the visualizer look at a larger slice of time, which results in less jumpy visualizer output.
.TP
@ -472,6 +479,7 @@ For song format you can use:
%l - length
%f - filename
%F - full filepath
%D - directory
%a - artist
%A - album artist

@ -34,7 +34,7 @@ enum class CopyResult { Success, NoArtist, AlbumArtistAlreadyInPlace };
bool is_framelist_empty(const TagLib::ID3v2::FrameList &list)
{
for (auto it = list.begin(); it != list.end(); ++it)
if ((*it)->toString() != TagLib::String::null)
if (!(*it)->toString().isEmpty())
return false;
return true;
}

@ -1746,33 +1746,46 @@ void JumpToPositionInSong::run()
std::string spos;
{
Statusbar::ScopedLock slock;
Statusbar::put() << "Position to go (in %/m:ss/seconds(s)): ";
Statusbar::put() << "Position to go (in %/h:m:ss/m:ss/seconds(s)): ";
spos = wFooter->prompt();
}
boost::regex rx;
boost::smatch what;
if (boost::regex_match(spos, what, rx.assign("([0-9]+):([0-9]{2})"))) // mm:ss
// mm:ss
if (boost::regex_match(spos, what, rx.assign("([0-9]+):([0-9]{2})")))
{
auto mins = fromString<unsigned>(what[1]);
auto secs = fromString<unsigned>(what[2]);
boundsCheck(secs, 0u, 60u);
Mpd.Seek(s.getPosition(), mins * 60 + secs);
}
else if (boost::regex_match(spos, what, rx.assign("([0-9]+)s"))) // position in seconds
// position in seconds
else if (boost::regex_match(spos, what, rx.assign("([0-9]+)s")))
{
auto secs = fromString<unsigned>(what[1]);
Mpd.Seek(s.getPosition(), secs);
}
else if (boost::regex_match(spos, what, rx.assign("([0-9]+)[%]{0,1}"))) // position in %
// position in%
else if (boost::regex_match(spos, what, rx.assign("([0-9]+)[%]{0,1}")))
{
auto percent = fromString<unsigned>(what[1]);
boundsCheck(percent, 0u, 100u);
int secs = (percent * s.getDuration()) / 100.0;
Mpd.Seek(s.getPosition(), secs);
}
// position in hh:mm:ss
else if (boost::regex_match(spos, what, rx.assign("([0-9]+):([0-9]{2}):([0-9]{2})")))
{
auto hours = fromString<unsigned>(what[1]);
auto mins = fromString<unsigned>(what[2]);
auto secs = fromString<unsigned>(what[3]);
boundsCheck(mins, 0u, 60u);
boundsCheck(secs, 0u, 60u);
Mpd.Seek(s.getPosition(), hours * 3600 + mins * 60 + secs);
}
else
Statusbar::print("Invalid format ([m]:[ss], [s]s, [%]%, [%] accepted)");
Statusbar::print("Invalid format ([h]:[mm]:[ss], [m]:[ss], [s]s, [%]%, [%] accepted)");
}
bool SelectItem::canBeRun()
@ -1985,7 +1998,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()

@ -156,14 +156,10 @@ bool configure(int argc, char **argv)
if (vm.count("test-lyrics-fetchers"))
{
std::vector<std::tuple<std::string, std::string, std::string>> fetcher_data = {
std::make_tuple("azlyrics", "rihanna", "umbrella"),
std::make_tuple("genius", "rihanna", "umbrella"),
std::make_tuple("musixmatch", "rihanna", "umbrella"),
std::make_tuple("sing365", "rihanna", "umbrella"),
std::make_tuple("metrolyrics", "rihanna", "umbrella"),
std::make_tuple("justsomelyrics", "rihanna", "umbrella"),
std::make_tuple("jahlyrics", "sean kingston", "dry your eyes"),
std::make_tuple("plyrics", "offspring", "genocide"),
std::make_tuple("plyrics", "rihanna", "umbrella"),
std::make_tuple("tekstowo", "rihanna", "umbrella"),
std::make_tuple("zeneszoveg", "rihanna", "umbrella"),
};
@ -175,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";
}

@ -41,7 +41,6 @@ CURLcode Curl::perform(std::string &data, const std::string &URL, const std::str
curl_easy_setopt(c, CURLOPT_WRITEDATA, &data);
curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT, timeout);
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1);
curl_easy_setopt(c, CURLOPT_USERAGENT, "ncmpcpp " VERSION);
if (follow_redirect)
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
if (!referer.empty())

@ -30,7 +30,8 @@ void verifyFormats(const NC::FormattedColor::Formats &formats)
if (fmt == NC::Format::NoBold
|| fmt == NC::Format::NoUnderline
|| fmt == NC::Format::NoReverse
|| fmt == NC::Format::NoAltCharset)
|| fmt == NC::Format::NoAltCharset
|| fmt == NC::Format::NoItalic)
throw std::logic_error("FormattedColor can't hold disabling formats");
}
}
@ -71,6 +72,9 @@ std::istream &NC::operator>>(std::istream &is, NC::FormattedColor &fc)
case 'a':
formats.push_back(NC::Format::AltCharset);
break;
case 'i':
formats.push_back(NC::Format::Italic);
break;
default:
is.setstate(std::ios::failbit);
break;

@ -24,6 +24,7 @@
#include <cstdlib>
#include <iostream>
#include <sys/select.h>
#include <termios.h>
#include <unistd.h>
#include "utility/readline.h"
@ -198,6 +199,8 @@ int add_base()
int color_pair_counter;
std::vector<int> color_pair_map;
termios orig_termios;
}
namespace NC {
@ -346,6 +349,10 @@ NC::Format reverseFormat(NC::Format fmt)
return NC::Format::NoAltCharset;
case NC::Format::NoAltCharset:
return NC::Format::AltCharset;
case NC::Format::Italic:
return NC::Format::NoItalic;
case NC::Format::NoItalic:
return NC::Format::Italic;
}
// Unreachable, silence GCC.
return fmt;
@ -396,6 +403,7 @@ int colorCount()
void initScreen(bool enable_colors, bool enable_mouse)
{
tcgetattr(STDIN_FILENO, &orig_termios);
initscr();
if (has_colors() && enable_colors)
{
@ -477,6 +485,7 @@ void destroyScreen()
Mouse::disable();
curs_set(1);
endwin();
tcsetattr(STDIN_FILENO, TCSANOW, &orig_termios);
}
Window::Window(size_t startx, size_t starty, size_t width, size_t height,
@ -494,14 +503,9 @@ Window::Window(size_t startx, size_t starty, size_t width, size_t height,
m_bold_counter(0),
m_underline_counter(0),
m_reverse_counter(0),
m_alt_charset_counter(0)
m_alt_charset_counter(0),
m_italic_counter(0)
{
if (m_start_x > size_t(COLS)
|| m_start_y > size_t(LINES)
|| m_width+m_start_x > size_t(COLS)
|| m_height+m_start_y > size_t(LINES))
throw std::logic_error("constructed window doesn't fit into the terminal");
if (m_border)
{
++m_start_x;
@ -542,6 +546,7 @@ Window::Window(const Window &rhs)
, m_underline_counter(rhs.m_underline_counter)
, m_reverse_counter(rhs.m_reverse_counter)
, m_alt_charset_counter(rhs.m_alt_charset_counter)
, m_italic_counter(rhs.m_italic_counter)
{
setColor(m_color);
}
@ -566,6 +571,7 @@ Window::Window(Window &&rhs)
, m_underline_counter(rhs.m_underline_counter)
, m_reverse_counter(rhs.m_reverse_counter)
, m_alt_charset_counter(rhs.m_alt_charset_counter)
, m_italic_counter(rhs.m_italic_counter)
{
rhs.m_window = nullptr;
}
@ -591,6 +597,7 @@ Window &Window::operator=(Window rhs)
std::swap(m_underline_counter, rhs.m_underline_counter);
std::swap(m_reverse_counter, rhs.m_reverse_counter);
std::swap(m_alt_charset_counter, rhs.m_alt_charset_counter);
std::swap(m_italic_counter, rhs.m_italic_counter);
return *this;
}
@ -780,6 +787,11 @@ void Window::altCharset(bool altcharset_state) const
(altcharset_state ? wattron : wattroff)(m_window, A_ALTCHARSET);
}
void Window::italic(bool italic_state) const
{
(italic_state ? wattron : wattroff)(m_window, A_ITALIC);
}
void Window::setTimeout(int timeout)
{
m_window_timeout = timeout;
@ -1413,6 +1425,12 @@ Window &Window::operator<<(Format format)
case Format::NoAltCharset:
decrease_flag(*this, m_alt_charset_counter, &Window::altCharset);
break;
case Format::Italic:
increase_flag(*this, m_italic_counter, &Window::italic);
break;
case Format::NoItalic:
decrease_flag(*this, m_italic_counter, &Window::italic);
break;
}
return *this;
}

@ -26,7 +26,6 @@
#include "config.h"
#include "curses.h"
#include "gcc.h"
#include <boost/optional.hpp>
#include <functional>
@ -200,7 +199,8 @@ enum class Format {
Bold, NoBold,
Underline, NoUnderline,
Reverse, NoReverse,
AltCharset, NoAltCharset
AltCharset, NoAltCharset,
Italic, NoItalic
};
NC::Format reverseFormat(NC::Format fmt);
@ -531,6 +531,11 @@ private:
/// @param altcharset_state state of altcharset attribute
///
void altCharset(bool altcharset_state) const;
/// Sets state of italic attribute (internal use only)
/// @param italic_state state of italic attribute
///
void italic(bool italic_state) const;
/// pointer to helper function used by getString()
/// @see getString()
@ -562,6 +567,7 @@ private:
int m_underline_counter;
int m_reverse_counter;
int m_alt_charset_counter;
int m_italic_counter;
};
}

@ -47,6 +47,8 @@ const wchar_t *toColumnName(char c)
return L"Filename";
case 'D':
return L"Directory";
case 'F':
return L"Filepath";
case 'a':
return L"Artist";
case 'A':
@ -110,6 +112,8 @@ void setProperties(NC::Menu<T> &menu, const MPD::Song &s, const SongList &list,
&& song_pos == Status::State::currentSongPosition();
if (is_now_playing)
menu << Config.now_playing_prefix;
else
menu << Config.not_playing_prefix;
is_in_playlist = !myPlaylist->isActiveWindow(menu)
&& myPlaylist->checkForSong(s);
@ -129,6 +133,8 @@ void unsetProperties(NC::Menu<T> &menu, bool separate_albums, bool is_now_playin
if (is_now_playing)
menu << Config.now_playing_suffix;
else
menu << Config.not_playing_suffix;
if (separate_albums)
menu << NC::Format::NoUnderline;
@ -158,6 +164,8 @@ void showSongs(NC::Menu<T> &menu, const MPD::Song &s, const SongList &list, cons
}
if (is_now_playing)
x_off -= Config.now_playing_suffix_length;
else
x_off -= Config.not_playing_suffix_length;
if (is_selected)
x_off -= Config.selected_item_suffix_length;
menu << NC::TermManip::ClearToEOL << NC::XY(x_off, y) << right_aligned;
@ -191,9 +199,14 @@ void showSongsInColumns(NC::Menu<T> &menu, const MPD::Song &s, const SongList &l
}
if (is_now_playing)
{
menu_width -= Config.now_playing_prefix_length;
menu_width -= Config.now_playing_suffix_length;
// menu_width -= Config.now_playing_prefix_length;
//menu_width -= Config.now_playing_suffix_length;
} else
{
//menu_width -= Config.not_playing_prefix_length;
//menu_width -= Config.not_playing_suffix_length;
}
if (is_selected)
{
menu_width -= Config.selected_item_prefix_length;
@ -274,7 +287,9 @@ std::string Display::Columns(size_t list_width)
std::string result;
if (Config.columns.empty())
return result;
result += Config.not_playing_prefix.str();
int width;
int remained_width = list_width;
std::vector<Column>::const_iterator it, last = Config.columns.end() - 1;
@ -293,11 +308,11 @@ std::string Display::Columns(size_t list_width)
// and next column, so we substract it now and restore later.
if (it != last)
--width;
// if column doesn't fit into screen, discard it and any other after it.
if (remained_width-width < 0 || width < 0 /* this one may come from (*) */)
break;
std::wstring name;
if (it->name.empty())
{
@ -315,7 +330,7 @@ std::string Display::Columns(size_t list_width)
else
name = it->name;
wideCut(name, width);
int x_off = std::max(0, width - int(wideLength(name)));
if (it->right_alignment)
{
@ -327,7 +342,7 @@ std::string Display::Columns(size_t list_width)
result += Charset::utf8ToLocale(ToString(name));
result += std::string(x_off, NC::Key::Space);
}
if (it != last)
{
// add missing width's part and restore the value.
@ -335,7 +350,7 @@ std::string Display::Columns(size_t list_width)
result += ' ';
}
}
return result;
}

@ -192,6 +192,8 @@ expressions<CharT> parseBracket(const string<CharT> &s,
result.push_back(NC::Format::AltCharset);
else if (flags & Format::Flags::Format && *it == 'r')
result.push_back(NC::Format::Reverse);
else if (flags & Format::Flags::Format && *it == 'i')
result.push_back(NC::Format::Italic);
else if (flags & Format::Flags::Format && *it == '/')
{
++it;
@ -204,6 +206,8 @@ expressions<CharT> parseBracket(const string<CharT> &s,
result.push_back(NC::Format::NoAltCharset);
else if (*it == 'r')
result.push_back(NC::Format::NoReverse);
else if (*it == 'i')
result.push_back(NC::Format::NoItalic);
else
throwError(s, it, invalidCharacter(*it));
}

@ -1,29 +0,0 @@
/***************************************************************************
* Copyright (C) 2008-2021 by Andrzej Rybczak *
* andrzej@rybczak.net *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
***************************************************************************/
#if defined(__GNUC__) && __GNUC__ >= 3
# define GNUC_NORETURN __attribute__((noreturn))
# define GNUC_UNUSED __attribute__((unused))
# define GNUC_PRINTF(a, b) __attribute__((format(printf, a, b)))
#else
# define GNUC_NORETURN
# define GNUC_UNUSED
# define GNUC_PRINTF(a, b)
#endif

@ -46,8 +46,8 @@ SongIterator makeSongIterator(IteratorT it)
> Extractor;
static_assert(
std::is_convertible<
typename std::result_of<Extractor(typename IteratorT::reference)>::type,
SongProperties &
std::invoke_result_t<Extractor, typename IteratorT::reference>,
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<Extractor(typename ConstIteratorT::reference)>::type,
const SongProperties &
std::invoke_result_t<Extractor, typename ConstIteratorT::reference>,
const SongProperties &
>::value, "invalid result type of SongPropertiesExtractor");
return ConstSongIterator(boost::make_transform_iterator(it, Extractor{}));
}

@ -25,7 +25,6 @@
#include <boost/tuple/tuple.hpp>
#include <string>
#include "enums.h"
#include "gcc.h"
#include "screens/screen.h"
#include "song.h"

@ -19,7 +19,6 @@
***************************************************************************/
#include "config.h"
#include "curl_handle.h"
#include <cstdlib>
#include <cstring>
@ -29,8 +28,15 @@
#include <boost/algorithm/string/trim.hpp>
#include <boost/regex.hpp>
#ifdef HAVE_TAGLIB_H
#include <fileref.h>
#include <tpropertymap.h>
#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"
@ -38,16 +44,8 @@ std::istream &operator>>(std::istream &is, LyricsFetcher_ &fetcher)
{
std::string s;
is >> s;
if (s == "azlyrics")
fetcher = std::make_unique<AzLyricsFetcher>();
else if (s == "genius")
if (s == "genius")
fetcher = std::make_unique<GeniusFetcher>();
else if (s == "musixmatch")
fetcher = std::make_unique<MusixmatchFetcher>();
else if (s == "sing365")
fetcher = std::make_unique<Sing365Fetcher>();
else if (s == "metrolyrics")
fetcher = std::make_unique<MetrolyricsFetcher>();
else if (s == "justsomelyrics")
fetcher = std::make_unique<JustSomeLyricsFetcher>();
else if (s == "jahlyrics")
@ -60,6 +58,10 @@ std::istream &operator>>(std::istream &is, LyricsFetcher_ &fetcher)
fetcher = std::make_unique<ZeneszovegFetcher>();
else if (s == "internet")
fetcher = std::make_unique<InternetLyricsFetcher>();
#ifdef HAVE_TAGLIB_H
else if (s == "tags")
fetcher = std::make_unique<TagsLyricsFetcher>();
#endif // HAVE_TAGLIB_H
else
is.setstate(std::ios::failbit);
return is;
@ -68,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;
@ -76,7 +79,7 @@ LyricsFetcher::Result LyricsFetcher::fetch(const std::string &artist,
std::string url = urlTemplate();
boost::replace_all(url, "%artist%", Curl::escape(artist));
boost::replace_all(url, "%title%", Curl::escape(title));
std::string data;
CURLcode code = Curl::perform(data, url, "", true);
@ -88,9 +91,11 @@ LyricsFetcher::Result LyricsFetcher::fetch(const std::string &artist,
auto lyrics = getContent(regex(), data);
//std::cerr << "URL: " << url << "\n";
//std::cerr << "Data: " << data << "\n";
if (lyrics.empty() || notLyrics(data))
{
//std::cerr << "Data: " << data << "\n";
//std::cerr << "Empty: " << lyrics.empty() << "\n";
//std::cerr << "Not Lyrics: " << notLyrics(data) << "\n";
result.second = msgNotFound;
@ -137,12 +142,14 @@ void LyricsFetcher::postProcess(std::string &data) const
stripHtmlTags(data);
// Remove indentation from each line and collapse multiple newlines into one.
std::vector<std::string> 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);
std::unique(lines.begin(), lines.end(), [](std::string &a, std::string &b) {
return a.empty() && b.empty();
});
auto last = std::unique(
lines.begin(),
lines.end(),
[](std::string &a, std::string &b) { return a.empty() && b.empty(); });
lines.erase(last, lines.end());
data = boost::algorithm::join(lines, "\n");
boost::trim(data);
}
@ -150,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;
@ -192,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)
@ -202,18 +210,11 @@ bool GoogleLyricsFetcher::isURLOk(const std::string &url)
/**********************************************************************/
bool MetrolyricsFetcher::isURLOk(const std::string &url)
{
// it sometimes return link to sitemap.xml, which is huge so we need to discard it
return GoogleLyricsFetcher::isURLOk(url) && url.find("sitemap") == std::string::npos;
}
/**********************************************************************/
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: ";
@ -226,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

@ -26,6 +26,8 @@
#include <memory>
#include <string>
#include "song.h"
struct LyricsFetcher
{
typedef std::pair<bool, std::string> 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; }
@ -69,34 +71,6 @@ private:
const char *URL;
};
struct MusixmatchFetcher : public GoogleLyricsFetcher
{
virtual const char *name() const override { return "musixmatch.com"; }
protected:
virtual const char *regex() const override { return "<span class=\"lyrics__content__.*?>(.*?)</span>"; }
virtual void postProcess(std::string &) const override { }
};
struct MetrolyricsFetcher : public GoogleLyricsFetcher
{
virtual const char *name() const override { return "metrolyrics.com"; }
protected:
virtual const char *regex() const override { return "<div class=\"lyrics-body\">(.*?)<!--WIDGET.*?<!-- Second Section -->(.*?)<!--WIDGET.*?<!-- Third Section -->(.*?)</div>"; }
virtual bool isURLOk(const std::string &url) override;
};
struct Sing365Fetcher : public GoogleLyricsFetcher
{
virtual const char *name() const override { return "lyrics007.com"; }
protected:
virtual const char *regex() const override { return "<div class=\"lyrics\">(.*?)</div>"; }
};
struct JustSomeLyricsFetcher : public GoogleLyricsFetcher
{
virtual const char *name() const override { return "justsomelyrics.com"; }
@ -105,20 +79,12 @@ protected:
virtual const char *regex() const override { return "<div class=\"content.*?</div>(.*?)See also"; }
};
struct AzLyricsFetcher : public GoogleLyricsFetcher
{
virtual const char *name() const override { return "azlyrics.com"; }
protected:
virtual const char *regex() const override { return "<div class=\"lyricsh\">.*?</h2>.*<div>(.*?)</div>"; }
};
struct GeniusFetcher : public GoogleLyricsFetcher
{
virtual const char *name() const override { return "genius.com"; }
protected:
virtual const char *regex() const override { return "<div class=\"[Ll]yrics.*?>(.*?)</div>"; }
virtual const char *regex() const override { return "<div data-lyrics-container.*?>(.*?)</div>"; }
};
struct JahLyricsFetcher : public GoogleLyricsFetcher
@ -142,7 +108,7 @@ struct TekstowoFetcher : public GoogleLyricsFetcher
virtual const char *name() const override { return "tekstowo.pl"; }
protected:
virtual const char *regex() const override { return "<div class=\"song-text\".*?>.*?</h2>(.*?)<a"; }
virtual const char *regex() const override { return "<div class=\"inner-text\">(.*?)</div>"; }
};
struct ZeneszovegFetcher : public GoogleLyricsFetcher
@ -150,13 +116,13 @@ struct ZeneszovegFetcher : public GoogleLyricsFetcher
virtual const char *name() const override { return "zeneszoveg.hu"; }
protected:
virtual const char *regex() const override { return "<div class=\"lyrics-plain-text.*?\">(.*?)</div>"; }
virtual const char *regex() const override { return "<div class=\"lyrics-plain-text trans_original\">(.*?)</div>"; }
};
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; }
@ -168,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

@ -147,7 +147,7 @@ unsigned Connection::Version() const
void Connection::SetHostname(const std::string &host)
{
size_t at = host.find("@");
if (at != std::string::npos)
if (at != 0 && at != std::string::npos)
{
m_password = host.substr(0, at);
m_host = host.substr(at+1);

@ -353,8 +353,14 @@ private:
};
template <typename ObjectT>
struct Iterator: std::iterator<std::input_iterator_tag, ObjectT>
struct Iterator
{
using iterator_category = std::input_iterator_tag;
using value_type = ObjectT;
using difference_type = std::ptrdiff_t;
using pointer = ObjectT *;
using reference = ObjectT &;
// shared state of the iterator
struct State
{

@ -219,7 +219,10 @@ int main(int argc, char **argv)
try
{
auto k = Bindings.get(input);
std::any_of(k.first, k.second, std::bind(&Binding::execute, ph::_1));
[[maybe_unused]] bool executed = std::any_of(
k.first,
k.second,
std::bind(&Binding::execute, ph::_1));
}
catch (ConversionError &e)
{

@ -151,7 +151,7 @@ boost::optional<std::string> 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)
@ -361,6 +361,25 @@ void Lyrics::toggleFetcher()
Statusbar::print("Using all lyrics fetchers");
}
/* For HTTP(S) streams, fetchInBackground makes ncmpcpp crash: the lyrics_file
* gets set to the stream URL, which is too long, hence
* boost::filesystem::exists() fails.
* Possible solutions:
* - truncate the URL and use that as a filename. Problem: resulting filename
* might not be unique.
* - generate filenames in a different way for streams. Problem: what is a good
* method for this? Perhaps hashing -- but then the lyrics filenames are not
* informative.
* - skip fetching lyrics for streams with URLs that are too long. Problem:
* this leads to inconsistent behavior since lyrics will be fetched for some
* streams but not others.
* - avoid fetching lyrics for streams altogether.
*
* We choose the last solution, and ignore streams when fetching lyrics. This
* is because fetching lyrics for a stream may not be accurate (streams do not
* always provide the necessary metadata to look up lyrics reliably).
* Furthermore, fetching lyrics for streams is not necessarily desirable.
*/
void Lyrics::fetchInBackground(const MPD::Song &s, bool notify_)
{
auto consumer_impl = [this] {
@ -377,7 +396,10 @@ void Lyrics::fetchInBackground(const MPD::Song &s, bool notify_)
break;
}
lyrics_file = lyricsFilename(consumer->songs.front().song());
if (!boost::filesystem::exists(lyrics_file))
// For long filenames (e.g. http(s) stream URLs), boost::filesystem::exists() fails.
// This if condition is fine, because evaluation order is guaranteed.
if (!consumer->songs.front().song().isStream() && !boost::filesystem::exists(lyrics_file))
{
cs = consumer->songs.front();
if (cs.notify())
@ -389,7 +411,7 @@ void Lyrics::fetchInBackground(const MPD::Song &s, bool notify_)
}
consumer->songs.pop();
}
if (!cs.song().empty())
if (!cs.song().empty() && !cs.song().isStream())
{
auto lyrics = downloadLyrics(cs.song(), nullptr, nullptr, m_fetcher);
if (lyrics)

@ -98,7 +98,7 @@ bool MoveToAlbum(NC::Menu<AlbumEntry> &albums, const std::string &primary_tag, c
struct SortSongs {
typedef NC::Menu<MPD::Song>::Item SongItem;
static const std::array<MPD::Song::GetFunction, 3> GetFuns;
static const std::array<MPD::Song::GetFunction, 4> GetFuns;
LocaleStringComparison m_cmp;
@ -117,26 +117,16 @@ public:
return ret < 0;
}
// Sort by track numbers.
try {
ret = boost::lexical_cast<int>(a.getTags(&MPD::Song::getTrackNumber))
- boost::lexical_cast<int>(b.getTags(&MPD::Song::getTrackNumber));
} catch (boost::bad_lexical_cast &) {
ret = a.getTrackNumber().compare(b.getTrackNumber());
}
if (ret != 0)
return ret < 0;
// If track numbers are equal, sort by the display format.
return Format::stringify<char>(Config.song_library_format, &a)
< Format::stringify<char>(Config.song_library_format, &b);
}
};
const std::array<MPD::Song::GetFunction, 3> SortSongs::GetFuns = {{
const std::array<MPD::Song::GetFunction, 4> SortSongs::GetFuns = {{
&MPD::Song::getDate,
&MPD::Song::getAlbum,
&MPD::Song::getDisc
&MPD::Song::getDisc,
&MPD::Song::getTrackNumber,
}};
class SortAlbumEntries {
@ -346,9 +336,9 @@ void MediaLibrary::update()
for (const auto &album : albums)
{
auto entry = AlbumEntry(
Album(std::move(std::get<0>(album.first)),
std::move(std::get<1>(album.first)),
std::move(std::get<2>(album.first)),
Album(std::get<0>(album.first),
std::get<1>(album.first),
std::get<2>(album.first),
album.second));
if (idx < Albums.size())
Albums[idx].value() = std::move(entry);
@ -445,8 +435,8 @@ void MediaLibrary::update()
{
auto entry = AlbumEntry(
Album(primary_tag,
std::move(std::get<0>(album.first)),
std::move(std::get<1>(album.first)),
std::get<0>(album.first),
std::get<1>(album.first),
album.second));
if (idx < Albums.size())
{

@ -169,10 +169,7 @@ void PlaylistEditor::update()
}
catch (MPD::ServerError &e)
{
if (e.code() == MPD_SERVER_ERROR_SYSTEM) // no playlists directory
Statusbar::print(e.what());
else
throw;
Status::handleServerError(e);
}
if (idx < Playlists.size())
Playlists.resizeList(idx);

@ -798,13 +798,16 @@ void TagEditor::runAction()
Statusbar::print("Writing changes...");
for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
{
Statusbar::printf("Writing tags in \"%1%\"...", (*it)->getName());
if (!Tags::write(**it))
if ((*it)->isModified())
{
Statusbar::printf("Error while writing tags to \"%1%\": %2%",
(*it)->getName(), strerror(errno));
success = 0;
break;
Statusbar::printf("Writing tags in \"%1%\"...", (*it)->getName());
if (!Tags::write(**it))
{
Statusbar::printf("Error while writing tags to \"%1%\": %2%",
(*it)->getName(), strerror(errno));
success = 0;
break;
}
}
}
if (success)

@ -83,7 +83,8 @@ Visualizer::Visualizer()
HZ_MIN(Config.visualizer_spectrum_hz_min),
HZ_MAX(Config.visualizer_spectrum_hz_max),
GAIN(Config.visualizer_spectrum_gain),
SMOOTH_CHARS(ToWString("▁▂▃▄▅▆▇█"))
SMOOTH_CHARS(ToWString("")),
SMOOTH_CHARS_FLIPPED(ToWString(""))
#endif
{
InitDataSource();
@ -95,7 +96,7 @@ Visualizer::Visualizer()
memset(m_fftw_input, 0, sizeof(double)*DFT_TOTAL_SIZE);
m_fftw_output = static_cast<fftw_complex *>(fftw_malloc(sizeof(fftw_complex)*m_fftw_results));
m_fftw_plan = fftw_plan_dft_r2c_1d(DFT_TOTAL_SIZE, m_fftw_input, m_fftw_output, FFTW_ESTIMATE);
m_dft_logspace.reserve(500);
m_dft_freqspace.reserve(500);
m_bar_heights.reserve(100);
# endif // HAVE_FFTW3_H
}
@ -107,7 +108,7 @@ void Visualizer::switchTo()
m_reset_output = true;
drawHeader();
# ifdef HAVE_FFTW3_H
GenLogspace();
GenFreqSpace();
m_bar_heights.reserve(w.getWidth());
# endif // HAVE_FFTW3_H
}
@ -121,7 +122,7 @@ void Visualizer::resize()
hasToBeResized = 0;
InitVisualization();
# ifdef HAVE_FFTW3_H
GenLogspace();
GenFreqSpace();
m_bar_heights.reserve(w.getWidth());
# endif // HAVE_FFTW3_H
}
@ -420,7 +421,7 @@ void Visualizer::DrawSoundEllipseStereo(const int16_t *buf_left, const int16_t *
auto c = toColor(sqrt(x*x + 4*y*y), radius, true);
w << NC::XY(left_half_width + x, top_half_height + y)
<< c
<< Config.visualizer_chars[1]
<< Config.visualizer_chars[0]
<< NC::FormattedColor::End<>(c);
}
}
@ -449,7 +450,7 @@ void Visualizer::DrawFrequencySpectrum(const int16_t *buf, ssize_t samples, size
const size_t win_width = w.getWidth();
size_t cur_bin = 0;
while (cur_bin < m_fftw_results && Bin2Hz(cur_bin) < m_dft_logspace[0])
while (cur_bin < m_fftw_results && Bin2Hz(cur_bin) < m_dft_freqspace[0])
++cur_bin;
for (size_t x = 0; x < win_width; ++x)
{
@ -458,10 +459,10 @@ void Visualizer::DrawFrequencySpectrum(const int16_t *buf, ssize_t samples, size
// accumulate bins
size_t count = 0;
// check right bound
while (cur_bin < m_fftw_results && Bin2Hz(cur_bin) < m_dft_logspace[x])
while (cur_bin < m_fftw_results && Bin2Hz(cur_bin) < m_dft_freqspace[x])
{
// check left bound if not first index
if (x == 0 || Bin2Hz(cur_bin) >= m_dft_logspace[x-1])
if (x == 0 || Bin2Hz(cur_bin) >= m_dft_freqspace[x-1])
{
bar_height += m_freq_magnitudes[cur_bin];
++count;
@ -475,8 +476,19 @@ void Visualizer::DrawFrequencySpectrum(const int16_t *buf, ssize_t samples, size
// average bins
bar_height /= count;
// log scale bar heights
bar_height = (20 * log10(bar_height) + DYNAMIC_RANGE + GAIN) / DYNAMIC_RANGE;
// apply scaling to bar heights
if (Config.visualizer_spectrum_log_scale_y) {
bar_height = (20 * log10(bar_height) + DYNAMIC_RANGE + GAIN) / DYNAMIC_RANGE;
} else {
// apply gain
bar_height *= pow(10, 1.8 + GAIN / 20);
// buff higher frequencies
bar_height *= log2(2 + x) * 80.0/win_width;
// moderately normalize the heights
bar_height = pow(bar_height, 0.65);
//bar_height = pow(10, 1 + GAIN / 20) * bar_height;
}
// Scale bar height between 0 and height
bar_height = bar_height > 0 ? bar_height * height : 0;
bar_height = bar_height > height ? height : bar_height;
@ -498,7 +510,12 @@ void Visualizer::DrawFrequencySpectrum(const int16_t *buf, ssize_t samples, size
++h_idx;
} else {
// data point does not exist, need to interpolate
h = Interpolate(x, h_idx);
if (Config.visualizer_spectrum_log_scale_x) {
h = InterpolateCubic(x, h_idx);
} else {
h = std::min(InterpolateLinear(x, h_idx), height / 1.0);
//h = 0;
}
}
for (size_t j = 0; j < h; ++j)
@ -518,8 +535,7 @@ void Visualizer::DrawFrequencySpectrum(const int16_t *buf, ssize_t samples, size
} else {
// fractional height
if (flipped) {
ch = SMOOTH_CHARS[size-idx-2];
color = NC::FormattedColor(color.color(), {NC::Format::Reverse});
ch = SMOOTH_CHARS_FLIPPED[idx];
} else {
ch = SMOOTH_CHARS[idx];
}
@ -544,7 +560,7 @@ void Visualizer::DrawFrequencySpectrumStereo(const int16_t *buf_left, const int1
DrawFrequencySpectrum(buf_right, samples, height, w.getHeight() - height);
}
double Visualizer::Interpolate(size_t x, size_t h_idx)
double Visualizer::InterpolateCubic(size_t x, size_t h_idx)
{
const double x_next = m_bar_heights[h_idx].first;
const double h_next = m_bar_heights[h_idx].second;
@ -589,6 +605,33 @@ double Visualizer::Interpolate(size_t x, size_t h_idx)
return h_next;
}
double Visualizer::InterpolateLinear(size_t x, size_t h_idx)
{
const double x_next = m_bar_heights[h_idx].first;
const double h_next = m_bar_heights[h_idx].second;
double dh = 0;
if (h_idx == 0) {
// no data points on the left, linear extrapolation
if (h_idx < m_bar_heights.size()) {
const double x_next2 = m_bar_heights[h_idx+1].first;
const double h_next2 = m_bar_heights[h_idx+1].second;
dh = (h_next2 - h_next) / (x_next2 - x_next);
}
return h_next - dh * (x_next - x);
} else if (h_idx < m_bar_heights.size()) {
// simple linear interpolation
const double x_prev = m_bar_heights[h_idx-1].first;
const double h_prev = m_bar_heights[h_idx-1].second;
const double m = (h_next - h_prev) / (x_next - x_prev);
return h_prev + m * (x - x_prev);
}
// no data points on the right: don't interpolate
return h_next;
}
void Visualizer::ApplyWindow(double *output, const int16_t *input, ssize_t samples)
{
// Use Blackman window for low sidelobes and fast sidelobe rolloff
@ -617,10 +660,36 @@ void Visualizer::GenLogspace()
const size_t win_width = w.getWidth();
const size_t left_bins = (log10(HZ_MIN) - win_width*log10(HZ_MIN)) / (log10(HZ_MIN) - log10(HZ_MAX));
// Generate logspaced frequencies
m_dft_logspace.resize(win_width);
const double log_scale = log10(HZ_MAX) / (left_bins + m_dft_logspace.size() - 1);
for (size_t i = left_bins; i < m_dft_logspace.size() + left_bins; ++i) {
m_dft_logspace[i - left_bins] = pow(10, i * log_scale);
m_dft_freqspace.resize(win_width);
const double log_scale = log10(HZ_MAX) / (left_bins + m_dft_freqspace.size() - 1);
for (size_t i = left_bins; i < m_dft_freqspace.size() + left_bins; ++i) {
m_dft_freqspace[i - left_bins] = pow(10, i * log_scale);
}
}
// Generate vector of linearly-spaced frequencies from HZ_MIN to HZ_MAX
void Visualizer::GenLinspace()
{
// Calculate number of extra bins needed between 0 HZ and HZ_MIN
const size_t win_width = w.getWidth();
const size_t left_bins = (HZ_MIN - win_width * HZ_MIN) / (HZ_MIN - HZ_MAX);
// Generate linspaced frequencies
m_dft_freqspace.resize(win_width);
const double lin_scale = HZ_MAX / (left_bins + m_dft_freqspace.size() - 1);
for (size_t i = left_bins; i < m_dft_freqspace.size() + left_bins; ++i) {
m_dft_freqspace[i - left_bins] = i * lin_scale;
}
}
// Generate vector of spectrum frequencies from HZ_MIN to HZ_MAX
// Frequencies are (not) log-scaled depending on
// Config.visualizer_spectrum_log_scale_x
void Visualizer::GenFreqSpace()
{
if (Config.visualizer_spectrum_log_scale_x) {
GenLogspace();
} else {
GenLinspace();
}
}
#endif // HAVE_FFTW3_H

@ -76,8 +76,11 @@ private:
void DrawFrequencySpectrumStereo(const int16_t *, const int16_t *, ssize_t, size_t);
void ApplyWindow(double *, const int16_t *, ssize_t);
void GenLogspace();
void GenLinspace();
void GenFreqSpace();
double Bin2Hz(size_t);
double Interpolate(size_t, size_t);
double InterpolateCubic(size_t, size_t);
double InterpolateLinear(size_t, size_t);
# endif // HAVE_FFTW3_H
void InitDataSource();
@ -113,7 +116,8 @@ private:
const double HZ_MAX;
const double GAIN;
const std::wstring SMOOTH_CHARS;
std::vector<double> m_dft_logspace;
const std::wstring SMOOTH_CHARS_FLIPPED;
std::vector<double> m_dft_freqspace;
std::vector<std::pair<size_t, double>> m_bar_heights;
std::vector<double> m_freq_magnitudes;

@ -212,28 +212,6 @@ void deprecated(const char *option, const char *version_removal,
bool Configuration::read(const std::vector<std::string> &config_paths, bool ignore_errors)
{
option_parser p;
// Deprecated options.
p.add("visualizer_fifo_path", &visualizer_fifo_path, "", [](std::string v) {
if (!v.empty())
{
deprecated("visualizer_fifo_path",
"0.10",
"replaced by visualizer_data_source");
}
return adjust_path(v);
});
p.add<void>("visualizer_sync_interval", nullptr, "", [](std::string v) {
if (!v.empty())
{
deprecated("visualizer_sync_interval",
"0.10",
"set 'buffer_time' parameter of your MPD audio output to '100000' "
"(100ms) or lower if you experience synchronization issues "
"between audio and visualization");
}
});
// keep the same order of variables as in configuration file
p.add("ncmpcpp_directory", &ncmpcpp_directory, "~/.config/ncmpcpp/", adjust_directory);
p.add("lyrics_directory", &lyrics_directory, "~/.lyrics/", adjust_directory);
@ -270,11 +248,12 @@ bool Configuration::read(const std::vector<std::string> &config_paths, bool igno
p.add("visualizer_fps", &visualizer_fps,
"60", [](std::string v) {
uint32_t result = verbose_lexical_cast<uint32_t>(v);
boundsCheck<uint32_t>(result, 30, 144);
boundsCheck<uint32_t>(result, 30, 1000);
return result;
});
p.add("visualizer_autoscale", &visualizer_autoscale, "no", yes_no);
p.add("visualizer_spectrum_smooth_look", &visualizer_spectrum_smooth_look, "yes", yes_no);
p.add("visualizer_spectrum_smooth_look_legacy_chars", &visualizer_spectrum_smooth_look_legacy_chars, "yes", yes_no);
p.add("visualizer_spectrum_dft_size", &visualizer_spectrum_dft_size,
"2", [](std::string v) {
auto result = verbose_lexical_cast<size_t>(v);
@ -299,6 +278,8 @@ bool Configuration::read(const std::vector<std::string> &config_paths, bool igno
lowerBoundCheck<double>(result, Config.visualizer_spectrum_hz_min+1);
return result;
});
p.add("visualizer_spectrum_log_scale_x", &visualizer_spectrum_log_scale_x, "yes", yes_no);
p.add("visualizer_spectrum_log_scale_y", &visualizer_spectrum_log_scale_y, "yes", yes_no);
p.add("visualizer_color", &visualizer_colors,
"blue, cyan, green, yellow, magenta, red", list_of<NC::FormattedColor>);
p.add("system_encoding", &system_encoding, "", [](std::string encoding) {
@ -375,6 +356,16 @@ bool Configuration::read(const std::vector<std::string> &config_paths, bool igno
nullptr,
std::ref(now_playing_suffix_length),
ph::_1));
p.add("not_playing_prefix", &not_playing_prefix, "",
std::bind(buffer_wlength,
nullptr,
std::ref(not_playing_prefix_length),
ph::_1));
p.add("not_playing_suffix", &not_playing_suffix, "",
std::bind(buffer_wlength,
nullptr,
std::ref(not_playing_suffix_length),
ph::_1));
p.add("browser_playlist_prefix", &browser_playlist_prefix, "$2playlist$9 ", buffer);
p.add("selected_item_prefix", &selected_item_prefix, "$6",
std::bind(buffer_wlength,
@ -480,9 +471,27 @@ bool Configuration::read(const std::vector<std::string> &config_paths, bool igno
p.add("titles_visibility", &titles_visibility, "yes", yes_no);
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", &lyrics_fetchers,
"azlyrics, genius, musixmatch, sing365, metrolyrics, justsomelyrics, jahlyrics, plyrics, tekstowo, zeneszoveg, internet",
list_of<LyricsFetcher_>);
p.add<void>("lyrics_fetchers", nullptr,
#ifdef HAVE_TAGLIB_H
"tags, "
#endif
"genius, tekstowo, plyrics, justsomelyrics, jahlyrics, zeneszoveg, internet", [this](std::string v) {
lyrics_fetchers = list_of<LyricsFetcher_>(v, [](std::string s) {
LyricsFetcher_ fetcher;
try {
fetcher = boost::lexical_cast<LyricsFetcher_>(s);
} catch (boost::bad_lexical_cast &) {
std::clog << "Unknown lyrics fetcher: " << s << "\n";
}
return fetcher;
});
auto last = std::remove_if(
lyrics_fetchers.begin(), lyrics_fetchers.end(),
[](const auto &f) { return f.get() == nullptr; });
lyrics_fetchers.erase(last, lyrics_fetchers.end());
if (lyrics_fetchers.empty())
invalid_value(v);
});
p.add("follow_now_playing_lyrics", &now_playing_lyrics, "no", yes_no);
p.add("fetch_lyrics_for_current_song_in_background", &fetch_lyrics_in_background,
"no", yes_no);

@ -86,10 +86,13 @@ struct Configuration
size_t visualizer_fps;
bool visualizer_autoscale;
bool visualizer_spectrum_smooth_look;
bool visualizer_spectrum_smooth_look_legacy_chars;
uint32_t visualizer_spectrum_dft_size;
double visualizer_spectrum_gain;
double visualizer_spectrum_hz_min;
double visualizer_spectrum_hz_max;
bool visualizer_spectrum_log_scale_x;
bool visualizer_spectrum_log_scale_y;
std::string pattern;
@ -109,6 +112,8 @@ struct Configuration
NC::Buffer selected_item_suffix;
NC::Buffer now_playing_prefix;
NC::Buffer now_playing_suffix;
NC::Buffer not_playing_prefix;
NC::Buffer not_playing_suffix;
NC::Buffer modified_item_prefix;
NC::Buffer current_item_prefix;
NC::Buffer current_item_suffix;
@ -209,6 +214,8 @@ struct Configuration
size_t selected_item_suffix_length;
size_t now_playing_prefix_length;
size_t now_playing_suffix_length;
size_t not_playing_prefix_length;
size_t not_playing_suffix_length;
size_t current_item_prefix_length;
size_t current_item_suffix_length;
size_t current_item_inactive_column_prefix_length;

@ -293,7 +293,9 @@ bool Song::isFromDatabase() const
bool Song::isStream() const
{
assert(m_song);
return !strncmp(mpd_song_get_uri(m_song.get()), "http://", 7);
const char *song_uri = mpd_song_get_uri(m_song.get());
// Stream schemas: http, https
return !strncmp(song_uri, "http://", 7) || !strncmp(song_uri, "https://", 8);
}
bool Song::empty() const
@ -308,7 +310,7 @@ std::string Song::ShowTime(unsigned length)
int minutes = length/60;
length -= minutes*60;
int seconds = length;
std::string result;
if (hours > 0)
result = (boost::format("%d:%02d:%02d") % hours % minutes % seconds).str();

@ -24,7 +24,6 @@
#include <boost/format.hpp>
#include "curses/window.h"
#include "settings.h"
#include "gcc.h"
#include "interfaces.h"
namespace Progressbar {

@ -92,7 +92,7 @@ void readID3v2Tags(mpd_song *s, TagLib::ID3v2::Tag *tag)
readFrame(frames["TRCK"], "Track");
readFrame(frames["TCON"], "Genre");
readFrame(frames["TCOM"], "Composer");
readFrame(frames["TPE3"], "Performer");
readFrame(frames["TPE4"], "Performer");
readFrame(frames["TPOS"], "Disc");
readFrame(frames["COMM"], "Comment");
}
@ -123,12 +123,12 @@ void writeCommonTags(const MPD::MutableSong &s, TagLib::Tag *tag)
tag->setArtist(ToWString(s.getArtist()));
tag->setAlbum(ToWString(s.getAlbum()));
try {
tag->setYear(boost::lexical_cast<TagLib::uint>(s.getDate()));
tag->setYear(boost::lexical_cast<unsigned>(s.getDate()));
} catch (boost::bad_lexical_cast &) {
std::cerr << "writeCommonTags: couldn't write 'year' tag to '" << s.getURI() << "' as it's not a positive integer\n";
}
try {
tag->setTrack(boost::lexical_cast<TagLib::uint>(s.getTrack()));
tag->setTrack(boost::lexical_cast<unsigned>(s.getTrack()));
} catch (boost::bad_lexical_cast &) {
std::cerr << "writeCommonTags: couldn't write 'track' tag to '" << s.getURI() << "' as it's not a positive integer\n";
}
@ -166,7 +166,7 @@ void writeID3v2Tags(const MPD::MutableSong &s, TagLib::ID3v2::Tag *tag)
writeID3v2("TRCK", tagList(s, &MPD::Song::getTrack));
writeID3v2("TCON", tagList(s, &MPD::Song::getGenre));
writeID3v2("TCOM", tagList(s, &MPD::Song::getComposer));
writeID3v2("TPE3", tagList(s, &MPD::Song::getPerformer));
writeID3v2("TPE4", tagList(s, &MPD::Song::getPerformer));
writeID3v2("TPOS", tagList(s, &MPD::Song::getDisc));
writeID3v2("COMM", tagList(s, &MPD::Song::getComment));
}
@ -174,6 +174,7 @@ void writeID3v2Tags(const MPD::MutableSong &s, TagLib::ID3v2::Tag *tag)
void writeXiphComments(const MPD::MutableSong &s, TagLib::Ogg::XiphComment *tag)
{
auto writeXiph = [&](const TagLib::String &type, const TagLib::StringList &list) {
tag->removeFields(type);
for (auto it = list.begin(); it != list.end(); ++it)
tag->addField(type, *it, it == list.begin());
};
@ -261,7 +262,7 @@ void read(mpd_song *s)
if (f.isNull())
return;
setAttribute(s, "Time", boost::lexical_cast<std::string>(f.audioProperties()->length()));
setAttribute(s, "Time", boost::lexical_cast<std::string>(f.audioProperties()->lengthInSeconds()));
if (auto mpeg_file = dynamic_cast<TagLib::MPEG::File *>(f.file()))
{
@ -301,15 +302,9 @@ bool write(MPD::MutableSong &s)
if (f.isNull())
return false;
bool saved = false;
if (auto mpeg_file = dynamic_cast<TagLib::MPEG::File *>(f.file()))
{
writeID3v2Tags(s, mpeg_file->ID3v2Tag(true));
// write id3v2.4 tags only
if (!mpeg_file->save(TagLib::MPEG::File::ID3v2, true, 4, false))
return false;
// do not call generic save() as it will duplicate tags
saved = true;
}
else if (auto vorbis_file = dynamic_cast<TagLib::Ogg::Vorbis::File *>(f.file()))
{
@ -326,7 +321,7 @@ bool write(MPD::MutableSong &s)
else
writeCommonTags(s, f.tag());
if (!saved && !f.save())
if (!f.save())
return false;
// TODO: move this somewhere else

@ -37,7 +37,7 @@ void drawHeader()
using Global::myScreen;
using Global::wHeader;
using Global::VolumeState;
if (!Config.header_visibility)
return;
switch (Config.design)
@ -62,9 +62,9 @@ void drawHeader()
mvwhline(wHeader->raw(), 4, 0, 0, COLS);
*wHeader << NC::FormattedColor::End<>(Config.alternative_ui_separator_color)
<< NC::XY((COLS-wideLength(title))/2, 3)
<< NC::Format::Bold
<< title
<< NC::Format::NoBold;
// << NC::Format::Bold
<< title;
// << NC::Format::NoBold;
break;
}
wHeader->refresh();

@ -34,10 +34,23 @@ bool hasTheWord(const std::string &s)
&& (s[3] == ' ');
}
long long strToLL(const char *s, size_t len)
{
long long n = 0;
while (len--)
n = 10*n + *s++ - '0';
return n;
}
}
int LocaleStringComparison::compare(const char *a, size_t a_len, const char *b, size_t b_len) const
{
// If both strings are numbers, compare them as such.
if ( a_len > 0 && std::all_of(a, a + a_len, isdigit)
&& b_len > 0 && std::all_of(b, b + b_len, isdigit))
return strToLL(a, a_len) - strToLL(b, b_len);
size_t ac_off = 0, bc_off = 0;
if (m_ignore_the)
{

@ -26,7 +26,6 @@
#include <boost/type_traits/is_unsigned.hpp>
#include "config.h"
#include "gcc.h"
struct ConversionError
{
@ -44,21 +43,21 @@ struct OutOfBounds : std::exception
const std::string &errorMessage() { return m_error_message; }
template <typename Type>
GNUC_NORETURN static void raise(const Type &value, const Type &lbound, const Type &ubound)
[[noreturn]] static void raise(const Type &value, const Type &lbound, const Type &ubound)
{
throw OutOfBounds((boost::format(
"value is out of bounds ([%1%, %2%] expected, %3% given)") % lbound % ubound % value).str());
}
template <typename Type>
GNUC_NORETURN static void raiseLower(const Type &value, const Type &lbound)
[[noreturn]] static void raiseLower(const Type &value, const Type &lbound)
{
throw OutOfBounds((boost::format(
"value is out of bounds ([%1%, ->) expected, %2% given)") % lbound % value).str());
}
template <typename Type>
GNUC_NORETURN static void raiseUpper(const Type &value, const Type &ubound)
[[noreturn]] static void raiseUpper(const Type &value, const Type &ubound)
{
throw OutOfBounds((boost::format(
"value is out of bounds ((<-, %1%] expected, %2% given)") % ubound % value).str());

@ -20,16 +20,29 @@
#include <algorithm>
#include <boost/algorithm/string/replace.hpp>
#include <cstdlib>
#include "utility/html.h"
std::string unescapeHtmlUtf8(const std::string &data)
{
int base;
size_t offset;
std::string result;
for (size_t i = 0, j; i < data.length(); ++i)
{
if (data[i] == '&' && data[i+1] == '#' && (j = data.find(';', i)) != std::string::npos)
{
int n = atoi(&data.c_str()[i+2]);
if (data[i+2] == 'x')
{
offset = 3;
base = 16;
}
else
{
offset = 2;
base = 10;
}
int n = strtol(&data.c_str()[i+offset], nullptr, base);
if (n >= 0x800)
{
result += (0xe0 | ((n >> 12) & 0x0f));

@ -25,7 +25,6 @@
#include <locale>
#include <string>
#include <vector>
#include "gcc.h"
template <size_t N> size_t const_strlen(const char (&)[N]) {
return N-1;

@ -168,6 +168,8 @@ MPD::Song::GetFunction charToGetFunction(char c)
return &MPD::Song::getDirectory;
case 'f':
return &MPD::Song::getName;
case 'F':
return &MPD::Song::getURI;
case 'a':
return &MPD::Song::getArtist;
case 'A':

Loading…
Cancel
Save