1 /****************************************************************************************
2 * Copyright (c) 2007 Leo Franchi <lfranchi@kde.org> *
3 * Copyright (c) 2009 Seb Ruiz <ruiz@kde.org> *
4 * *
5 * This program is free software; you can redistribute it and/or modify it under *
6 * the terms of the GNU General Public License as published by the Free Software *
7 * Foundation; either version 2 of the License, or (at your option) any later *
8 * version. *
9 * *
10 * This program is distributed in the hope that it will be useful, but WITHOUT ANY *
11 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
12 * PARTICULAR PURPOSE. See the GNU General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU General Public License along with *
15 * this program. If not, see <http://www.gnu.org/licenses/>. *
16 ****************************************************************************************/
17
18 #define DEBUG_PREFIX "LyricsManager"
19
20 #include "LyricsManager.h"
21
22 #include "EngineController.h"
23 #include "core/meta/Meta.h"
24 #include "core/support/Debug.h"
25 #include "core-impl/collections/support/CollectionManager.h"
26
27 #include <QDomDocument>
28 #include <QTextEdit>
29 #include <QXmlStreamReader>
30
31 #include <KLocalizedString>
32
33
34 #define APIURL "https://lyrics.fandom.com/api.php?action=query&prop=revisions&rvprop=content&format=xml&titles="
35
36
37 LyricsManager* LyricsManager::s_self = nullptr;
38
LyricsManager()39 LyricsManager::LyricsManager()
40 {
41 s_self = this;
42 connect( The::engineController(), &EngineController::trackChanged, this, &LyricsManager::newTrack );
43 }
44
45 void
newTrack(const Meta::TrackPtr & track)46 LyricsManager::newTrack( const Meta::TrackPtr &track )
47 {
48 loadLyrics( track );
49 }
50
51 void
lyricsResult(const QByteArray & lyricsXML,Meta::TrackPtr track)52 LyricsManager::lyricsResult( const QByteArray& lyricsXML, Meta::TrackPtr track ) //SLOT
53 {
54 DEBUG_BLOCK
55
56 QXmlStreamReader xml( lyricsXML );
57 while( !xml.atEnd() )
58 {
59 xml.readNext();
60
61 if( xml.name() == QStringLiteral("lyric") || xml.name() == QStringLiteral( "lyrics" ) )
62 {
63 QString lyrics( xml.readElementText() );
64 if( !isEmpty( lyrics ) )
65 {
66 // overwrite cached lyrics (as either there were no lyrics available previously OR
67 // the user explicitly agreed to overwrite the lyrics)
68 debug() << "setting cached lyrics...";
69 track->setCachedLyrics( lyrics ); // TODO: setLyricsByPath?
70 Q_EMIT newLyrics( track );
71 }
72 else
73 {
74 ::error() << i18n("Retrieved lyrics is empty");
75 return;
76 }
77 }
78 else if( xml.name() == QLatin1String("suggestions") )
79 {
80 QVariantList suggestions;
81 while( xml.readNextStartElement() )
82 {
83 if( xml.name() != QLatin1String("suggestion") )
84 continue;
85
86 const QXmlStreamAttributes &a = xml.attributes();
87
88 QString artist = a.value( QLatin1String("artist") ).toString();
89 QString title = a.value( QLatin1String("title") ).toString();
90 QString url = a.value( QLatin1String("url") ).toString();
91
92 if( !url.isEmpty() )
93 suggestions << ( QStringList() << title << artist << url );
94
95 xml.skipCurrentElement();
96 }
97
98 debug() << "got" << suggestions.size() << "suggestions";
99
100 if( !suggestions.isEmpty() )
101 Q_EMIT newSuggestions( suggestions );
102
103 return;
104 }
105 }
106
107 if( xml.hasError() )
108 {
109 warning() << "errors occurred during reading lyrics xml result:" << xml.errorString();
110 Q_EMIT error( i18n("Lyrics data could not be parsed") );
111 }
112 }
113
loadLyrics(Meta::TrackPtr track,bool overwrite)114 void LyricsManager::loadLyrics( Meta::TrackPtr track, bool overwrite )
115 {
116 DEBUG_BLOCK
117
118 if( !track )
119 {
120 debug() << "no current track";
121 return;
122 }
123
124 // -- get current title and artist
125 QString title = track->name();
126 QString artist = track->artist() ? track->artist()->name() : QString();
127
128 sanitizeTitle( title );
129 sanitizeArtist( artist );
130
131 if( !isEmpty( track->cachedLyrics() ) && !overwrite )
132 {
133 debug() << "Lyrics already cached.";
134 return;
135 }
136
137 QUrl url( APIURL + artist + QLatin1Char(':') + title );
138 m_trackMap.insert( url, track );
139
140 connect( NetworkAccessManagerProxy::instance(), &NetworkAccessManagerProxy::requestRedirectedUrl,
141 this, &LyricsManager::updateRedirectedUrl);
142
143 NetworkAccessManagerProxy::instance()->getData( url, this, &LyricsManager::lyricsLoaded );
144 }
145
lyricsLoaded(const QUrl & url,const QByteArray & data,const NetworkAccessManagerProxy::Error & err)146 void LyricsManager::lyricsLoaded( const QUrl& url, const QByteArray& data, const NetworkAccessManagerProxy::Error &err )
147 {
148 DEBUG_BLOCK
149
150 if( err.code )
151 {
152 warning() << "A network error occurred:" << err.description;
153 return;
154 }
155
156 Meta::TrackPtr track = m_trackMap.take( url );
157 if( !track )
158 {
159 warning() << "No track belongs to this url:" << url.url();
160 return;
161 }
162
163 QDomDocument document;
164 document.setContent( data );
165 auto list = document.elementsByTagName( QStringLiteral( "rev" ) );
166 if( list.isEmpty() )
167 {
168 if( track->album() && track->album()->albumArtist() )
169 {
170 QString albumArtist = track->album()->albumArtist()->name();
171 QString artist = track->artist() ? track->artist()->name() : QString();
172 QString title = track->name();
173 sanitizeTitle( title );
174 sanitizeArtist( artist );
175 sanitizeArtist( albumArtist );
176
177 //Try with album artist
178 if( url == QUrl( APIURL + artist + QLatin1Char(':') + title ) && albumArtist != artist )
179 {
180 debug() << "Try again with album artist.";
181
182 QUrl newUrl( APIURL + albumArtist + QLatin1Char(':') + title );
183 m_trackMap.insert( newUrl, track );
184 NetworkAccessManagerProxy::instance()->getData( newUrl, this, &LyricsManager::lyricsLoaded );
185 return;
186 }
187 }
188
189 debug() << "No lyrics found for track:" << track->name();
190 return;
191 }
192
193 QString rev = list.at( 0 ).toElement().text();
194 if( rev.contains( QStringLiteral( "lyrics" ) ) )
195 {
196 int lindex = rev.indexOf( QStringLiteral( "<lyrics>" ) );
197 int rindex = rev.indexOf( QStringLiteral( "</lyrics>" ) );
198 lyricsResult( (rev.mid( lindex, rindex - lindex ) + "</lyrics>" ).toUtf8(), track );
199 }
200 else if( rev.contains( QStringLiteral( "lyric" ) ) )
201 {
202 int lindex = rev.indexOf( QStringLiteral( "<lyric>" ) );
203 int rindex = rev.indexOf( QStringLiteral( "</lyric>" ) );
204 lyricsResult( (rev.mid( lindex, rindex - lindex ) + "</lyric>" ).toUtf8(), track );
205 }
206 else if( rev.contains( QStringLiteral( "#REDIRECT" ) ) )
207 {
208 debug() << "Redirect:" << data;
209
210 int lindex = rev.indexOf( QStringLiteral( "#REDIRECT [[" ) ) + 12;
211 int rindex = rev.indexOf( QStringLiteral( "]]" ) );
212 QStringList list = rev.mid( lindex, rindex - lindex ).split( QLatin1Char(':') );
213 if( list.size() == 2 )
214 {
215 list[0] = list[0].replace( '&', QStringLiteral( "%26" ) );
216 list[1] = list[1].replace( '&', QStringLiteral( "%26" ) );
217 QUrl newUrl( APIURL + list.join( QLatin1Char(':') ) );
218 m_trackMap.insert( newUrl, track );
219 NetworkAccessManagerProxy::instance()->getData( newUrl, this, &LyricsManager::lyricsLoaded );
220 }
221 }
222 else
223 warning() << "No lyrics found in data:" << data;
224 }
225
sanitizeTitle(QString & title)226 void LyricsManager::sanitizeTitle( QString& title )
227 {
228 const QString magnatunePreviewString = QStringLiteral( "PREVIEW: buy it at www.magnatune.com" );
229
230 if( title.contains(magnatunePreviewString, Qt::CaseSensitive) )
231 title = title.remove( " (" + magnatunePreviewString + ')' );
232
233 title = title.remove( QStringLiteral( "(Live)" ) );
234 title = title.remove( QStringLiteral( "(live)" ) );
235 title = title.replace( '`', QStringLiteral( "'" ) );
236 title = title.replace( '&', QStringLiteral( "%26" ) );
237 }
238
sanitizeArtist(QString & artist)239 void LyricsManager::sanitizeArtist( QString& artist )
240 {
241 const QString magnatunePreviewString = QStringLiteral( "PREVIEW: buy it at www.magnatune.com" );
242
243 if( artist.contains(magnatunePreviewString, Qt::CaseSensitive) )
244 artist = artist.remove( " (" + magnatunePreviewString + ')' );
245
246 // strip "featuring <someone else>" from the artist
247 int strip = artist.toLower().indexOf( QLatin1String(" ft. "));
248 if ( strip != -1 )
249 artist = artist.mid( 0, strip );
250
251 strip = artist.toLower().indexOf( QLatin1String(" feat. ") );
252 if ( strip != -1 )
253 artist = artist.mid( 0, strip );
254
255 strip = artist.toLower().indexOf( QLatin1String(" featuring ") );
256 if ( strip != -1 )
257 artist = artist.mid( 0, strip );
258
259 artist = artist.replace( '`', QStringLiteral( "'" ) );
260 artist = artist.replace( '&', QStringLiteral( "%26" ) );
261 }
262
isEmpty(const QString & lyrics) const263 bool LyricsManager::isEmpty( const QString &lyrics ) const
264 {
265 QTextEdit testItem;
266
267 // Set the text of the TextItem.
268 if( Qt::mightBeRichText( lyrics ) )
269 testItem.setHtml( lyrics );
270 else
271 testItem.setPlainText( lyrics );
272
273 // Get the plaintext content.
274 // We use toPlainText() to strip all Html formatting,
275 // so we can test if there's any text given.
276 QString testText = testItem.toPlainText().trimmed();
277
278 return testText.isEmpty();
279 }
280
updateRedirectedUrl(const QUrl & oldUrl,const QUrl & newUrl)281 void LyricsManager::updateRedirectedUrl(const QUrl& oldUrl, const QUrl& newUrl)
282 {
283 if( m_trackMap.contains( oldUrl ) && !m_trackMap.contains( newUrl ) )
284 {
285 // Get track for the old URL.
286 Meta::TrackPtr track = m_trackMap.value( oldUrl );
287
288 // Replace with redirected url for correct lookup
289 m_trackMap.insert( newUrl, track );
290 m_trackMap.remove( oldUrl );
291 }
292 }
293