Move ncurses related files to curses directory
[ncmpcpp.git] / src / lyrics.cpp
bloba9a7b5589dbf2a171504c1a3b712cf638591125f
1 /***************************************************************************
2 * Copyright (C) 2008-2016 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
4 * *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation; either version 2 of the License, or *
8 * (at your option) any later version. *
9 * *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
14 * *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program; if not, write to the *
17 * Free Software Foundation, Inc., *
18 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
19 ***************************************************************************/
21 #include <boost/algorithm/string/classification.hpp>
22 #include <boost/filesystem/operations.hpp>
23 #include <boost/range/algorithm_ext/erase.hpp>
24 #include <cassert>
25 #include <cerrno>
26 #include <cstring>
27 #include <fstream>
28 #include <thread>
30 #include "curses/scrollpad.h"
31 #include "browser.h"
32 #include "charset.h"
33 #include "curl_handle.h"
34 #include "format_impl.h"
35 #include "global.h"
36 #include "helpers.h"
37 #include "lyrics.h"
38 #include "playlist.h"
39 #include "settings.h"
40 #include "song.h"
41 #include "statusbar.h"
42 #include "title.h"
43 #include "screen_switcher.h"
44 #include "utility/string.h"
46 using Global::MainHeight;
47 using Global::MainStartY;
49 Lyrics *myLyrics;
51 namespace {
53 std::string removeExtension(std::string filename)
55 size_t dot = filename.rfind('.');
56 if (dot != std::string::npos)
57 filename.resize(dot);
58 return filename;
61 std::string lyricsFilename(const MPD::Song &s)
63 std::string filename;
64 if (Config.store_lyrics_in_song_dir && !s.isStream())
66 if (s.isFromDatabase())
67 filename = Config.mpd_music_dir + "/";
68 filename += removeExtension(s.getURI());
69 removeExtension(filename);
71 else
73 std::string artist = s.getArtist();
74 std::string title = s.getTitle();
75 if (artist.empty() || title.empty())
76 filename = removeExtension(s.getName());
77 else
78 filename = artist + " - " + title;
79 removeInvalidCharsFromFilename(filename, Config.generate_win32_compatible_filenames);
80 filename = Config.lyrics_directory + "/" + filename;
82 filename += ".txt";
83 return filename;
86 bool loadLyrics(NC::Scrollpad &w, const std::string &filename)
88 std::ifstream input(filename);
89 if (input.is_open())
91 std::string line;
92 bool first_line = true;
93 while (std::getline(input, line))
95 // Remove carriage returns as they mess up the display.
96 boost::remove_erase(line, '\r');
97 if (!first_line)
98 w << '\n';
99 w << Charset::utf8ToLocale(line);
100 first_line = false;
102 return true;
104 else
105 return false;
108 bool saveLyrics(const std::string &filename, const std::string &lyrics)
110 std::ofstream output(filename);
111 if (output.is_open())
113 output << lyrics;
114 output.close();
115 return true;
117 else
118 return false;
121 boost::optional<std::string> downloadLyrics(
122 const MPD::Song &s,
123 std::shared_ptr<Shared<NC::Buffer>> shared_buffer,
124 std::shared_ptr<std::atomic<bool>> download_stopper,
125 LyricsFetcher *current_fetcher)
127 std::string s_artist = Curl::escape(s.getArtist());
128 std::string s_title = Curl::escape(s.getTitle());
129 // If artist or title is empty, use filename. This should give reasonable
130 // results for google search based lyrics fetchers.
131 if (s_artist.empty() || s_title.empty())
133 s_artist.clear();
134 s_title = s.getName();
135 // Get rid of underscores to improve search results.
136 std::replace_if(s_title.begin(), s_title.end(), boost::is_any_of("-_"), ' ');
137 size_t dot = s_title.rfind('.');
138 if (dot != std::string::npos)
139 s_title.resize(dot);
140 s_title = Curl::escape(s_title);
143 auto fetch_lyrics = [&](auto &fetcher_) {
145 if (shared_buffer)
147 auto buf = shared_buffer->acquire();
148 *buf << "Fetching lyrics from "
149 << NC::Format::Bold
150 << fetcher_->name()
151 << NC::Format::NoBold << "... ";
154 auto result_ = fetcher_->fetch(s_artist, s_title);
155 if (result_.first == false)
157 if (shared_buffer)
159 auto buf = shared_buffer->acquire();
160 *buf << NC::Color::Red
161 << result_.second
162 << NC::Color::End
163 << '\n';
166 return result_;
169 LyricsFetcher::Result fetcher_result;
170 if (current_fetcher == nullptr)
172 for (auto &fetcher : Config.lyrics_fetchers)
174 if (download_stopper && download_stopper->load())
175 return boost::none;
176 fetcher_result = fetch_lyrics(fetcher);
177 if (fetcher_result.first)
178 break;
181 else
182 fetcher_result = fetch_lyrics(current_fetcher);
184 boost::optional<std::string> result;
185 if (fetcher_result.first)
186 result = std::move(fetcher_result.second);
187 return result;
192 Lyrics::Lyrics()
193 : Screen(NC::Scrollpad(0, MainStartY, COLS, MainHeight, "", Config.main_color, NC::Border()))
194 , m_refresh_window(false)
195 , m_scroll_begin(0)
196 , m_fetcher(nullptr)
199 void Lyrics::resize()
201 size_t x_offset, width;
202 getWindowResizeParams(x_offset, width);
203 w.resize(width, MainHeight);
204 w.moveTo(x_offset, MainStartY);
205 hasToBeResized = 0;
208 void Lyrics::update()
210 if (m_worker.valid())
212 auto buffer = m_shared_buffer->acquire();
213 if (!buffer->empty())
215 w << *buffer;
216 buffer->clear();
217 m_refresh_window = true;
220 if (m_worker.is_ready())
222 auto lyrics = m_worker.get();
223 if (lyrics)
225 w.clear();
226 w << Charset::utf8ToLocale(*lyrics);
227 std::string filename = lyricsFilename(m_song);
228 if (!saveLyrics(filename, *lyrics))
229 Statusbar::printf("Couldn't save lyrics as \"%1%\": %2%",
230 filename, strerror(errno));
232 else
233 w << "\nLyrics were not found.\n";
234 clearWorker();
235 m_refresh_window = true;
239 if (m_refresh_window)
241 m_refresh_window = false;
242 w.flush();
243 w.refresh();
247 void Lyrics::switchTo()
249 using Global::myScreen;
250 if (myScreen != this)
252 SwitchTo::execute(this);
253 m_scroll_begin = 0;
254 drawHeader();
256 else
257 switchToPreviousScreen();
260 std::wstring Lyrics::title()
262 std::wstring result = L"Lyrics: ";
263 result += Scroller(
264 Format::stringify<wchar_t>(Format::parse(L"{%a - %t}|{%f}"), &m_song),
265 m_scroll_begin,
266 COLS - result.length() - (Config.design == Design::Alternative
268 : Global::VolumeState.length()));
269 return result;
272 void Lyrics::fetch(const MPD::Song &s)
274 if (!m_worker.valid() || s != m_song)
276 stopDownload();
277 w.clear();
278 m_song = s;
279 if (loadLyrics(w, lyricsFilename(m_song)))
281 clearWorker();
282 m_refresh_window = true;
284 else
286 m_download_stopper = std::make_shared<std::atomic<bool>>(false);
287 m_shared_buffer = std::make_shared<Shared<NC::Buffer>>();
288 m_worker = boost::async(
289 boost::launch::async,
290 std::bind(downloadLyrics,
291 m_song, m_shared_buffer, m_download_stopper, m_fetcher));
296 void Lyrics::refetchCurrent()
298 std::string filename = lyricsFilename(m_song);
299 if (std::remove(filename.c_str()) == -1 && errno != ENOENT)
301 const char msg[] = "Couldn't remove \"%1%\": %2%";
302 Statusbar::printf(msg, wideShorten(filename, COLS - const_strlen(msg) - 25),
303 strerror(errno));
305 else
307 clearWorker();
308 fetch(m_song);
312 void Lyrics::edit()
314 if (Config.external_editor.empty())
316 Statusbar::print("external_editor variable has to be set in configuration file");
317 return;
320 Statusbar::print("Opening lyrics in external editor...");
322 GNUC_UNUSED int res;
323 std::string command;
324 std::string filename = lyricsFilename(m_song);
325 if (Config.use_console_editor)
327 command = "/bin/sh -c \"" + Config.external_editor + " \\\"" + filename + "\\\"\"";
328 res = system(command.c_str());
329 fetch(m_song);
330 // Reset ncurses state to refresh the screen.
331 endwin();
332 initscr();
333 curs_set(0);
335 else
337 command = "nohup " + Config.external_editor
338 + " \"" + filename + "\" > /dev/null 2>&1 &";
339 res = system(command.c_str());
343 void Lyrics::toggleFetcher()
345 if (m_fetcher != nullptr)
347 auto fetcher = std::find_if(Config.lyrics_fetchers.begin(),
348 Config.lyrics_fetchers.end(),
349 [this](auto &f) { return f.get() == m_fetcher; });
350 assert(fetcher != Config.lyrics_fetchers.end());
351 ++fetcher;
352 if (fetcher != Config.lyrics_fetchers.end())
353 m_fetcher = fetcher->get();
354 else
355 m_fetcher = nullptr;
357 else
359 assert(!Config.lyrics_fetchers.empty());
360 m_fetcher = Config.lyrics_fetchers[0].get();
363 if (m_fetcher != nullptr)
364 Statusbar::printf("Using lyrics fetcher: %s", m_fetcher->name());
365 else
366 Statusbar::print("Using all lyrics fetchers");
369 void Lyrics::fetchInBackground(const MPD::Song &s, bool notify_)
371 auto consumer_impl = [this] {
372 std::string lyrics_file;
373 while (true)
375 ConsumerState::Song cs;
377 auto consumer = m_consumer_state.acquire();
378 assert(consumer->running);
379 if (consumer->songs.empty())
381 consumer->running = false;
382 break;
384 lyrics_file = lyricsFilename(consumer->songs.front().song());
385 if (!boost::filesystem::exists(lyrics_file))
387 cs = consumer->songs.front();
388 if (cs.notify())
390 consumer->message = "Fetching lyrics for \""
391 + Format::stringify<char>(Config.song_status_format, &cs.song())
392 + "\"...";
395 consumer->songs.pop();
397 if (!cs.song().empty())
399 auto lyrics = downloadLyrics(cs.song(), nullptr, nullptr, m_fetcher);
400 if (lyrics)
401 saveLyrics(lyrics_file, *lyrics);
406 auto consumer = m_consumer_state.acquire();
407 consumer->songs.emplace(s, notify_);
408 // Start the consumer if it's not running.
409 if (!consumer->running)
411 std::thread t(consumer_impl);
412 t.detach();
413 consumer->running = true;
417 boost::optional<std::string> Lyrics::tryTakeConsumerMessage()
419 boost::optional<std::string> result;
420 auto consumer = m_consumer_state.acquire();
421 if (consumer->message)
423 result = std::move(consumer->message);
424 consumer->message = boost::none;
426 return result;
429 void Lyrics::clearWorker()
431 m_shared_buffer.reset();
432 m_worker = boost::BOOST_THREAD_FUTURE<boost::optional<std::string>>();
435 void Lyrics::stopDownload()
437 if (m_download_stopper)
438 m_download_stopper->store(true);