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