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