1 /***************************************************************************
2 * Copyright (C) 2008-2021 by Andrzej Rybczak *
3 * andrzej@rybczak.net *
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 ***************************************************************************/
20
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>
29
30 #include "curses/scrollpad.h"
31 #include "screens/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 "macro_utilities.h"
38 #include "screens/lyrics.h"
39 #include "screens/playlist.h"
40 #include "settings.h"
41 #include "song.h"
42 #include "statusbar.h"
43 #include "title.h"
44 #include "screens/screen_switcher.h"
45 #include "utility/string.h"
46
47 using Global::MainHeight;
48 using Global::MainStartY;
49
50 Lyrics *myLyrics;
51
52 namespace {
53
removeExtension(std::string filename)54 std::string removeExtension(std::string filename)
55 {
56 size_t dot = filename.rfind('.');
57 if (dot != std::string::npos)
58 filename.resize(dot);
59 return filename;
60 }
61
lyricsFilename(const MPD::Song & s)62 std::string lyricsFilename(const MPD::Song &s)
63 {
64 std::string filename;
65 if (Config.store_lyrics_in_song_dir && !s.isStream())
66 {
67 if (s.isFromDatabase())
68 filename = Config.mpd_music_dir + "/";
69 filename += removeExtension(s.getURI());
70 removeExtension(filename);
71 }
72 else
73 {
74 std::string artist = s.getArtist();
75 std::string title = s.getTitle();
76 if (artist.empty() || title.empty())
77 filename = removeExtension(s.getName());
78 else
79 filename = artist + " - " + title;
80 removeInvalidCharsFromFilename(filename, Config.generate_win32_compatible_filenames);
81 filename = Config.lyrics_directory + "/" + filename;
82 }
83 filename += ".txt";
84 return filename;
85 }
86
loadLyrics(NC::Scrollpad & w,const std::string & filename)87 bool loadLyrics(NC::Scrollpad &w, const std::string &filename)
88 {
89 std::ifstream input(filename);
90 if (input.is_open())
91 {
92 std::string line;
93 bool first_line = true;
94 while (std::getline(input, line))
95 {
96 // Remove carriage returns as they mess up the display.
97 boost::remove_erase(line, '\r');
98 if (!first_line)
99 w << '\n';
100 w << Charset::utf8ToLocale(line);
101 first_line = false;
102 }
103 return true;
104 }
105 else
106 return false;
107 }
108
saveLyrics(const std::string & filename,const std::string & lyrics)109 bool saveLyrics(const std::string &filename, const std::string &lyrics)
110 {
111 std::ofstream output(filename);
112 if (output.is_open())
113 {
114 output << lyrics;
115 output.close();
116 return true;
117 }
118 else
119 return false;
120 }
121
downloadLyrics(const MPD::Song & s,std::shared_ptr<Shared<NC::Buffer>> shared_buffer,std::shared_ptr<std::atomic<bool>> download_stopper,LyricsFetcher * current_fetcher)122 boost::optional<std::string> downloadLyrics(
123 const MPD::Song &s,
124 std::shared_ptr<Shared<NC::Buffer>> shared_buffer,
125 std::shared_ptr<std::atomic<bool>> download_stopper,
126 LyricsFetcher *current_fetcher)
127 {
128 std::string s_artist = s.getArtist();
129 std::string s_title = s.getTitle();
130 // If artist or title is empty, use filename. This should give reasonable
131 // results for google search based lyrics fetchers.
132 if (s_artist.empty() || s_title.empty())
133 {
134 s_artist.clear();
135 s_title = s.getName();
136 // Get rid of underscores to improve search results.
137 std::replace_if(s_title.begin(), s_title.end(), boost::is_any_of("-_"), ' ');
138 size_t dot = s_title.rfind('.');
139 if (dot != std::string::npos)
140 s_title.resize(dot);
141 }
142
143 auto fetch_lyrics = [&](auto &fetcher_) {
144 {
145 if (shared_buffer)
146 {
147 auto buf = shared_buffer->acquire();
148 *buf << "Fetching lyrics from "
149 << NC::Format::Bold
150 << fetcher_->name()
151 << NC::Format::NoBold << "... ";
152 }
153 }
154 auto result_ = fetcher_->fetch(s_artist, s_title);
155 if (result_.first == false)
156 {
157 if (shared_buffer)
158 {
159 auto buf = shared_buffer->acquire();
160 *buf << NC::Color::Red
161 << result_.second
162 << NC::Color::End
163 << '\n';
164 }
165 }
166 return result_;
167 };
168
169 LyricsFetcher::Result fetcher_result;
170 if (current_fetcher == nullptr)
171 {
172 for (auto &fetcher : Config.lyrics_fetchers)
173 {
174 if (download_stopper && download_stopper->load())
175 return boost::none;
176 fetcher_result = fetch_lyrics(fetcher);
177 if (fetcher_result.first)
178 break;
179 }
180 }
181 else
182 fetcher_result = fetch_lyrics(current_fetcher);
183
184 boost::optional<std::string> result;
185 if (fetcher_result.first)
186 result = std::move(fetcher_result.second);
187 return result;
188 }
189
190 }
191
Lyrics()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)
197 { }
198
resize()199 void Lyrics::resize()
200 {
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;
206 }
207
update()208 void Lyrics::update()
209 {
210 if (m_worker.valid())
211 {
212 auto buffer = m_shared_buffer->acquire();
213 if (!buffer->empty())
214 {
215 w << *buffer;
216 buffer->clear();
217 m_refresh_window = true;
218 }
219
220 if (m_worker.is_ready())
221 {
222 auto lyrics = m_worker.get();
223 if (lyrics)
224 {
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));
231 }
232 else
233 w << "\nLyrics were not found.\n";
234 clearWorker();
235 m_refresh_window = true;
236 }
237 }
238
239 if (m_refresh_window)
240 {
241 m_refresh_window = false;
242 w.flush();
243 w.refresh();
244 }
245 }
246
switchTo()247 void Lyrics::switchTo()
248 {
249 using Global::myScreen;
250 if (myScreen != this)
251 {
252 SwitchTo::execute(this);
253 m_scroll_begin = 0;
254 drawHeader();
255 }
256 else
257 switchToPreviousScreen();
258 }
259
title()260 std::wstring Lyrics::title()
261 {
262 std::wstring result = L"Lyrics";
263 if (!m_song.empty())
264 {
265 result += L": ";
266 result += Scroller(
267 Format::stringify<wchar_t>(Format::parse(L"{%a - %t}|{%f}"), &m_song),
268 m_scroll_begin,
269 COLS - result.length() - (Config.design == Design::Alternative
270 ? 2
271 : Global::VolumeState.length()));
272 }
273 return result;
274 }
275
fetch(const MPD::Song & s)276 void Lyrics::fetch(const MPD::Song &s)
277 {
278 if (!m_worker.valid() || s != m_song)
279 {
280 stopDownload();
281 w.clear();
282 w.reset();
283 m_song = s;
284 if (loadLyrics(w, lyricsFilename(m_song)))
285 {
286 clearWorker();
287 m_refresh_window = true;
288 }
289 else
290 {
291 m_download_stopper = std::make_shared<std::atomic<bool>>(false);
292 m_shared_buffer = std::make_shared<Shared<NC::Buffer>>();
293 m_worker = boost::async(
294 boost::launch::async,
295 std::bind(downloadLyrics,
296 m_song, m_shared_buffer, m_download_stopper, m_fetcher));
297 }
298 }
299 }
300
refetchCurrent()301 void Lyrics::refetchCurrent()
302 {
303 std::string filename = lyricsFilename(m_song);
304 if (std::remove(filename.c_str()) == -1 && errno != ENOENT)
305 {
306 const char msg[] = "Couldn't remove \"%1%\": %2%";
307 Statusbar::printf(msg, wideShorten(filename, COLS - const_strlen(msg) - 25),
308 strerror(errno));
309 }
310 else
311 {
312 clearWorker();
313 fetch(m_song);
314 }
315 }
316
edit()317 void Lyrics::edit()
318 {
319 if (Config.external_editor.empty())
320 {
321 Statusbar::print("external_editor variable has to be set in configuration file");
322 return;
323 }
324
325 Statusbar::print("Opening lyrics in external editor...");
326
327 std::string filename = lyricsFilename(m_song);
328 escapeSingleQuotes(filename);
329 if (Config.use_console_editor)
330 {
331 runExternalConsoleCommand(Config.external_editor + " '" + filename + "'");
332 fetch(m_song);
333 }
334 else
335 runExternalCommand(Config.external_editor + " '" + filename + "'", false);
336 }
337
toggleFetcher()338 void Lyrics::toggleFetcher()
339 {
340 if (m_fetcher != nullptr)
341 {
342 auto fetcher = std::find_if(Config.lyrics_fetchers.begin(),
343 Config.lyrics_fetchers.end(),
344 [this](auto &f) { return f.get() == m_fetcher; });
345 assert(fetcher != Config.lyrics_fetchers.end());
346 ++fetcher;
347 if (fetcher != Config.lyrics_fetchers.end())
348 m_fetcher = fetcher->get();
349 else
350 m_fetcher = nullptr;
351 }
352 else
353 {
354 assert(!Config.lyrics_fetchers.empty());
355 m_fetcher = Config.lyrics_fetchers[0].get();
356 }
357
358 if (m_fetcher != nullptr)
359 Statusbar::printf("Using lyrics fetcher: %s", m_fetcher->name());
360 else
361 Statusbar::print("Using all lyrics fetchers");
362 }
363
fetchInBackground(const MPD::Song & s,bool notify_)364 void Lyrics::fetchInBackground(const MPD::Song &s, bool notify_)
365 {
366 auto consumer_impl = [this] {
367 std::string lyrics_file;
368 while (true)
369 {
370 ConsumerState::Song cs;
371 {
372 auto consumer = m_consumer_state.acquire();
373 assert(consumer->running);
374 if (consumer->songs.empty())
375 {
376 consumer->running = false;
377 break;
378 }
379 lyrics_file = lyricsFilename(consumer->songs.front().song());
380 if (!boost::filesystem::exists(lyrics_file))
381 {
382 cs = consumer->songs.front();
383 if (cs.notify())
384 {
385 consumer->message = "Fetching lyrics for \""
386 + Format::stringify<char>(Config.song_status_format, &cs.song())
387 + "\"...";
388 }
389 }
390 consumer->songs.pop();
391 }
392 if (!cs.song().empty())
393 {
394 auto lyrics = downloadLyrics(cs.song(), nullptr, nullptr, m_fetcher);
395 if (lyrics)
396 saveLyrics(lyrics_file, *lyrics);
397 }
398 }
399 };
400
401 auto consumer = m_consumer_state.acquire();
402 consumer->songs.emplace(s, notify_);
403 // Start the consumer if it's not running.
404 if (!consumer->running)
405 {
406 std::thread t(consumer_impl);
407 t.detach();
408 consumer->running = true;
409 }
410 }
411
tryTakeConsumerMessage()412 boost::optional<std::string> Lyrics::tryTakeConsumerMessage()
413 {
414 boost::optional<std::string> result;
415 auto consumer = m_consumer_state.acquire();
416 if (consumer->message)
417 {
418 result = std::move(consumer->message);
419 consumer->message = boost::none;
420 }
421 return result;
422 }
423
clearWorker()424 void Lyrics::clearWorker()
425 {
426 m_shared_buffer.reset();
427 m_worker = boost::BOOST_THREAD_FUTURE<boost::optional<std::string>>();
428 }
429
stopDownload()430 void Lyrics::stopDownload()
431 {
432 if (m_download_stopper)
433 m_download_stopper->store(true);
434 }
435