Clementine-audio-player-Mac.../src/internet/lastfmservice.cpp

399 lines
12 KiB
C++
Raw Normal View History

/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine 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 3 of the License, or
(at your option) any later version.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
// StringBuilder is activated to speed-up QString concatenation. As explained
// here:
// http://labs.qt.nokia.com/2011/06/13/string-concatenation-with-qstringbuilder/
// this cause some compilation errors in some cases. As Lasfm library inlines
// some functions in their includes files, which aren't compatible with
// QStringBuilder, we undef it here
#include <QtGlobal>
#if QT_VERSION >= 0x040600
#if QT_VERSION >= 0x040800
#undef QT_USE_QSTRINGBUILDER
#else
#undef QT_USE_FAST_CONCATENATION
#undef QT_USE_FAST_OPERATOR_PLUS
#endif // QT_VERSION >= 0x040800
#endif // QT_VERSION >= 0x040600
#include "lastfmservice.h"
2012-10-12 14:31:31 +02:00
#include <QMenu>
#include <QSettings>
#ifdef HAVE_LIBLASTFM1
#include <lastfm/RadioStation.h>
2012-10-12 14:31:31 +02:00
#else
#include <lastfm/RadioStation>
2012-10-12 14:31:31 +02:00
#endif
#include "lastfmcompat.h"
#include "internetmodel.h"
#include "internetplaylistitem.h"
#include "core/application.h"
2012-10-12 14:31:31 +02:00
#include "core/closure.h"
2011-04-22 18:50:29 +02:00
#include "core/logging.h"
#include "core/player.h"
#include "core/song.h"
#include "core/taskmanager.h"
#include "covers/coverproviders.h"
#include "covers/lastfmcoverprovider.h"
#include "ui/iconloader.h"
#include "ui/settingsdialog.h"
using lastfm::XmlQuery;
uint qHash(const lastfm::Track& track) {
return qHash(track.title()) ^ qHash(track.artist().name()) ^
qHash(track.album().title());
}
2009-12-29 20:22:02 +01:00
const char* LastFMService::kServiceName = "Last.fm";
2009-12-26 18:19:14 +01:00
const char* LastFMService::kSettingsGroup = "Last.fm";
2009-12-29 20:22:02 +01:00
const char* LastFMService::kAudioscrobblerClientId = "tng";
const char* LastFMService::kApiKey = "75d20fb472be99275392aefa2760ea09";
const char* LastFMService::kSecret = "d3072b60ae626be12be69448f5c46e70";
2009-12-26 18:19:14 +01:00
LastFMService::LastFMService(Application* app, QObject* parent)
: Scrobbler(parent),
scrobbler_(nullptr),
already_scrobbled_(false),
scrobbling_enabled_(false),
connection_problems_(false),
app_(app) {
ReloadSettings();
// we emit the signal the first time to be sure the buttons are in the right
// state
emit ScrobblingEnabledChanged(scrobbling_enabled_);
2009-12-30 00:01:07 +01:00
app_->cover_providers()->AddProvider(new LastFmCoverProvider(this));
2009-12-26 18:19:14 +01:00
}
LastFMService::~LastFMService() {}
2010-02-03 19:32:48 +01:00
void LastFMService::ReloadSettings() {
bool scrobbling_enabled_old = scrobbling_enabled_;
2010-02-03 19:32:48 +01:00
QSettings settings;
settings.beginGroup(kSettingsGroup);
lastfm::ws::Username = settings.value("Username").toString();
lastfm::ws::SessionKey = settings.value("Session").toString();
scrobbling_enabled_ = settings.value("ScrobblingEnabled", true).toBool();
buttons_visible_ = settings.value("ShowLoveBanButtons", true).toBool();
scrobble_button_visible_ =
settings.value("ShowScrobbleButton", true).toBool();
prefer_albumartist_ = settings.value("PreferAlbumArtist", false).toBool();
// avoid emitting signal if it's not changed
if (scrobbling_enabled_old != scrobbling_enabled_)
emit ScrobblingEnabledChanged(scrobbling_enabled_);
emit ButtonVisibilityChanged(buttons_visible_);
emit ScrobbleButtonVisibilityChanged(scrobble_button_visible_);
emit PreferAlbumArtistChanged(prefer_albumartist_);
2009-12-29 20:22:02 +01:00
}
2010-02-03 19:32:48 +01:00
void LastFMService::ShowConfig() {
app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Lastfm);
2010-02-03 19:32:48 +01:00
}
2009-12-29 21:48:50 +01:00
2010-02-03 19:32:48 +01:00
bool LastFMService::IsAuthenticated() const {
return !lastfm::ws::SessionKey.isEmpty();
2009-12-29 21:48:50 +01:00
}
bool LastFMService::IsSubscriber() const {
QSettings settings;
settings.beginGroup(kSettingsGroup);
return settings.value("Subscriber", false).toBool();
}
void LastFMService::Authenticate(const QString& username,
const QString& password) {
2009-12-26 18:19:14 +01:00
QMap<QString, QString> params;
params["method"] = "auth.getMobileSession";
params["username"] = username;
params["authToken"] =
lastfm::md5((username + lastfm::md5(password.toUtf8())).toUtf8());
2009-12-26 18:19:14 +01:00
QNetworkReply* reply = lastfm::ws::post(params);
2012-10-12 14:31:31 +02:00
NewClosure(reply, SIGNAL(finished()), this,
SLOT(AuthenticateReplyFinished(QNetworkReply*)), reply);
// If we need more detailed error reporting, handle error(NetworkError) signal
2009-12-26 18:19:14 +01:00
}
void LastFMService::SignOut() {
lastfm::ws::Username.clear();
lastfm::ws::SessionKey.clear();
QSettings settings;
settings.beginGroup(kSettingsGroup);
2011-09-24 18:01:18 +02:00
settings.setValue("Username", QString());
settings.setValue("Session", QString());
}
2012-10-12 14:31:31 +02:00
void LastFMService::AuthenticateReplyFinished(QNetworkReply* reply) {
reply->deleteLater();
2009-12-26 18:19:14 +01:00
// Parse the reply
lastfm::XmlQuery lfm(lastfm::compat::EmptyXmlQuery());
if (lastfm::compat::ParseQuery(reply->readAll(), &lfm)) {
2009-12-26 18:19:14 +01:00
lastfm::ws::Username = lfm["session"]["name"].text();
lastfm::ws::SessionKey = lfm["session"]["key"].text();
QString subscribed = lfm["session"]["subscriber"].text();
const bool is_subscriber = (subscribed.toInt() == 1);
// Save the session key
QSettings settings;
settings.beginGroup(kSettingsGroup);
settings.setValue("Username", lastfm::ws::Username);
settings.setValue("Session", lastfm::ws::SessionKey);
settings.setValue("Subscriber", is_subscriber);
2012-06-25 12:30:53 +02:00
} else {
emit AuthenticationComplete(false, lfm["error"].text().trimmed());
2009-12-26 18:19:14 +01:00
return;
}
2009-12-29 20:22:02 +01:00
// Invalidate the scrobbler - it will get recreated later
delete scrobbler_;
scrobbler_ = nullptr;
2009-12-29 20:22:02 +01:00
emit AuthenticationComplete(true, QString());
2009-12-26 18:19:14 +01:00
}
2009-12-26 22:35:45 +01:00
void LastFMService::UpdateSubscriberStatus() {
QMap<QString, QString> params;
params["method"] = "user.getInfo";
params["user"] = lastfm::ws::Username;
QNetworkReply* reply = lastfm::ws::post(params);
2012-10-12 14:31:31 +02:00
NewClosure(reply, SIGNAL(finished()), this,
SLOT(UpdateSubscriberStatusFinished(QNetworkReply*)), reply);
}
2012-10-12 14:31:31 +02:00
void LastFMService::UpdateSubscriberStatusFinished(QNetworkReply* reply) {
reply->deleteLater();
bool is_subscriber = false;
lastfm::XmlQuery lfm(lastfm::compat::EmptyXmlQuery());
if (lastfm::compat::ParseQuery(reply->readAll(), &lfm,
&connection_problems_)) {
QString subscriber = lfm["user"]["subscriber"].text();
is_subscriber = (subscriber.toInt() == 1);
QSettings settings;
settings.beginGroup(kSettingsGroup);
settings.setValue("Subscriber", is_subscriber);
2011-04-22 18:50:29 +02:00
qLog(Info) << lastfm::ws::Username << "Subscriber status:" << is_subscriber;
}
emit UpdatedSubscriberStatus(is_subscriber);
}
QUrl LastFMService::FixupUrl(const QUrl& url) {
QUrl ret;
ret.setEncodedUrl(url.toEncoded().replace(
"USERNAME", QUrl::toPercentEncoding(lastfm::ws::Username)));
return ret;
2009-12-26 22:35:45 +01:00
}
QString LastFMService::ErrorString(lastfm::ws::Error error) const {
switch (error) {
case lastfm::ws::InvalidService:
return tr("Invalid service");
case lastfm::ws::InvalidMethod:
return tr("Invalid method");
case lastfm::ws::AuthenticationFailed:
return tr("Authentication failed");
case lastfm::ws::InvalidFormat:
return tr("Invalid format");
case lastfm::ws::InvalidParameters:
return tr("Invalid parameters");
case lastfm::ws::InvalidResourceSpecified:
return tr("Invalid resource specified");
case lastfm::ws::OperationFailed:
return tr("Operation failed");
case lastfm::ws::InvalidSessionKey:
return tr("Invalid session key");
case lastfm::ws::InvalidApiKey:
return tr("Invalid API key");
case lastfm::ws::ServiceOffline:
return tr("Service offline");
case lastfm::ws::SubscribersOnly:
return tr("This stream is for paid subscribers only");
case lastfm::ws::TryAgainLater:
return tr("Last.fm is currently busy, please try again in a few minutes");
case lastfm::ws::NotEnoughContent:
return tr("Not enough content");
case lastfm::ws::NotEnoughMembers:
return tr("Not enough members");
case lastfm::ws::NotEnoughFans:
return tr("Not enough fans");
case lastfm::ws::NotEnoughNeighbours:
return tr("Not enough neighbors");
case lastfm::ws::MalformedResponse:
return tr("Malformed response");
2009-12-26 22:35:45 +01:00
case lastfm::ws::UnknownError:
default:
2010-02-23 19:33:09 +01:00
return tr("Unknown error");
2009-12-26 22:35:45 +01:00
}
}
2009-12-29 20:22:02 +01:00
bool LastFMService::InitScrobbler() {
if (!IsAuthenticated() || !IsScrobblingEnabled()) return false;
2009-12-29 20:22:02 +01:00
2009-12-29 20:57:33 +01:00
if (!scrobbler_)
2009-12-29 20:22:02 +01:00
scrobbler_ = new lastfm::Audioscrobbler(kAudioscrobblerClientId);
// reemit the signal since the sender is private
#ifdef HAVE_LIBLASTFM1
connect(scrobbler_, SIGNAL(scrobblesSubmitted(QList<lastfm::Track>)),
SIGNAL(ScrobbleSubmitted()));
connect(scrobbler_, SIGNAL(nowPlayingError(int, QString)),
SIGNAL(ScrobbleError(int)));
#else
connect(scrobbler_, SIGNAL(status(int)), SLOT(ScrobblerStatus(int)));
#endif
2009-12-29 20:22:02 +01:00
return true;
}
void LastFMService::ScrobblerStatus(int value) {
switch (value) {
case 2:
case 3:
emit ScrobbleSubmitted();
break;
default:
emit ScrobbleError(value);
break;
}
}
lastfm::Track LastFMService::TrackFromSong(const Song& song) const {
2009-12-29 20:22:02 +01:00
if (song.title() == last_track_.title() &&
song.artist() == last_track_.artist() &&
song.album() == last_track_.album())
return last_track_;
lastfm::Track ret;
song.ToLastFM(&ret, PreferAlbumArtist());
2009-12-29 20:22:02 +01:00
return ret;
}
void LastFMService::NowPlaying(const Song& song) {
if (!InitScrobbler()) return;
2009-12-29 20:22:02 +01:00
// Scrobbling streams is difficult if we don't have length of each individual
// part. In Song::ToLastFm we set the Track's source to
// NonPersonalisedBroadcast if it's such a stream, so we have to scrobble it
// when we change to a different track, but only if enough time has elapsed
// since it started playing.
if (!last_track_.isNull() &&
last_track_.source() == lastfm::Track::NonPersonalisedBroadcast) {
const int duration_secs =
last_track_.timestamp().secsTo(QDateTime::currentDateTime());
if (duration_secs >= lastfm::compat::ScrobbleTimeMin()) {
lastfm::MutableTrack mtrack(last_track_);
mtrack.setDuration(duration_secs);
qLog(Info) << "Scrobbling stream track" << mtrack.title() << "length"
<< duration_secs;
scrobbler_->cache(mtrack);
scrobbler_->submit();
emit ScrobbledRadioStream();
}
}
lastfm::MutableTrack mtrack(TrackFromSong(song));
mtrack.stamp();
already_scrobbled_ = false;
last_track_ = mtrack;
2009-12-29 21:11:03 +01:00
#ifndef HAVE_LIBLASTFM1
// Check immediately if the song is valid
Scrobble::Invalidity invalidity;
if (!lastfm::Scrobble(last_track_).isValid(&invalidity)) {
// for now just notify this, we can also see the cause
emit ScrobbleError(-1);
return;
}
#else
// TODO: validity was removed from liblastfm1 but might reappear, it should have
// no impact as we get a different error when actually trying to scrobble.
#endif
2010-03-21 01:22:15 +01:00
scrobbler_->nowPlaying(mtrack);
2009-12-29 20:22:02 +01:00
}
2009-12-29 21:11:03 +01:00
void LastFMService::Scrobble() {
if (!InitScrobbler()) return;
2009-12-29 20:22:02 +01:00
lastfm::compat::ScrobbleCache cache(lastfm::ws::Username);
qLog(Debug) << "There are" << cache.tracks().count()
<< "tracks in the last.fm cache.";
2009-12-29 21:11:03 +01:00
scrobbler_->cache(last_track_);
// Let's mark a track as cached, useful when the connection is down
2012-06-25 12:30:53 +02:00
emit ScrobbleError(30);
2009-12-29 21:11:03 +01:00
scrobbler_->submit();
already_scrobbled_ = true;
2009-12-29 20:22:02 +01:00
}
2009-12-29 21:11:03 +01:00
void LastFMService::Love() {
if (!IsAuthenticated()) ShowConfig();
2009-12-29 21:48:50 +01:00
2009-12-29 21:11:03 +01:00
lastfm::MutableTrack mtrack(last_track_);
2009-12-29 20:22:02 +01:00
mtrack.love();
2010-03-21 01:22:15 +01:00
last_track_ = mtrack;
if (already_scrobbled_) {
// The love only takes effect when the song is scrobbled, but we've already
// scrobbled this one so we have to do it again.
Scrobble();
}
2009-12-29 20:22:02 +01:00
}
2009-12-29 21:11:03 +01:00
void LastFMService::Ban() {
lastfm::MutableTrack mtrack(last_track_);
2009-12-29 20:22:02 +01:00
mtrack.ban();
2010-03-21 01:22:15 +01:00
last_track_ = mtrack;
2009-12-29 21:48:50 +01:00
Scrobble();
app_->player()->Next();
2009-12-29 20:22:02 +01:00
}
2009-12-30 00:01:07 +01:00
void LastFMService::ToggleScrobbling() {
// toggle status
scrobbling_enabled_ = !scrobbling_enabled_;
// save to the settings
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("ScrobblingEnabled", scrobbling_enabled_);
s.endGroup();
emit ScrobblingEnabledChanged(scrobbling_enabled_);
}