You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
441 lines
11 KiB
441 lines
11 KiB
/*************************************************************************** |
|
* Copyright (C) 2008-2017 by Andrzej Rybczak * |
|
* electricityispower@gmail.com * |
|
* * |
|
* 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. * |
|
***************************************************************************/ |
|
|
|
#include <boost/algorithm/string/classification.hpp> |
|
#include <boost/filesystem/operations.hpp> |
|
#include <boost/range/algorithm_ext/erase.hpp> |
|
#include <cassert> |
|
#include <cerrno> |
|
#include <cstring> |
|
#include <fstream> |
|
#include <thread> |
|
|
|
#include "curses/scrollpad.h" |
|
#include "screens/browser.h" |
|
#include "charset.h" |
|
#include "curl_handle.h" |
|
#include "format_impl.h" |
|
#include "global.h" |
|
#include "helpers.h" |
|
#include "screens/lyrics.h" |
|
#include "screens/playlist.h" |
|
#include "settings.h" |
|
#include "song.h" |
|
#include "statusbar.h" |
|
#include "title.h" |
|
#include "screens/screen_switcher.h" |
|
#include "utility/string.h" |
|
|
|
using Global::MainHeight; |
|
using Global::MainStartY; |
|
|
|
Lyrics *myLyrics; |
|
|
|
namespace { |
|
|
|
std::string removeExtension(std::string filename) |
|
{ |
|
size_t dot = filename.rfind('.'); |
|
if (dot != std::string::npos) |
|
filename.resize(dot); |
|
return filename; |
|
} |
|
|
|
std::string lyricsFilename(const MPD::Song &s) |
|
{ |
|
std::string filename; |
|
if (Config.store_lyrics_in_song_dir && !s.isStream()) |
|
{ |
|
if (s.isFromDatabase()) |
|
filename = Config.mpd_music_dir + "/"; |
|
filename += removeExtension(s.getURI()); |
|
removeExtension(filename); |
|
} |
|
else |
|
{ |
|
std::string artist = s.getArtist(); |
|
std::string title = s.getTitle(); |
|
if (artist.empty() || title.empty()) |
|
filename = removeExtension(s.getName()); |
|
else |
|
filename = artist + " - " + title; |
|
removeInvalidCharsFromFilename(filename, Config.generate_win32_compatible_filenames); |
|
filename = Config.lyrics_directory + "/" + filename; |
|
} |
|
filename += ".txt"; |
|
return filename; |
|
} |
|
|
|
bool loadLyrics(NC::Scrollpad &w, const std::string &filename) |
|
{ |
|
std::ifstream input(filename); |
|
if (input.is_open()) |
|
{ |
|
std::string line; |
|
bool first_line = true; |
|
while (std::getline(input, line)) |
|
{ |
|
// Remove carriage returns as they mess up the display. |
|
boost::remove_erase(line, '\r'); |
|
if (!first_line) |
|
w << '\n'; |
|
w << Charset::utf8ToLocale(line); |
|
first_line = false; |
|
} |
|
return true; |
|
} |
|
else |
|
return false; |
|
} |
|
|
|
bool saveLyrics(const std::string &filename, const std::string &lyrics) |
|
{ |
|
std::ofstream output(filename); |
|
if (output.is_open()) |
|
{ |
|
output << lyrics; |
|
output.close(); |
|
return true; |
|
} |
|
else |
|
return false; |
|
} |
|
|
|
boost::optional<std::string> downloadLyrics( |
|
const MPD::Song &s, |
|
std::shared_ptr<Shared<NC::Buffer>> shared_buffer, |
|
std::shared_ptr<std::atomic<bool>> download_stopper, |
|
LyricsFetcher *current_fetcher) |
|
{ |
|
std::string s_artist = s.getArtist(); |
|
std::string s_title = s.getTitle(); |
|
// If artist or title is empty, use filename. This should give reasonable |
|
// results for google search based lyrics fetchers. |
|
if (s_artist.empty() || s_title.empty()) |
|
{ |
|
s_artist.clear(); |
|
s_title = s.getName(); |
|
// Get rid of underscores to improve search results. |
|
std::replace_if(s_title.begin(), s_title.end(), boost::is_any_of("-_"), ' '); |
|
size_t dot = s_title.rfind('.'); |
|
if (dot != std::string::npos) |
|
s_title.resize(dot); |
|
} |
|
|
|
auto fetch_lyrics = [&](auto &fetcher_) { |
|
{ |
|
if (shared_buffer) |
|
{ |
|
auto buf = shared_buffer->acquire(); |
|
*buf << "Fetching lyrics from " |
|
<< NC::Format::Bold |
|
<< fetcher_->name() |
|
<< NC::Format::NoBold << "... "; |
|
} |
|
} |
|
auto result_ = fetcher_->fetch(s_artist, s_title); |
|
if (result_.first == false) |
|
{ |
|
if (shared_buffer) |
|
{ |
|
auto buf = shared_buffer->acquire(); |
|
*buf << NC::Color::Red |
|
<< result_.second |
|
<< NC::Color::End |
|
<< '\n'; |
|
} |
|
} |
|
return result_; |
|
}; |
|
|
|
LyricsFetcher::Result fetcher_result; |
|
if (current_fetcher == nullptr) |
|
{ |
|
for (auto &fetcher : Config.lyrics_fetchers) |
|
{ |
|
if (download_stopper && download_stopper->load()) |
|
return boost::none; |
|
fetcher_result = fetch_lyrics(fetcher); |
|
if (fetcher_result.first) |
|
break; |
|
} |
|
} |
|
else |
|
fetcher_result = fetch_lyrics(current_fetcher); |
|
|
|
boost::optional<std::string> result; |
|
if (fetcher_result.first) |
|
result = std::move(fetcher_result.second); |
|
return result; |
|
} |
|
|
|
} |
|
|
|
Lyrics::Lyrics() |
|
: Screen(NC::Scrollpad(0, MainStartY, COLS, MainHeight, "", Config.main_color, NC::Border())) |
|
, m_refresh_window(false) |
|
, m_scroll_begin(0) |
|
, m_fetcher(nullptr) |
|
{ } |
|
|
|
void Lyrics::resize() |
|
{ |
|
size_t x_offset, width; |
|
getWindowResizeParams(x_offset, width); |
|
w.resize(width, MainHeight); |
|
w.moveTo(x_offset, MainStartY); |
|
hasToBeResized = 0; |
|
} |
|
|
|
void Lyrics::update() |
|
{ |
|
if (m_worker.valid()) |
|
{ |
|
auto buffer = m_shared_buffer->acquire(); |
|
if (!buffer->empty()) |
|
{ |
|
w << *buffer; |
|
buffer->clear(); |
|
m_refresh_window = true; |
|
} |
|
|
|
if (m_worker.is_ready()) |
|
{ |
|
auto lyrics = m_worker.get(); |
|
if (lyrics) |
|
{ |
|
w.clear(); |
|
w << Charset::utf8ToLocale(*lyrics); |
|
std::string filename = lyricsFilename(m_song); |
|
if (!saveLyrics(filename, *lyrics)) |
|
Statusbar::printf("Couldn't save lyrics as \"%1%\": %2%", |
|
filename, strerror(errno)); |
|
} |
|
else |
|
w << "\nLyrics were not found.\n"; |
|
clearWorker(); |
|
m_refresh_window = true; |
|
} |
|
} |
|
|
|
if (m_refresh_window) |
|
{ |
|
m_refresh_window = false; |
|
w.flush(); |
|
w.refresh(); |
|
} |
|
} |
|
|
|
void Lyrics::switchTo() |
|
{ |
|
using Global::myScreen; |
|
if (myScreen != this) |
|
{ |
|
SwitchTo::execute(this); |
|
m_scroll_begin = 0; |
|
drawHeader(); |
|
} |
|
else |
|
switchToPreviousScreen(); |
|
} |
|
|
|
std::wstring Lyrics::title() |
|
{ |
|
std::wstring result = L"Lyrics"; |
|
if (!m_song.empty()) |
|
{ |
|
result += L": "; |
|
result += Scroller( |
|
Format::stringify<wchar_t>(Format::parse(L"{%a - %t}|{%f}"), &m_song), |
|
m_scroll_begin, |
|
COLS - result.length() - (Config.design == Design::Alternative |
|
? 2 |
|
: Global::VolumeState.length())); |
|
} |
|
return result; |
|
} |
|
|
|
void Lyrics::fetch(const MPD::Song &s) |
|
{ |
|
if (!m_worker.valid() || s != m_song) |
|
{ |
|
stopDownload(); |
|
w.clear(); |
|
w.reset(); |
|
m_song = s; |
|
if (loadLyrics(w, lyricsFilename(m_song))) |
|
{ |
|
clearWorker(); |
|
m_refresh_window = true; |
|
} |
|
else |
|
{ |
|
m_download_stopper = std::make_shared<std::atomic<bool>>(false); |
|
m_shared_buffer = std::make_shared<Shared<NC::Buffer>>(); |
|
m_worker = boost::async( |
|
boost::launch::async, |
|
std::bind(downloadLyrics, |
|
m_song, m_shared_buffer, m_download_stopper, m_fetcher)); |
|
} |
|
} |
|
} |
|
|
|
void Lyrics::refetchCurrent() |
|
{ |
|
std::string filename = lyricsFilename(m_song); |
|
if (std::remove(filename.c_str()) == -1 && errno != ENOENT) |
|
{ |
|
const char msg[] = "Couldn't remove \"%1%\": %2%"; |
|
Statusbar::printf(msg, wideShorten(filename, COLS - const_strlen(msg) - 25), |
|
strerror(errno)); |
|
} |
|
else |
|
{ |
|
clearWorker(); |
|
fetch(m_song); |
|
} |
|
} |
|
|
|
void Lyrics::edit() |
|
{ |
|
if (Config.external_editor.empty()) |
|
{ |
|
Statusbar::print("external_editor variable has to be set in configuration file"); |
|
return; |
|
} |
|
|
|
Statusbar::print("Opening lyrics in external editor..."); |
|
|
|
GNUC_UNUSED int res; |
|
std::string command; |
|
std::string filename = lyricsFilename(m_song); |
|
if (Config.use_console_editor) |
|
{ |
|
command = Config.external_editor + " '" + filename + "'"; |
|
NC::pauseScreen(); |
|
res = system(command.c_str()); |
|
NC::unpauseScreen(); |
|
fetch(m_song); |
|
} |
|
else |
|
{ |
|
command = "nohup " + Config.external_editor |
|
+ " \"" + filename + "\" > /dev/null 2>&1 &"; |
|
res = system(command.c_str()); |
|
} |
|
} |
|
|
|
void Lyrics::toggleFetcher() |
|
{ |
|
if (m_fetcher != nullptr) |
|
{ |
|
auto fetcher = std::find_if(Config.lyrics_fetchers.begin(), |
|
Config.lyrics_fetchers.end(), |
|
[this](auto &f) { return f.get() == m_fetcher; }); |
|
assert(fetcher != Config.lyrics_fetchers.end()); |
|
++fetcher; |
|
if (fetcher != Config.lyrics_fetchers.end()) |
|
m_fetcher = fetcher->get(); |
|
else |
|
m_fetcher = nullptr; |
|
} |
|
else |
|
{ |
|
assert(!Config.lyrics_fetchers.empty()); |
|
m_fetcher = Config.lyrics_fetchers[0].get(); |
|
} |
|
|
|
if (m_fetcher != nullptr) |
|
Statusbar::printf("Using lyrics fetcher: %s", m_fetcher->name()); |
|
else |
|
Statusbar::print("Using all lyrics fetchers"); |
|
} |
|
|
|
void Lyrics::fetchInBackground(const MPD::Song &s, bool notify_) |
|
{ |
|
auto consumer_impl = [this] { |
|
std::string lyrics_file; |
|
while (true) |
|
{ |
|
ConsumerState::Song cs; |
|
{ |
|
auto consumer = m_consumer_state.acquire(); |
|
assert(consumer->running); |
|
if (consumer->songs.empty()) |
|
{ |
|
consumer->running = false; |
|
break; |
|
} |
|
lyrics_file = lyricsFilename(consumer->songs.front().song()); |
|
if (!boost::filesystem::exists(lyrics_file)) |
|
{ |
|
cs = consumer->songs.front(); |
|
if (cs.notify()) |
|
{ |
|
consumer->message = "Fetching lyrics for \"" |
|
+ Format::stringify<char>(Config.song_status_format, &cs.song()) |
|
+ "\"..."; |
|
} |
|
} |
|
consumer->songs.pop(); |
|
} |
|
if (!cs.song().empty()) |
|
{ |
|
auto lyrics = downloadLyrics(cs.song(), nullptr, nullptr, m_fetcher); |
|
if (lyrics) |
|
saveLyrics(lyrics_file, *lyrics); |
|
} |
|
} |
|
}; |
|
|
|
auto consumer = m_consumer_state.acquire(); |
|
consumer->songs.emplace(s, notify_); |
|
// Start the consumer if it's not running. |
|
if (!consumer->running) |
|
{ |
|
std::thread t(consumer_impl); |
|
t.detach(); |
|
consumer->running = true; |
|
} |
|
} |
|
|
|
boost::optional<std::string> Lyrics::tryTakeConsumerMessage() |
|
{ |
|
boost::optional<std::string> result; |
|
auto consumer = m_consumer_state.acquire(); |
|
if (consumer->message) |
|
{ |
|
result = std::move(consumer->message); |
|
consumer->message = boost::none; |
|
} |
|
return result; |
|
} |
|
|
|
void Lyrics::clearWorker() |
|
{ |
|
m_shared_buffer.reset(); |
|
m_worker = boost::BOOST_THREAD_FUTURE<boost::optional<std::string>>(); |
|
} |
|
|
|
void Lyrics::stopDownload() |
|
{ |
|
if (m_download_stopper) |
|
m_download_stopper->store(true); |
|
}
|
|
|