1 /* LookupThread.cpp */
2 
3 /* Copyright (C) 2011-2020 Michael Lugmair (Lucio Carreras)
4  *
5  * This file is part of sayonara player
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11 
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16 
17  * You should have received a copy of the GNU General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 /*
22  * LookupThread.cpp
23  *
24  *  Created on: May 21, 2011
25  *      Author: Michael Lugmair (Lucio Carreras)
26  */
27 
28 #include "LyricLookup.h"
29 #include "LyricServer.h"
30 #include "LyricWebpageParser.h"
31 #include "LyricServerJsonWriter.h"
32 
33 #include "Utils/WebAccess/AsyncWebAccess.h"
34 #include "Utils/Algorithm.h"
35 #include "Utils/Logger/Logger.h"
36 #include "Utils/StandardPaths.h"
37 
38 #include <QStringList>
39 #include <QRegExp>
40 #include <QMap>
41 #include <QFile>
42 #include <QDir>
43 
44 using namespace Lyrics;
45 
46 namespace
47 {
extractUrlFromSearchResults(Server * server,const QString & website)48 	QString extractUrlFromSearchResults(Server* server, const QString& website)
49 	{
50 		if(website.isEmpty())
51 		{
52 			return QString();
53 		}
54 
55 		QString url;
56 
57 		auto re = QRegExp(server->searchResultRegex());
58 		re.setMinimal(true);
59 		if(re.indexIn(website) > 0)
60 		{
61 			const auto parsedUrl = re.cap(1);
62 			url = server->searchResultUrlTemplate();
63 			url.replace("<SERVER>", server->address());
64 			url.replace("<SEARCH_RESULT_CAPTION>", parsedUrl);
65 		}
66 
67 		return url;
68 	}
69 
addServer(Server * server,QList<Server * > & servers)70 	void addServer(Server* server, QList<Server*>& servers)
71 	{
72 		if(!server || (!server->canFetchDirectly() && !server->canSearch()))
73 		{
74 			return;
75 		}
76 
77 		const auto name = server->name();
78 		const auto index = Util::Algorithm::indexOf(servers, [&](auto* s) {
79 			return (s->name() == name);
80 		});
81 
82 		const auto found = (index >= 0);
83 		if(found)
84 		{
85 			servers.replace(index, server);
86 		}
87 
88 		else
89 		{
90 			servers << server;
91 		}
92 	}
93 
calcUrl(Server * server,const QString & urlTemplate,const QString & artist,const QString & song)94 	QString calcUrl(Server* server, const QString& urlTemplate, const QString& artist, const QString& song)
95 	{
96 		const auto replacedArtist = Server::applyReplacements(artist, server->replacements());
97 		const auto replacedSong = Server::applyReplacements(song, server->replacements());
98 
99 		auto url = urlTemplate;
100 		url.replace("<SERVER>", server->address());
101 		url.replace("<FIRST_ARTIST_LETTER>", QString(replacedArtist).trimmed());
102 		url.replace("<ARTIST>", replacedArtist.trimmed());
103 		url.replace("<TITLE>", replacedSong.trimmed());
104 
105 		return server->isLowercase()
106 		       ? url.toLower()
107 		       : url;
108 	}
109 
calcSearchUrl(Server * server,const QString & artist,const QString & song)110 	QString calcSearchUrl(Server* server, const QString& artist, const QString& song)
111 	{
112 		return calcUrl(server, server->searchUrlTemplate(), artist, song);
113 	}
114 
calcServerUrl(Server * server,const QString & artist,const QString & song)115 	QString calcServerUrl(Server* server, const QString& artist, const QString& song)
116 	{
117 		return calcUrl(server, server->directUrlTemplate(), artist, song);
118 	}
119 
getLyricHeader(const QString & artist,const QString & title,const QString & serverName,const QString & url)120 	QString getLyricHeader(const QString& artist, const QString& title, const QString& serverName, const QString& url)
121 	{
122 		return QString("<b>%1 - %2</b><br>%3: %4")
123 			.arg(artist)
124 			.arg(title)
125 			.arg(serverName)
126 			.arg(url);
127 	}
128 }
129 
130 struct LookupThread::Private
131 {
132 	QString artist;
133 	QString title;
134 
135 	QList<Server*> servers;
136 	QString lyricsData;
137 	QMap<QString, QString> regexConversions;
138 	QString lyricHeader;
139 
140 	AsyncWebAccess* currentAwa;
141 	int currentServerIndex;
142 	bool hasError;
143 
PrivateLookupThread::Private144 	Private() :
145 		currentAwa {nullptr},
146 		currentServerIndex {-1},
147 		hasError {false}
148 	{
149 		regexConversions =
150 			{{"$", "\\$"},
151 			 {"*", "\\*"},
152 			 {"+", "\\+"},
153 			 {"?", "\\?"},
154 			 {"[", "\\["},
155 			 {"]", "\\]"},
156 			 {"(", "\\("},
157 			 {")", "\\)"},
158 			 {"{", "\\{"},
159 			 {"}", "\\}"},
160 			 {"^", "\\^"},
161 			 {"|", "\\|"},
162 			 {".", "\\."}};
163 	}
164 };
165 
LookupThread(QObject * parent)166 LookupThread::LookupThread(QObject* parent) :
167 	QObject(parent)
168 {
169 	m = Pimpl::make<LookupThread::Private>();
170 
171 	initServerList();
172 }
173 
174 LookupThread::~LookupThread() = default;
175 
run(const QString & artist,const QString & title,int serverIndex)176 void LookupThread::run(const QString& artist, const QString& title, int serverIndex)
177 {
178 	m->artist = artist;
179 	m->title = title;
180 
181 	m->currentServerIndex = std::max(0, serverIndex);
182 	m->currentServerIndex = std::min(serverIndex, m->servers.size() - 1);
183 
184 	if(m->artist.isEmpty() && m->title.isEmpty())
185 	{
186 		m->lyricsData = "No track selected";
187 		return;
188 	}
189 
190 	m->lyricsData.clear();
191 
192 	auto* server = m->servers[m->currentServerIndex];
193 	if(server->canFetchDirectly())
194 	{
195 		const auto url = calcServerUrl(server, artist, title);
196 		callWebsite(url);
197 	}
198 
199 	else if(server->canSearch())
200 	{
201 		const auto url = calcSearchUrl(server, artist, title);
202 		startSearch(url);
203 	}
204 
205 	else
206 	{
207 		spLog(Log::Warning, this) << "Search server " << server->name() << " cannot do anything at all!";
208 		emit sigFinished();
209 	}
210 }
211 
startSearch(const QString & url)212 void LookupThread::startSearch(const QString& url)
213 {
214 	spLog(Log::Debug, this) << "Search Lyrics from " << url;
215 
216 	auto* awa = new AsyncWebAccess(this, QByteArray(), AsyncWebAccess::Behavior::AsBrowser);
217 	connect(awa, &AsyncWebAccess::sigFinished, this, &LookupThread::searchFinished);
218 	awa->run(url);
219 }
220 
searchFinished()221 void LookupThread::searchFinished()
222 {
223 	auto* awa = static_cast<AsyncWebAccess*>(sender());
224 
225 	auto* server = m->servers[m->currentServerIndex];
226 
227 	const auto data = awa->data();
228 	const auto url = extractUrlFromSearchResults(server, QString::fromLocal8Bit(data));
229 
230 	if(!url.isEmpty())
231 	{
232 		callWebsite(url);
233 	}
234 
235 	else
236 	{
237 		spLog(Log::Debug, this) << "Search Lyrics not successful ";
238 		m->lyricsData = tr("Cannot fetch lyrics from %1").arg(awa->url());
239 		m->hasError = true;
240 		emit sigFinished();
241 	}
242 
243 	awa->deleteLater();
244 }
245 
callWebsite(const QString & url)246 void LookupThread::callWebsite(const QString& url)
247 {
248 	stop();
249 
250 	spLog(Log::Debug, this) << "Fetch Lyrics from " << url;
251 
252 	m->currentAwa = new AsyncWebAccess(this);
253 	connect(m->currentAwa, &AsyncWebAccess::sigFinished, this, &LookupThread::contentFetched);
254 	m->currentAwa->run(url);
255 }
256 
contentFetched()257 void LookupThread::contentFetched()
258 {
259 	auto* awa = static_cast<AsyncWebAccess*>(sender());
260 	auto* server = m->servers[m->currentServerIndex];
261 
262 	m->currentAwa = nullptr;
263 	m->lyricHeader = getLyricHeader(m->artist, m->title, server->name(), awa->url());
264 
265 	m->hasError = (!awa->hasData() || awa->hasError());
266 	if(m->hasError)
267 	{
268 		m->lyricsData = tr("Cannot fetch lyrics from %1").arg(awa->url());
269 	}
270 
271 	else if(awa->data().isEmpty())
272 	{
273 		m->lyricsData = tr("No lyrics found") + "<br />" + awa->url();
274 	}
275 
276 	else
277 	{
278 		m->lyricsData = Lyrics::WebpageParser::parseWebpage(awa->data(), m->regexConversions, server);
279 	}
280 
281 	awa->deleteLater();
282 	emit sigFinished();
283 }
284 
stop()285 void LookupThread::stop()
286 {
287 	if(m->currentAwa)
288 	{
289 		disconnect(m->currentAwa, &AsyncWebAccess::sigFinished, this, &LookupThread::contentFetched);
290 		m->currentAwa->stop();
291 	}
292 }
293 
hasError() const294 bool LookupThread::hasError() const
295 {
296 	return m->hasError;
297 }
298 
initServerList()299 void LookupThread::initServerList()
300 {
301 	// motörhead
302 	// crosby, stills & nash
303 	// guns 'n' roses
304 	// AC/DC
305 	// the doors
306 	// the rolling stones
307 	// petr nalitch
308 	// eric burdon and the animals
309 	// Don't speak
310 
311 	const auto servers = Lyrics::ServerJsonReader::parseJsonFile(":/lyrics/lyrics.json");
312 	for(auto* server : servers)
313 	{
314 		addServer(server, m->servers);
315 	}
316 
317 	initCustomServers();
318 
319 	m->currentServerIndex = 0;
320 }
321 
initCustomServers()322 void LookupThread::initCustomServers()
323 {
324 	const auto lyricsPath = Util::lyricsPath();
325 	const auto dir = QDir(lyricsPath);
326 	const auto jsonFiles = dir.entryList(QStringList {"*.json"}, QDir::Files);
327 
328 	for(auto jsonFile : jsonFiles)
329 	{
330 		jsonFile.prepend(lyricsPath + "/");
331 		auto servers = Lyrics::ServerJsonReader::parseJsonFile(jsonFile);
332 		for(auto* server : servers)
333 		{
334 			addServer(server, m->servers);
335 		}
336 	}
337 }
338 
servers() const339 QStringList LookupThread::servers() const
340 {
341 	QStringList serverName;
342 	Util::Algorithm::transform(m->servers, serverName, [](const auto* server) {
343 		return server->name();
344 	});
345 
346 	return serverName;
347 }
348 
lyricHeader() const349 QString LookupThread::lyricHeader() const
350 {
351 	return m->lyricHeader;
352 }
353 
lyricData() const354 QString LookupThread::lyricData() const
355 {
356 	return m->lyricsData;
357 }
358