diff --git a/CMakeLists.txt b/CMakeLists.txt index 215a192f5..f102931b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,7 +104,8 @@ SET( CANTATA_SRCS gui/application.cpp gui/main.cpp gui/initialsettingswizard.cpp devices/deviceoptions.cpp context/lyricsettings.cpp context/ultimatelyricsprovider.cpp context/ultimatelyrics.cpp context/lyricsdialog.cpp context/contextpage.cpp context/view.cpp context/artistview.cpp context/albumview.cpp context/songview.cpp context/contextengine.cpp - context/wikipediaengine.cpp context/wikipediasettings.cpp context/othersettings.cpp context/contextsettings.cpp context/togglelist.cpp) + context/wikipediaengine.cpp context/wikipediasettings.cpp context/othersettings.cpp context/contextsettings.cpp context/togglelist.cpp + context/lastfmengine.cpp context/metaengine.cpp) SET( CANTATA_MOC_HDRS gui/initialsettingswizard.h gui/mainwindow.h gui/settings.h gui/covers.h gui/folderpage.h gui/librarypage.h gui/albumspage.h gui/playlistspage.h gui/serverplaybacksettings.h gui/serversettings.h gui/preferencesdialog.h gui/filesettings.h @@ -121,7 +122,7 @@ SET( CANTATA_MOC_HDRS network/networkaccessmanager.h context/togglelist.h context/ultimatelyrics.h context/ultimatelyricsprovider.h context/lyricsdialog.h context/contextpage.h context/artistview.h context/albumview.h context/songview.h context/view.h context/contextengine.h - context/wikipediaengine.h context/wikipediasettings.h context/othersettings.h) + context/wikipediaengine.h context/wikipediasettings.h context/othersettings.h context/lastfmengine.h context/metaengine.h) SET( CANTATA_UIS gui/initialsettingswizard.ui gui/mainwindow.ui gui/folderpage.ui gui/librarypage.ui gui/albumspage.ui gui/playlistspage.ui gui/filesettings.ui gui/interfacesettings.ui gui/playbacksettings.ui gui/serverplaybacksettings.ui gui/serversettings.ui diff --git a/context/albumview.cpp b/context/albumview.cpp index 8ce01a393..3d145f890 100644 --- a/context/albumview.cpp +++ b/context/albumview.cpp @@ -235,7 +235,7 @@ void AlbumView::searchResponse(const QString &resp, const QString &lang) } if (!resp.isEmpty()) { - details=resp; + details=engine->translateLinks(resp); if (!lang.isEmpty()) { QFile f(cacheFileName(Covers::fixArtist(currentSong.albumArtist()), currentSong.album, lang, true)); QtIOCompressor compressor(&f); diff --git a/context/artistview.cpp b/context/artistview.cpp index 62d0b386a..b4185b3d4 100644 --- a/context/artistview.cpp +++ b/context/artistview.cpp @@ -361,7 +361,7 @@ void ArtistView::abort() void ArtistView::searchResponse(const QString &resp, const QString &lang) { - biography=resp; + biography=engine->translateLinks(resp); emit haveBio(currentSong.artist, resp); hideSpinner(); diff --git a/context/contextengine.cpp b/context/contextengine.cpp index 49b862a7c..895b90cad 100644 --- a/context/contextengine.cpp +++ b/context/contextengine.cpp @@ -22,12 +22,13 @@ */ #include "contextengine.h" +#include "metaengine.h" #include "wikipediaengine.h" #include ContextEngine * ContextEngine::create(QObject *parent) { - return new WikipediaEngine(parent); + return new MetaEngine(parent); } ContextEngine::ContextEngine(QObject *p) @@ -41,6 +42,22 @@ ContextEngine::~ContextEngine() cancel(); } +QStringList ContextEngine::fixQuery(const QStringList &query) const +{ + QStringList fixedQuery; + foreach (QString q, query) { + if (q.contains(QLatin1String("PREVIEW: buy it at www.magnatune.com"))) { + q = q.remove(QLatin1String(" (PREVIEW: buy it at www.magnatune.com)")); + int index = q.indexOf(QLatin1Char('-')); + if (-1!=index) { + q = q.left(index - 1); + } + } + fixedQuery.append(q); + } + return fixedQuery; +} + void ContextEngine::cancel() { if (job) { diff --git a/context/contextengine.h b/context/contextengine.h index af866c543..ef375d5fc 100644 --- a/context/contextengine.h +++ b/context/contextengine.h @@ -44,8 +44,10 @@ public: ContextEngine(QObject *p); virtual ~ContextEngine(); - virtual const QStringList & getLangs()=0; - virtual QString getPrefix(const QString &key)=0; + virtual QString translateLinks(QString text) const =0; + virtual QStringList getLangs() const =0; + virtual QString getPrefix(const QString &key) const =0; + QStringList fixQuery(const QStringList &query) const; void cancel(); diff --git a/context/lastfmengine.cpp b/context/lastfmengine.cpp new file mode 100644 index 000000000..4caa5572c --- /dev/null +++ b/context/lastfmengine.cpp @@ -0,0 +1,196 @@ +/* + * Cantata + * + * Copyright (c) 2011-2013 Craig Drummond + * + * ---- + * + * 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; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "lastfmengine.h" +#include "networkaccessmanager.h" +#include "localize.h" +#include "covers.h" +#include +#if QT_VERSION >= 0x050000 +#include +#endif +#include +#include +#include + +//#define DBUG qWarning() << "LastFmEngine" + +#ifndef DBUG +#define DBUG qDebug() +#endif + +const QLatin1String LastFmEngine::constLang("lastfm"); +const QLatin1String LastFmEngine::constLinkPlaceholder("XXX_CONTEXT_READ_MORE_ON_LASTFM_XXX"); + +static const char * constModeProperty="mode"; +static const char * constQuery="query"; +static const char * constRedirectsProperty="redirects"; +static const int constMaxRedirects=3; + +LastFmEngine::LastFmEngine(QObject *p) + : ContextEngine(p) +{ +} + +QStringList LastFmEngine::getLangs() const +{ + QStringList langs; + langs.append(constLang); + return langs; +} + +QString LastFmEngine::translateLinks(QString text) const +{ + text=text.replace(constLinkPlaceholder, i18n("Read more on last.fm")); + return text; +} + +void LastFmEngine::search(const QStringList &query, Mode mode) +{ + QStringList fixedQuery=fixQuery(query); + QUrl url("https://ws.audioscrobbler.com/2.0/"); + #if QT_VERSION < 0x050000 + QUrl &urlQuery=url; + #else + QUrlQuery urlQuery; + #endif + + urlQuery.addQueryItem("method", Artist==mode ? "artist.getInfo" : "album.getInfo"); + urlQuery.addQueryItem("api_key", Covers::constLastFmApiKey); + urlQuery.addQueryItem("autocorrect", "1"); + urlQuery.addQueryItem("artist", Covers::fixArtist(fixedQuery.at(0))); + if (Album==mode) { + urlQuery.addQueryItem("album", fixedQuery.at(1)); + } + #if QT_VERSION >= 0x050000 + url.setQuery(urlQuery); + #endif + + job=NetworkAccessManager::self()->get(url); + job->setProperty(constModeProperty, (int)mode); + job->setProperty(constRedirectsProperty, 0); + + QStringList queryString; + foreach (QString q, fixedQuery) { + q=q.replace("/", "%2F"); + q=q.replace(" ", "+"); + queryString.append(q); + } + job->setProperty(constQuery, queryString.join("/")); + DBUG << __FUNCTION__ << url.toString(); + connect(job, SIGNAL(finished()), this, SLOT(parseResponse())); +} + +void LastFmEngine::parseResponse() +{ + DBUG << __FUNCTION__; + QNetworkReply *reply = getReply(sender()); + if (!reply) { + return; + } + + QVariant redirect = reply->header(QNetworkRequest::LocationHeader); + int numRirects=reply->property(constRedirectsProperty).toInt(); + if (redirect.isValid() && ++numRirectsget(redirect.toString()); + job->setProperty(constRedirectsProperty, numRirects); + job->setProperty(constModeProperty, reply->property(constModeProperty)); + DBUG << __FUNCTION__ << "Redirect" << redirect.toString(); + connect(job, SIGNAL(finished()), this, SLOT(parseResponse())); + return; + } + + QByteArray data=reply->readAll(); + if (QNetworkReply::NoError!=reply->error() || data.isEmpty()) { + DBUG << __FUNCTION__ << "Empty/error"; + emit searchResult(QString(), QString()); + return; + } + + Mode mode=(Mode)reply->property(constModeProperty).toInt(); + QString text=Artist==mode ? parseArtistResponse(data) : parseAlbumResponse(data); + if (!text.isEmpty()) { + static const QRegExp constLicense("User-contributed text is available.*"); + text.remove(constLicense); + DBUG << __FUNCTION__ << "Text:" << text; + QString query=reply->property(constQuery).toString(); + int split = text.indexOf('\n', 512); + if (-1==split) { + split = text.indexOf(". ", 512); + } + + text = text.left(split); + if (-1!=split) { + text += QLatin1String("

")+constLinkPlaceholder+QLatin1String(""); + } + } + emit searchResult(text, text.isEmpty() ? QString() : constLang); +} + +QString LastFmEngine::parseArtistResponse(const QByteArray &data) +{ + QXmlStreamReader xml(data); + while (xml.readNextStartElement()) { + if (QLatin1String("artist")==xml.name()) { + while (xml.readNextStartElement()) { + if (QLatin1String("bio")==xml.name()) { + while (xml.readNextStartElement()) { + if (QLatin1String("content")==xml.name()) { + return xml.readElementText().trimmed(); + } else { + xml.skipCurrentElement(); + } + } + } else { + xml.skipCurrentElement(); + } + } + } + } + + return QString(); +} + +QString LastFmEngine::parseAlbumResponse(const QByteArray &data) +{ + QXmlStreamReader xml(data); + while (xml.readNextStartElement()) { + if (QLatin1String("album")==xml.name()) { + while (xml.readNextStartElement()) { + if (QLatin1String("wiki")==xml.name()) { + while (xml.readNextStartElement()) { + if (QLatin1String("content")==xml.name()) { + return xml.readElementText().trimmed(); + } else { + xml.skipCurrentElement(); + } + } + } else { + xml.skipCurrentElement(); + } + } + } + } + + return QString(); +} diff --git a/context/lastfmengine.h b/context/lastfmengine.h new file mode 100644 index 000000000..27d3a77f4 --- /dev/null +++ b/context/lastfmengine.h @@ -0,0 +1,55 @@ +/* + * Cantata + * + * Copyright (c) 2011-2013 Craig Drummond + * + * ---- + * + * 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; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef LASTFM_ENGINE_H +#define LASTFM_ENGINE_H + +#include "contextengine.h" +#include + +class LastFmEngine : public ContextEngine +{ + Q_OBJECT + +public: + static const QLatin1String constLang; + static const QLatin1String constLinkPlaceholder; + + LastFmEngine(QObject *p); + + QStringList getLangs() const; + virtual QString getPrefix(const QString &) const { return constLang; } + QString translateLinks(QString text) const; + +public Q_SLOTS: + void search(const QStringList &query, Mode mode); + +private Q_SLOTS: + void parseResponse(); + +private: + QString parseArtistResponse(const QByteArray &data); + QString parseAlbumResponse(const QByteArray &data); +}; + +#endif diff --git a/context/metaengine.cpp b/context/metaengine.cpp new file mode 100644 index 000000000..c4b3d0412 --- /dev/null +++ b/context/metaengine.cpp @@ -0,0 +1,97 @@ +/* + * Cantata + * + * Copyright (c) 2011-2013 Craig Drummond + * + * ---- + * + * 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; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "metaengine.h" +#include "wikipediaengine.h" +#include "lastfmengine.h" + +static const QLatin1String constBlankResp("-"); + +MetaEngine::MetaEngine(QObject *p) + : ContextEngine(p) +{ + wiki=new WikipediaEngine(this); + lastfm=new LastFmEngine(this); + connect(wiki, SIGNAL(searchResult(QString,QString)), SLOT(wikiResponse(QString,QString))); + connect(lastfm, SIGNAL(searchResult(QString,QString)), SLOT(lastFmResponse(QString,QString))); +} + +QStringList MetaEngine::getLangs() const +{ + QStringList langs=wiki->getLangs(); + langs.append(LastFmEngine::constLang); + return langs; +} + +QString MetaEngine::getPrefix(const QString &key) const +{ + return key==LastFmEngine::constLang ? LastFmEngine::constLang : wiki->getPrefix(key); +} + +QString MetaEngine::translateLinks(QString text) const +{ + return lastfm->translateLinks(wiki->translateLinks(text)); +} + +void MetaEngine::search(const QStringList &query, Mode mode) +{ + responses.clear(); + wiki->cancel(); + lastfm->cancel(); + wiki->search(query, mode); + lastfm->search(query, mode); +} + +void MetaEngine::wikiResponse(const QString &html, const QString &lang) +{ + if (!html.isEmpty()) { + // Got a wikipedia reponse, use it! + lastfm->cancel(); + emit searchResult(html, lang); + responses.clear(); + } else if (responses[LastFm].html.isEmpty()) { + // Wikipedia response is empty, but have not received last.fm reply yet. + // So, indicate that Wikipedia was empty - and wait for last.fm + responses[Wiki]=Response(constBlankResp, lang); + } else if (constBlankResp==responses[LastFm].html) { + // Last.fmn is empty as well :-( + emit searchResult(QString(), QString()); + responses.clear(); + } else { + // Got a last.fm response, use that! + emit searchResult(responses[LastFm].html, responses[LastFm].lang); + responses.clear(); + } +} + +void MetaEngine::lastFmResponse(const QString &html, const QString &lang) +{ + if (constBlankResp==responses[Wiki].html) { + // Wikipedia failed, so use last.fm response... + emit searchResult(html, lang); + responses.clear(); + } else if (responses[Wiki].html.isEmpty()) { + // No Wikipedia response yet, so save last.fm response... + responses[Wiki]=Response(html.isEmpty() ? constBlankResp : html, lang); + } +} diff --git a/context/metaengine.h b/context/metaengine.h new file mode 100644 index 000000000..8dc439baa --- /dev/null +++ b/context/metaengine.h @@ -0,0 +1,69 @@ +/* + * Cantata + * + * Copyright (c) 2011-2013 Craig Drummond + * + * ---- + * + * 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; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef META_ENGINE_H +#define META_ENGINE_H + +#include "contextengine.h" +#include +#include + +class WikipediaEngine; +class LastFmEngine; + +class MetaEngine : public ContextEngine +{ + Q_OBJECT + + enum Engines { + Wiki = 0, + LastFm = 1 + }; + + struct Response { + Response(const QString &h=QString(), const QString &l=QString()) : html(h), lang(l) { } + QString html; + QString lang; + }; + +public: + MetaEngine(QObject *p); + + QStringList getLangs() const; + QString getPrefix(const QString &key) const; + QString translateLinks(QString text) const; + +public Q_SLOTS: + void search(const QStringList &query, Mode mode); + +private Q_SLOTS: + void wikiResponse(const QString &html, const QString &lang); + void lastFmResponse(const QString &html, const QString &lang); + +private: + QMap responses; + WikipediaEngine *wiki; + LastFmEngine *lastfm; +}; + +#endif diff --git a/context/wikipediaengine.cpp b/context/wikipediaengine.cpp index 88195a119..48833bad4 100644 --- a/context/wikipediaengine.cpp +++ b/context/wikipediaengine.cpp @@ -230,7 +230,7 @@ static QString wikiToHtml(QString answer, bool introOnly, const QUrl &url) if (!answer.endsWith("
")) { answer+="
"; } - answer+=QString("
%2").arg(u).arg(i18n("Read more on wikipedia")); + answer+=QString("
%2").arg(u).arg(WikipediaEngine::constReadMorePlaceholder); } } else { answer.replace("
", ""); @@ -263,7 +263,7 @@ static QString wikiToHtml(QString answer, bool introOnly, const QUrl &url) if (!answer.endsWith("
")) { answer+="
"; } - answer+=QString("
%2").arg(u).arg(i18n("Open in browser")); + answer+=QString("
%2").arg(u).arg(WikipediaEngine::constOpenInBrowserPlaceholder); } return answer; @@ -276,6 +276,8 @@ static inline QString getLang(const QUrl &url) QStringList WikipediaEngine::preferredLangs; bool WikipediaEngine::introOnly=true; +const QLatin1String WikipediaEngine::constReadMorePlaceholder("XXX_CONTEXT_READ_MORE_ON_WIKIPEDIA_XXX"); +const QLatin1String WikipediaEngine::constOpenInBrowserPlaceholder("XXX_CONTEXT_OPEN_IN_BROWSER_WIKIPEDIA_XXX"); WikipediaEngine::WikipediaEngine(QObject *p) : ContextEngine(p) @@ -294,23 +296,17 @@ void WikipediaEngine::setPreferedLangs(const QStringList &l) } } +QString WikipediaEngine::translateLinks(QString text) const +{ + text=text.replace(constReadMorePlaceholder, i18n("Read more on wikipedia")); + text=text.replace(constOpenInBrowserPlaceholder, i18n("Open in browser")); + return text; +} + void WikipediaEngine::search(const QStringList &query, Mode mode) { titles.clear(); - - QStringList fixedQuery; - foreach (QString q, query) { - if (q.contains(QLatin1String("PREVIEW: buy it at www.magnatune.com"))) { - q = q.remove(QLatin1String(" (PREVIEW: buy it at www.magnatune.com)")); - int index = q.indexOf(QLatin1Char('-')); - if (-1!=index) { - q = q.left(index - 1); - } - } - fixedQuery.append(q); - } - - requestTitles(fixedQuery, mode, getPrefix(preferredLangs.first())); + requestTitles(fixQuery(query), mode, getPrefix(preferredLangs.first())); } void WikipediaEngine::requestTitles(const QStringList &query, Mode mode, const QString &lang) @@ -530,12 +526,14 @@ void WikipediaEngine::parsePage() job->setProperty(constRedirectsProperty, numRirects); job->setProperty(constModeProperty, reply->property(constModeProperty)); job->setProperty(constQueryProperty, reply->property(constQueryProperty)); + DBUG << __FUNCTION__ << "Redirect" << redirect.toString(); connect(job, SIGNAL(finished()), this, SLOT(parsePage())); return; } QByteArray data=reply->readAll(); if (QNetworkReply::NoError!=reply->error() || data.isEmpty()) { + DBUG << __FUNCTION__ << "Empty/error"; emit searchResult(QString(), QString()); return; } diff --git a/context/wikipediaengine.h b/context/wikipediaengine.h index 529916c19..147071aac 100644 --- a/context/wikipediaengine.h +++ b/context/wikipediaengine.h @@ -34,12 +34,16 @@ class WikipediaEngine : public ContextEngine public: WikipediaEngine(QObject *p); + static const QLatin1String constReadMorePlaceholder; + static const QLatin1String constOpenInBrowserPlaceholder; + static void setPreferedLangs(const QStringList &l); static const QStringList & getPreferedLangs() { return preferredLangs; } static void setIntroOnly(bool v) { introOnly=v; } - const QStringList & getLangs() { return getPreferedLangs(); } - QString getPrefix(const QString &key) { return key.split(QLatin1Char(':')).back(); } + QString translateLinks(QString text) const; + QStringList getLangs() const { return getPreferedLangs(); } + QString getPrefix(const QString &key) const { return key.split(QLatin1Char(':')).back(); } public Q_SLOTS: void search(const QStringList &query, Mode mode); diff --git a/gui/main.cpp b/gui/main.cpp index 84963b145..4bfc180d3 100644 --- a/gui/main.cpp +++ b/gui/main.cpp @@ -102,6 +102,7 @@ int main(int argc, char *argv[]) aboutData.addAuthor(ki18n("Armin Walland"), ki18n("QtMPC author"), QByteArray(), "http://qtmpc.lowblog.nl"); aboutData.addCredit(ki18n("Home Theater Backdrops"), ki18n("Context view backdrops"), QByteArray(), "www.htbackdrops.com"); aboutData.addCredit(ki18n("Wikipedia"), ki18n("Context view metadata"), QByteArray(), "www.wikipedia.org"); + aboutData.addCredit(ki18n("Last.fm"), ki18n("Context view metadata"), QByteArray(), "www.last.fm"); KCmdLineArgs::init(argc, argv, &aboutData); #ifdef TAGLIB_FOUND KCmdLineOptions options; diff --git a/gui/mainwindow.cpp b/gui/mainwindow.cpp index 5a29a29ee..dd90c3009 100644 --- a/gui/mainwindow.cpp +++ b/gui/mainwindow.cpp @@ -1455,7 +1455,7 @@ void MainWindow::showAboutDialog() i18nc("Qt-only", "Cantata %1

MPD client.

(c) Craig Drummond 2011-2013.
Released under the GPLv3

" "Based upon QtMPC - (C) 2007-2010 The QtMPC Authors
" "Context view backdrops courtesy of Home Theater Backdrops
" - "Context view metadata courtesy of Wikipedia
").arg(PACKAGE_VERSION_STRING)+ + "Context view metadata courtesy of Wikipedia and Last.fm").arg(PACKAGE_VERSION_STRING)+ QLatin1String("")); } #endif