Merge branch 'ncmpcpp:master' into master

This commit is contained in:
Salastil
2024-08-25 21:35:43 -04:00
committed by GitHub
33 changed files with 321 additions and 211 deletions

View File

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

View File

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

View File

@@ -3,6 +3,22 @@
* 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.
# ncmpcpp-0.9.2 (2021-01-24)
* Revert suppression of output of all external commands as that makes e.g album

View File

@@ -1,11 +1,8 @@
# NCurses Music Player Client (Plus Plus)
Project page - https://rybczak.net/ncmpcpp/
## ncmpcpp featureful ncurses based MPD client inspired by ncmpc
### Main features:
* tag editor
* playlist editor
* easy to use search engine
@@ -18,19 +15,18 @@ Project page - https://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.
@@ -55,7 +51,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`.

View File

@@ -1,15 +1,15 @@
AC_INIT([ncmpcpp], [0.10_dev])
AC_INIT([ncmpcpp],[0.10_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)
@@ -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

View File

@@ -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
@@ -408,7 +422,7 @@
#
#cyclic_scrolling = no
#
#lyrics_fetchers = azlyrics, genius, musixmatch, sing365, metrolyrics, justsomelyrics, jahlyrics, plyrics, tekstowo, zeneszoveg, internet
#lyrics_fetchers = genius, tekstowo, plyrics, justsomelyrics, jahlyrics, zeneszoveg, internet
#
#follow_now_playing_lyrics = no
#

View File

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

View File

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

View File

@@ -1755,33 +1755,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()

View File

@@ -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"),
};

View File

@@ -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 {
@@ -400,6 +403,7 @@ int colorCount()
void initScreen(bool enable_colors, bool enable_mouse)
{
tcgetattr(STDIN_FILENO, &orig_termios);
initscr();
if (has_colors() && enable_colors)
{
@@ -481,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,
@@ -501,12 +506,6 @@ Window::Window(size_t startx, size_t starty, size_t width, size_t height,
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;

View File

@@ -26,7 +26,6 @@
#include "config.h"
#include "curses.h"
#include "gcc.h"
#include <boost/optional.hpp>
#include <functional>

View File

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

View File

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

View File

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

View File

@@ -38,16 +38,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")
@@ -76,7 +68,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 +80,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;
@@ -140,9 +134,11 @@ void LyricsFetcher::postProcess(std::string &data) const
boost::split(lines, data, boost::is_any_of("\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);
}
@@ -202,14 +198,6 @@ 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)
{

View File

@@ -69,34 +69,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,14 +77,6 @@ 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"; }
@@ -142,7 +106,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,7 +114,7 @@ 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 id=\"tartalom_slide_content\"> (.*?)<style>"; }
};
struct InternetLyricsFetcher : public GoogleLyricsFetcher

View File

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

View File

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

View File

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

View File

@@ -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())
{

View File

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

View File

@@ -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("▔🮂🮃🮄🬎🮅🮆█")) // https://unicode.org/charts/PDF/U1FB00.pdf
#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,12 @@ 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});
if (Config.visualizer_spectrum_smooth_look_legacy_chars) {
ch = SMOOTH_CHARS_FLIPPED[idx];
} else {
ch = SMOOTH_CHARS[size-idx-2];
color = NC::FormattedColor(color.color(), {NC::Format::Reverse});
}
} else {
ch = SMOOTH_CHARS[idx];
}
@@ -544,7 +565,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 +610,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 +665,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

View File

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

View File

@@ -253,6 +253,7 @@ bool Configuration::read(const std::vector<std::string> &config_paths, bool igno
});
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);
@@ -277,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) {
@@ -458,9 +461,24 @@ 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,
"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);

View File

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

View File

@@ -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();

View File

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

View File

@@ -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";
}
@@ -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

View File

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

View File

@@ -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());

View File

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

View File

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