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