Files
cantata/gui/lyrics.cpp
2011-11-27 15:24:05 +00:00

327 lines
11 KiB
C++

/**************************************************************************
* Converted to C++ from the Amarok lyrics script...
*
* Amarok 2 lyrics script to fetch lyrics from lyrics.wikia.com *
* (formerly lyricwiki.org) *
* *
* Copyright *
* (C) 2008 Aaron Reichman <reldruh@gmail.com> *
* (C) 2008 Leo Franchi <lfranchi@kde.org> *
* (C) 2008 Mark Kretschmann <kretschmann@kde.org> *
* (C) 2008 Peter ZHOU <peterzhoulei@gmail.org> *
* (C) 2009 Jakob Kummerow <jakob.kummerow@gmail.com> *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
**************************************************************************/
#include "lyrics.h"
#include "musiclibrarymodel.h"
#include "lib/song.h"
#ifdef ENABLE_KDE_SUPPORT
#include <KDE/KGlobal>
#include <KDE/KLocale>
#include <KDE/KIO/AccessManager>
#endif
#include <QtXml/QDomDocument>
#include <QtGui/QTextDocument>
#include <QtCore/QDebug>
#include <QtCore/QFile>
#include <QtCore/QDir>
#include <QtCore/QTextStream>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkReply>
// url to get lyrics using mediawiki API
static const QLatin1String constApiUrl("http://lyrics.wikia.com/api.php?action=query&prop=revisions&rvprop=content&format=xml&titles=");
static const int constMaxRedirects=3;
static QString changeExt(const QString &f, const QString &newExt)
{
QString newStr(f);
int dotPos(newStr.lastIndexOf('.'));
if (-1==dotPos) {
newStr+=newExt;
} else {
newStr.remove(dotPos, newStr.length());
newStr+=newExt;
}
return newStr;
}
struct Decoded
{
enum Type
{
Empty,
Lyric,
Redirect
};
Type type;
QString value;
Decoded() : type(Empty) { }
Decoded(Type t, const QString &v) : type(t), value(v) { }
};
static Decoded decode(const QByteArray &data)
{
QDomDocument doc;
doc.setContent(data);
QString response = doc.elementsByTagName("rev").at(0).toElement().text();
qDebug() << "DECODE:" << response;
int open=response.indexOf("<lyric");
int close=-1==open ? -1 : response.indexOf("</lyric", open);
if (-1!=close) {
if(response[open+6]=='s') {
open+=8;
} else {
open+=7;
}
if(response[open]=='\n') {
open++;
}
if(response[close]=='\n') {
close--;
}
return Decoded(Decoded::Lyric, response.mid(open, close-open));
}
if (response.startsWith("#REDIRECT [[", Qt::CaseInsensitive)) {
open=QString("#REDIRECT [[").length();
close=response.indexOf("]]", open);
return Decoded(Decoded::Redirect, constApiUrl+QUrl::toPercentEncoding(response.mid(open, close-open)));
}
return Decoded();
}
// build a URL component out of a string containing an artist or a song title
static QString convertToUrl(const QString &str)
{
// replace (erroneously used) accent ` with a proper apostrophe '
QString string(str);
string.replace( "`", "'" );
// split into words, then treat each word separately
QStringList words = string.split( " " );
for ( int i = 0; i < words.length(); i++ ) {
int upper = 1; // normally, convert first character only to uppercase, but:
// if we have a Roman numeral (well, at least either of "ii", "iii"), convert all "i"s
if ( words[i].at(0).toUpper() == QChar('I') ) {
// count "i" letters
while ( words[i].length() > upper && words[i].at(upper).toUpper() == QChar('I') ) {
upper++;
}
}
// if the word starts with an apostrophe or parenthesis, the next character has to be uppercase
if ( words[i].at(0) == QChar('\'') || words[i].at(0) == QChar('(') ) {
upper++;
}
// finally, perform the capitalization
if ( upper < words[i].length() ) {
words[i] = words[i].mid( 0, upper ).toUpper() + words[i].mid( upper );
} else {
words[i] = words[i].toUpper();
}
// now take care of more special cases
// names like "McSomething"
if ( words[i].mid( 0, 2 ) == QLatin1String("Mc") ) {
words[i] = QLatin1String("Mc") + words[i][2].toUpper() + words[i].mid( 3 );
}
// URI-encode the word
words[i] = QUrl::toPercentEncoding(words[i]);
}
// join the words back together and return the result
return words.join( "_" );
}
// convert all HTML entities to their applicable characters
static QString entityDecode(const QString &string)
{
QString convertXml = QLatin1String("<?xml version=\"1.0\" encoding=\"UTF-8\" ?><body><entity>") + string + QLatin1String("</entity></body>");
QDomDocument doc;
if (doc.setContent(convertXml))
{ // xml is valid
return doc.elementsByTagName( "entity" ).at( 0 ).toElement().text();
}
return string;
}
#ifdef ENABLE_KDE_SUPPORT
K_GLOBAL_STATIC(Lyrics, instance)
#endif
Lyrics * Lyrics::self()
{
#ifdef ENABLE_KDE_SUPPORT
return instance;
#else
static Lyrics *instance=0;;
if(!instance) {
instance=new Lyrics;
}
return instance;
#endif
}
Lyrics::Lyrics()
: manager(0)
{
}
static const QLatin1String constKeySep("<<##>>");
static const QLatin1String constLyricsDir("lyrics/");
static const QLatin1String constExtension(".lyrics");
QString cacheFile(QString artist, QString title, bool createDir=false)
{
title.replace("/", "_");
artist.replace("/", "_");
return QDir::toNativeSeparators(MusicLibraryModel::cacheDir(constLyricsDir+artist+'/', createDir))+title+constExtension;
}
void Lyrics::get(const Song &song)
{
if (song.artist.isEmpty() || song.title.isEmpty()) {
return;
}
if (!mpdDir.isEmpty()) {
// Check for MPD file...
QString mpdLyrics=changeExt(mpdDir+song.file, constExtension);
if (QFile::exists(mpdLyrics)) {
QFile f(mpdLyrics);
if (f.open(QIODevice::ReadOnly)) {
emit lyrics(song.artist, song.title, f.readAll());
f.close();
return;
}
}
}
// Check for cached file...
QString file=cacheFile(song.artist, song.title);
if (QFile::exists(file)) {
QFile f(file);
if (f.open(QIODevice::ReadOnly)) {
emit lyrics(song.artist, song.title, f.readAll());
f.close();
return;
}
}
// Check we are not aleady downloading...
QString key=song.artist+constKeySep+song.title;
if (jobs.contains(key)) {
return;
}
// strip "featuring <someone else>" from the song.artist
QString artistFixed=song.artist;
QStringList toStrip;
toStrip << QLatin1String(" ft. ") << QLatin1String(" feat. ") << QLatin1String(" featuring ");
foreach (const QString s, toStrip) {
int strip = artistFixed.toLower().indexOf( " ft. ");
if ( strip != -1 ) {
artistFixed = artistFixed.mid( 0, strip );
}
}
if (!manager) {
// QNetworkProxy proxy;
// proxy.setType(QNetworkProxy::HttpProxy);
// proxy.setHostName("proxy.fr.nds.com");
// proxy.setPort(8080);
// QNetworkProxy::setApplicationProxy(proxy);
#ifdef ENABLE_KDE_SUPPORT
manager = new KIO::Integration::AccessManager(this);
#else
manager=new QNetworkAccessManager(this);
#endif
connect(manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(jobFinished(QNetworkReply *)));
}
QUrl url;
url.setEncodedUrl(QString(constApiUrl + convertToUrl(entityDecode(artistFixed))+":"+convertToUrl(entityDecode(song.title))).toLatin1());
qDebug() << "URL" << url.toString();
QNetworkReply *reply=manager->get(QNetworkRequest(url));
jobs.insert(key, Job(reply));
}
void Lyrics::jobFinished(QNetworkReply *reply)
{
// TODO: Handle redirects: QNetworkRequest::RedirectionTargetAttribute
QMap<QString, Job>::Iterator it(jobs.begin());
QMap<QString, Job>::Iterator end(jobs.end());
for (; it!=end; ++it) {
if (it.value().reply==reply) {
QStringList parts=it.key().split(constKeySep);
Decoded decoded=QNetworkReply::NoError==reply->error() ? decode(reply->readAll()) : Decoded();
switch(decoded.type) {
case Decoded::Empty:
#ifdef ENABLE_KDE_SUPPORT
emit lyrics(parts[0], parts[1], i18n("No lyrics found"));
#else
emit lyrics(parts[0], parts[1], tr("No lyrics found"));
#endif
jobs.remove(it.key());
break;
case Decoded::Lyric: {
QFile f(cacheFile(parts[0], parts[1], true));
if (f.open(QIODevice::WriteOnly)) {
QTextStream(&f) << decoded.value;
f.close();
}
emit lyrics(parts[0], parts[1], decoded.value);
jobs.remove(it.key());
break;
}
case Decoded::Redirect: {
if(++(it.value().redirects) < constMaxRedirects) {
qDebug() << "Redirect URL" << decoded.value;
QNetworkReply *reply=manager->get(QNetworkRequest(QUrl(decoded.value)));
it.value().reply=reply;
} else {
#ifdef ENABLE_KDE_SUPPORT
emit lyrics(parts[0], parts[1], i18n("No lyrics found"));
#else
emit lyrics(parts[0], parts[1], tr("No lyrics found"));
#endif
jobs.remove(it.key());
}
break;
}
}
}
}
reply->deleteLater();
}