actions: use unique_ptr for storing actions
[ncmpcpp.git] / src / lyrics.cpp
blob49155ebd73d02141bb0cf362fe949e1b6a27127b
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>
29 #include "browser.h"
30 #include "charset.h"
31 #include "curl_handle.h"
32 #include "format_impl.h"
33 #include "global.h"
34 #include "helpers.h"
35 #include "lyrics.h"
36 #include "playlist.h"
37 #include "scrollpad.h"
38 #include "settings.h"
39 #include "song.h"
40 #include "statusbar.h"
41 #include "title.h"
42 #include "screen_switcher.h"
43 #include "utility/string.h"
45 using Global::MainHeight;
46 using Global::MainStartY;
48 Lyrics *myLyrics;
50 namespace {
52 std::string removeExtension(std::string filename)
54 size_t dot = filename.rfind('.');
55 if (dot != std::string::npos)
56 filename.resize(dot);
57 return filename;
60 std::string lyricsFilename(const MPD::Song &s)
62 std::string filename;
63 if (Config.store_lyrics_in_song_dir && !s.isStream())
65 if (s.isFromDatabase())
66 filename = Config.mpd_music_dir + "/";
67 filename += removeExtension(s.getURI());
68 removeExtension(filename);
70 else
72 std::string artist = s.getArtist();
73 std::string title = s.getTitle();
74 if (artist.empty() || title.empty())
75 filename = removeExtension(s.getName());
76 else
77 filename = artist + " - " + title;
78 removeInvalidCharsFromFilename(filename, Config.generate_win32_compatible_filenames);
79 filename = Config.lyrics_directory + "/" + filename;
81 filename += ".txt";
82 return filename;
85 bool loadLyrics(NC::Scrollpad &w, const std::string &filename)
87 std::ifstream input(filename);
88 if (input.is_open())
90 std::string line;
91 bool first_line = true;
92 while (std::getline(input, line))
94 // Remove carriage returns as they mess up the display.
95 boost::remove_erase(line, '\r');
96 if (!first_line)
97 w << '\n';
98 w << Charset::utf8ToLocale(line);
99 first_line = false;
101 return true;
103 else
104 return false;
107 bool saveLyrics(const std::string &filename, const std::string &lyrics)
109 std::ofstream output(filename);
110 if (output.is_open())
112 output << lyrics;
113 output.close();
114 return true;
116 else
117 return false;
120 boost::optional<std::string> downloadLyrics(
121 const MPD::Song &s,
122 std::shared_ptr<Shared<NC::Buffer>> shared_buffer,
123 LyricsFetcher *current_fetcher)
125 std::string s_artist = Curl::escape(s.getArtist());
126 std::string s_title = Curl::escape(s.getTitle());
127 // If artist or title is empty, use filename. This should give reasonable
128 // results for google search based lyrics fetchers.
129 if (s_artist.empty() || s_title.empty())
131 s_artist.clear();
132 s_title = s.getName();
133 // Get rid of underscores to improve search results.
134 std::replace_if(s_title.begin(), s_title.end(), boost::is_any_of("-_"), ' ');
135 size_t dot = s_title.rfind('.');
136 if (dot != std::string::npos)
137 s_title.resize(dot);
138 s_title = Curl::escape(s_title);
141 auto fetch_lyrics = [&](auto &fetcher_) {
143 if (shared_buffer)
145 auto buf = shared_buffer->acquire();
146 *buf << "Fetching lyrics from "
147 << NC::Format::Bold
148 << fetcher_->name()
149 << NC::Format::NoBold << "... ";
152 auto result_ = fetcher_->fetch(s_artist, s_title);
153 if (result_.first == false)
155 if (shared_buffer)
157 auto buf = shared_buffer->acquire();
158 *buf << NC::Color::Red
159 << result_.second
160 << NC::Color::End
161 << '\n';
164 return result_;
167 LyricsFetcher::Result fetcher_result;
168 if (current_fetcher == nullptr)
170 for (auto &fetcher : Config.lyrics_fetchers)
172 fetcher_result = fetch_lyrics(fetcher);
173 if (fetcher_result.first)
174 break;
177 else
178 fetcher_result = fetch_lyrics(current_fetcher);
180 boost::optional<std::string> result;
181 if (fetcher_result.first)
182 result = std::move(fetcher_result.second);
183 return result;
188 Lyrics::Lyrics()
189 : Screen(NC::Scrollpad(0, MainStartY, COLS, MainHeight, "", Config.main_color, NC::Border()))
190 , m_refresh_window(false)
191 , m_scroll_begin(0)
192 , m_fetcher(nullptr)
195 void Lyrics::resize()
197 size_t x_offset, width;
198 getWindowResizeParams(x_offset, width);
199 w.resize(width, MainHeight);
200 w.moveTo(x_offset, MainStartY);
201 hasToBeResized = 0;
204 void Lyrics::update()
206 if (m_worker.valid())
208 auto buffer = m_shared_buffer->acquire();
209 if (!buffer->empty())
211 w << *buffer;
212 buffer->clear();
213 m_refresh_window = true;
216 if (m_worker.wait_for(std::chrono::seconds(0)) == std::future_status::ready)
218 auto lyrics = m_worker.get();
219 if (lyrics)
221 w.clear();
222 w << Charset::utf8ToLocale(*lyrics);
223 std::string filename = lyricsFilename(m_song);
224 if (!saveLyrics(filename, *lyrics))
225 Statusbar::printf("Couldn't save lyrics as \"%1%\": %2%",
226 filename, strerror(errno));
228 else
229 w << "\nLyrics were not found.\n";
230 clearWorker();
231 m_refresh_window = true;
235 if (m_refresh_window)
237 m_refresh_window = false;
238 w.flush();
239 w.refresh();
243 void Lyrics::switchTo()
245 using Global::myScreen;
246 if (myScreen != this)
248 SwitchTo::execute(this);
249 m_scroll_begin = 0;
250 drawHeader();
252 else
253 switchToPreviousScreen();
256 std::wstring Lyrics::title()
258 std::wstring result = L"Lyrics: ";
259 result += Scroller(
260 Format::stringify<wchar_t>(Format::parse(L"{%a - %t}|{%f}"), &m_song),
261 m_scroll_begin,
262 COLS - result.length() - (Config.design == Design::Alternative
264 : Global::VolumeState.length()));
265 return result;
268 void Lyrics::fetch(const MPD::Song &s)
270 if (!m_worker.valid() || s != m_song)
272 w.clear();
273 m_song = s;
274 if (loadLyrics(w, lyricsFilename(m_song)))
276 clearWorker();
277 m_refresh_window = true;
279 else
281 m_shared_buffer = std::make_shared<Shared<NC::Buffer>>();
282 m_worker = std::async(
283 std::launch::async,
284 std::bind(downloadLyrics, m_song, m_shared_buffer, m_fetcher));
289 void Lyrics::refetchCurrent()
291 std::string filename = lyricsFilename(m_song);
292 if (std::remove(filename.c_str()) == -1 && errno != ENOENT)
294 const char msg[] = "Couldn't remove \"%1%\": %2%";
295 Statusbar::printf(msg, wideShorten(filename, COLS - const_strlen(msg) - 25),
296 strerror(errno));
298 else
300 clearWorker();
301 fetch(m_song);
305 void Lyrics::edit()
307 if (Config.external_editor.empty())
309 Statusbar::print("external_editor variable has to be set in configuration file");
310 return;
313 Statusbar::print("Opening lyrics in external editor...");
315 GNUC_UNUSED int res;
316 std::string command;
317 std::string filename = lyricsFilename(m_song);
318 if (Config.use_console_editor)
320 command = "/bin/sh -c \"" + Config.external_editor + " \\\"" + filename + "\\\"\"";
321 res = system(command.c_str());
322 fetch(m_song);
323 // Reset ncurses state to refresh the screen.
324 endwin();
325 initscr();
326 curs_set(0);
328 else
330 command = "nohup " + Config.external_editor
331 + " \"" + filename + "\" > /dev/null 2>&1 &";
332 res = system(command.c_str());
336 void Lyrics::toggleFetcher()
338 if (m_fetcher != nullptr)
340 auto fetcher = std::find_if(Config.lyrics_fetchers.begin(),
341 Config.lyrics_fetchers.end(),
342 [this](auto &f) { return f.get() == m_fetcher; });
343 assert(fetcher != Config.lyrics_fetchers.end());
344 ++fetcher;
345 if (fetcher != Config.lyrics_fetchers.end())
346 m_fetcher = fetcher->get();
347 else
348 m_fetcher = nullptr;
350 else
352 assert(!Config.lyrics_fetchers.empty());
353 m_fetcher = Config.lyrics_fetchers[0].get();
356 if (m_fetcher != nullptr)
357 Statusbar::printf("Using lyrics fetcher: %s", m_fetcher->name());
358 else
359 Statusbar::print("Using all lyrics fetchers");
362 void Lyrics::fetchInBackground(const MPD::Song &s, bool notify_)
364 auto consumer_impl = [this] {
365 std::string lyrics_file;
366 while (true)
368 ConsumerState::Song cs;
370 auto consumer = m_consumer_state.acquire();
371 assert(consumer->running);
372 if (consumer->songs.empty())
374 consumer->running = false;
375 break;
377 lyrics_file = lyricsFilename(consumer->songs.front().song());
378 if (!boost::filesystem::exists(lyrics_file))
380 cs = consumer->songs.front();
381 if (cs.notify())
383 consumer->message = "Fetching lyrics for \""
384 + Format::stringify<char>(Config.song_status_format, &cs.song())
385 + "\"...";
388 consumer->songs.pop();
390 if (!cs.song().empty())
392 auto lyrics = downloadLyrics(cs.song(), nullptr, m_fetcher);
393 if (lyrics)
394 saveLyrics(lyrics_file, *lyrics);
399 auto consumer = m_consumer_state.acquire();
400 consumer->songs.emplace(s, notify_);
401 // Start the consumer if it's not running.
402 if (!consumer->running)
404 std::thread t(consumer_impl);
405 t.detach();
406 consumer->running = true;
410 boost::optional<std::string> Lyrics::tryTakeConsumerMessage()
412 boost::optional<std::string> result;
413 auto consumer = m_consumer_state.acquire();
414 if (consumer->message)
416 result = std::move(consumer->message);
417 consumer->message = boost::none;
419 return result;
422 void Lyrics::clearWorker()
424 m_shared_buffer.reset();
425 m_worker = std::future<boost::optional<std::string>>();