/*
 * Cantata
 *
 * Copyright (c) 2011-2022 Craig Drummond <craig.p.drummond@gmail.com>
 *
 * This file contains code from QMPDClient:
 * Copyright (C) 2005-2008 Håvard Tautra Knutsen <havtknut@tihlde.org>
 * Copyright (C) 2009 Voker57 <voker57@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; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "scrobbler.h"
#include "config.h"
#include "gui/apikeys.h"
#include "mpd-interface/mpdconnection.h"
#include "network/networkaccessmanager.h"
#include "pausabletimer.h"
#include "support/configuration.h"
#include "support/globalstatic.h"
#include "support/utils.h"
#include <QCryptographicHash>
#include <QDir>
#include <QFile>
#include <QSslSocket>
#include <QStringList>
#include <QTimer>
#include <QUrl>
#include <QXmlStreamReader>
#ifdef BUNDLED_KARCHIVE
#include <kcompressiondevice.h>
#else
#include <KCompressionDevice>
#endif

#include <QDebug>
static bool debugIsEnabled = false;
static bool fakeScrobbling = false;
#define DBUG \
	if (debugIsEnabled) qWarning() << metaObject()->className() << __FUNCTION__
#define DBUG_CLASS(C) \
	if (debugIsEnabled) qWarning() << C << __FUNCTION__
void Scrobbler::enableDebug()
{
	debugIsEnabled = true;
}

const QLatin1String Scrobbler::constCacheDir("scrobbling");
const QLatin1String Scrobbler::constCacheFile("tracks.xml.gz");
static const QLatin1String constSettingsGroup("Scrobbling");
static const QString constSecretKey = QLatin1String("0753a75ccded9b17b872744d4bb60b35");
static const int constMaxBatchSize = 50;
static const int constNowPlayingInterval = 5000;

GLOBAL_STATIC(Scrobbler, instance)

enum LastFmErrors {
	NoError = 0,// ??

	InvalidService = 2,
	InvalidMethod,
	AuthenticationFailed,
	InvalidFormat,
	InvalidParameters,
	InvalidResourceSpecified,
	OperationFailed,
	InvalidSessionKey,
	InvalidApiKey,
	ServiceOffline,

	TokenNotAuthorised = 14,

	TryAgainLater = 16,

	RateLimitExceeded = 29
};

static QString errorString(int code, const QString& msg)
{
	switch (code) {
	case InvalidService: return QObject::tr("Invalid service");
	case InvalidMethod: return QObject::tr("Invalid method");
	case AuthenticationFailed: return QObject::tr("Authentication failed");
	case InvalidFormat: return QObject::tr("Invalid format");
	case InvalidParameters: return QObject::tr("Invalid parameters");
	case InvalidResourceSpecified: return QObject::tr("Invalid resource specified");
	case OperationFailed: return QObject::tr("Operation failed");
	case InvalidSessionKey: return QObject::tr("Invalid session key");
	case InvalidApiKey: return QObject::tr("Invalid API key");
	case ServiceOffline: return QObject::tr("Service offline");
	case TryAgainLater: return QObject::tr("Last.fm is currently busy, please try again in a few minutes");
	case RateLimitExceeded: return QObject::tr("Rate-limit exceeded");
	default:
		return msg.isEmpty() ? QObject::tr("Unknown error") : msg.trimmed();
	}
}

static QString md5(const QString& s)
{
	return QString::fromLatin1(QCryptographicHash::hash(s.toUtf8(), QCryptographicHash::Md5).toHex());
}

static void sign(QMap<QString, QString>& params)
{
	QString s;
	params[QLatin1String("api_key")] = ApiKeys::self()->get(ApiKeys::LastFm);
	QStringList keys = params.keys();
	keys.sort();
	for (const QString& k : keys) {
		s += k + params[k];
	}
	s += constSecretKey;
	params[QLatin1String("api_sig")] = md5(s);
}

static QByteArray format(const QMap<QString, QString>& params)
{
	QByteArray data;
	QMapIterator<QString, QString> i(params);
	while (i.hasNext()) {
		i.next();
		data += i.key().toLatin1() + '=' + QUrl::toPercentEncoding(i.value());
		if (i.hasNext()) {
			data += '&';
		}
	}
	DBUG_CLASS("Scrobbler") << data;
	return data;
}

static QString cacheName(bool createDir)
{
	QString dir = Utils::cacheDir(Scrobbler::constCacheDir, createDir);
	return dir.isEmpty() ? QString() : (dir + Scrobbler::constCacheFile);
}

Scrobbler::Track::Track(const Song& s)
{
	if (!(s.blank & Song::BlankTitle)) {
		title = s.title;
	}
	if (!(s.blank & Song::BlankArtist)) {
		artist = s.artist;
	}
	if (!(s.blank & Song::BlankAlbum)) {
		album = s.album;
	}
	albumartist = s.albumartist;
	track = s.track;
	length = s.time;
	timestamp = 0;
}

Scrobbler::Scrobbler()
	: QObject(nullptr), scrobblingEnabled(false), loveIsEnabled(false), scrobbler("Last.fm"), lastNowPlaying(0), nowPlayingIsPending(false), lovePending(false), nowPlayingSent(false), loveSent(false), scrobbledCurrent(false), scrobbleViaMpd(false), failedCount(0), lastState(MPDState_Inactive), authJob(nullptr), scrobbleJob(nullptr)
{
	hardFailTimer = new QTimer(this);
	hardFailTimer->setInterval(60 * 1000);
	hardFailTimer->setSingleShot(true);
	scrobbleTimer = new PausableTimer();
	scrobbleTimer->setInterval(9000000);// fakeScrobblinghuge number to avoid scrobbling just started song start (rare case)
	scrobbleTimer->setSingleShot(true);
	nowPlayingTimer = new PausableTimer();
	nowPlayingTimer->setSingleShot(true);
	nowPlayingTimer->setInterval(constNowPlayingInterval);
	retryTimer = new QTimer(this);
	retryTimer->setSingleShot(true);
	retryTimer->setInterval(10000);
	connect(scrobbleTimer, SIGNAL(timeout()), this, SLOT(scrobbleCurrent()));
	connect(retryTimer, SIGNAL(timeout()), this, SLOT(scrobbleQueued()));
	connect(nowPlayingTimer, SIGNAL(timeout()), this, SLOT(scrobbleNowPlaying()));
	connect(hardFailTimer, SIGNAL(timeout()), this, SLOT(authenticate()));
	loadSettings();
	connect(this, SIGNAL(clientMessage(QString, QString, QString)), MPDConnection::self(), SLOT(sendClientMessage(QString, QString, QString)));
	connect(MPDConnection::self(), SIGNAL(clientMessageFailed(QString, QString)), SLOT(clientMessageFailed(QString, QString)));
	connect(MPDConnection::self(), SIGNAL(statusUpdated(MPDStatusValues)), this, SLOT(mpdStatusUpdated(MPDStatusValues)));
	connect(MPDConnection::self(), SIGNAL(currentSongUpdated(Song)), this, SLOT(setSong(Song)));
}

Scrobbler::~Scrobbler()
{
	DBUG;
	stop();
}

void Scrobbler::stop()
{
	cancelJobs();
	saveCache();
}

void Scrobbler::setActive()
{
	if (isEnabled()) {
		loadCache();
	}
	else {
		reset();
	}

	if (isEnabled() || scrobbleViaMpd) {
		connect(MPDStatus::self(), SIGNAL(updated()), this, SLOT(mpdStateUpdated()));
	}
	else {
		disconnect(MPDStatus::self(), SIGNAL(updated()), this, SLOT(mpdStateUpdated()));
	}

	if (isEnabled() && !inactiveSong.isEmpty()) {
		Song s;
		s.title = inactiveSong.title;
		s.artist = inactiveSong.artist;
		s.albumartist = inactiveSong.albumartist;
		s.album = inactiveSong.album;
		s.track = inactiveSong.track;
		s.time = inactiveSong.length;
		setSong(s);
		inactiveSong.clear();
	}
	if (!isAuthenticated()) {
		authenticate();
	}
	else if (!songQueue.isEmpty()) {
		scrobbleQueued();
	}
}

static const QLatin1String constFakeScrobbling("fake");

void Scrobbler::loadSettings()
{
	Configuration cfg(constSettingsGroup);

	userName = cfg.get("userName", userName);
	//    password=cfg.get("password", password);
	sessionKey = cfg.get("sessionKey", sessionKey);
	scrobblingEnabled = cfg.get("enabled", scrobblingEnabled);
	loveIsEnabled = cfg.get("loveEnabled", loveIsEnabled);
	scrobbler = cfg.get("scrobbler", scrobbler);
	scrobbleViaMpd = viaMpd(scrobblerUrl());
	fakeScrobbling = constFakeScrobbling == scrobbler;
	if (fakeScrobbling) {
		sessionKey = constFakeScrobbling;
	}
	DBUG << scrobbler << userName << sessionKey.isEmpty() << scrobblingEnabled;
	emit authenticated(isAuthenticated());
	emit enabled(isEnabled());
	emit loveEnabled(loveIsEnabled);
	setActive();
}

void Scrobbler::setDetails(const QString& s, const QString& u, const QString& p)
{
	if (fakeScrobbling) {
		return;
	}

	if (u != scrobbler || u != userName || p != password) {
		DBUG << "details changed";
		Configuration cfg(constSettingsGroup);

		scrobbler = s;
		userName = u;
		password = p;
		sessionKey = QString();
		reset();
		cfg.set("scrobbler", scrobbler);
		cfg.set("userName", userName);
		cfg.set("sessionKey", sessionKey);
		scrobbleViaMpd = viaMpd(scrobblerUrl());
		//        cfg.set("password", password);
		setActive();
		if (!isEnabled()) {
			emit authenticated(false);
		}
		emit scrobblerChanged();
	}
	else if (!isAuthenticated() && haveLoginDetails()) {
		authenticate();
	}
	else if (!scrobbleViaMpd) {
		DBUG << "details NOT changed";
		emit authenticated(isAuthenticated());
	}
}

void Scrobbler::love()
{
	if (lovedTrack()) {
		return;
	}

	lovePending = false;
	if (!loveIsEnabled) {
		return;
	}

	const Track& song = isEnabled() ? currentSong : inactiveSong;
	if (song.title.isEmpty() || song.artist.isEmpty() || loveSent) {
		return;
	}

	if (scrobbleViaMpd) {
		emit clientMessage(scrobblerUrl(), QLatin1String("love"), scrobbler);
		loveSent = true;
		return;
	}

	if (!ensureAuthenticated()) {
		lovePending = true;
		return;
	}

	QMap<QString, QString> params;
	params["method"] = "track.love";
	params["track"] = song.title;
	params["artist"] = song.artist;
	params["sk"] = sessionKey;
	sign(params);
	DBUG << song.title << song.artist;
	loveSent = true;
	if (fakeScrobbling) {
		DBUG << "MSG" << params;
	}
	else {
		QNetworkReply* job = NetworkAccessManager::self()->postFormData(QUrl(scrobblerUrl()), format(params));
		connect(job, SIGNAL(finished()), this, SLOT(handleResp()));
	}
}

void Scrobbler::setEnabled(bool e)
{
	if (e != scrobblingEnabled) {
		scrobblingEnabled = e;
		Configuration(constSettingsGroup).set("enabled", scrobblingEnabled);
		setActive();
		emit enabled(e);
	}
}

void Scrobbler::setLoveEnabled(bool e)
{
	if (e != loveIsEnabled) {
		loveIsEnabled = e;
		Configuration(constSettingsGroup).set("loveEnabled", loveIsEnabled);
		setActive();
		emit loveEnabled(e);
	}
}

void Scrobbler::calcScrobbleIntervals()
{
	int elapsed = MPDStatus::self()->timeElapsed() * 1000;
	if (elapsed < 0) {
		elapsed = 0;
	}
	int nowPlayingTimemout = constNowPlayingInterval;
	if (elapsed > 4000) {
		nowPlayingTimemout = 10;
	}
	else {
		nowPlayingTimemout -= elapsed;
	}
	nowPlayingTimer->setInterval(nowPlayingTimemout);
	int timeout = qMin(currentSong.length / 2, (quint32)240) * 1000;// Scrobble at 1/2 way point or 4 mins - whichever comes first!
	DBUG << "timeout" << timeout << elapsed << nowPlayingTimemout;
	if (timeout > elapsed) {
		timeout -= elapsed;
	}
	else {
		timeout = 100;
	}
	scrobbleTimer->setInterval(timeout);
}

void Scrobbler::setSong(const Song& s)
{
	DBUG << isEnabled() << s.isStandardStream() << s.time << s.file << s.title << s.artist << s.album << s.albumartist << s.blank;
	if (!scrobbleViaMpd && !isEnabled()) {
		if (inactiveSong.artist != s.artist || inactiveSong.title != s.title || inactiveSong.album != s.album) {
			emit songChanged(!s.isStandardStream() && !s.isEmpty());
		}
		inactiveSong = Track(s);
		return;
	}

	inactiveSong.clear();
	if (currentSong.artist != s.artist || currentSong.title != s.title || currentSong.album != s.album) {
		nowPlayingSent = scrobbledCurrent = loveSent = lovePending = nowPlayingIsPending = false;
		currentSong = Track(s);
		lastNowPlaying = 0;
		emit songChanged(!s.isStandardStream() && !s.isEmpty());
		if (scrobbleViaMpd || !isEnabled() || s.isStandardStream() || s.time < 30) {
			return;
		}

		calcScrobbleIntervals();
		if (MPDState_Playing == MPDStatus::self()->state()) {
			mpdStateUpdated(true);
		}
	}
}

void Scrobbler::scrobbleNowPlaying()
{
	nowPlayingIsPending = false;
	if (!ensureAuthenticated()) {
		nowPlayingIsPending = true;
		return;
	}
	if (currentSong.title.isEmpty() || currentSong.artist.isEmpty() || nowPlayingSent || scrobbleViaMpd) {
		return;
	}
	QMap<QString, QString> params;
	params["method"] = "track.updateNowPlaying";
	params["track"] = currentSong.title;
	if (!currentSong.album.isEmpty()) {
		params["album"] = currentSong.album;
	}
	params["artist"] = currentSong.artist;
	if (!currentSong.albumartist.isEmpty() && currentSong.albumartist != currentSong.artist) {
		params["albumArtist"] = currentSong.albumartist;
	}
	if (currentSong.track) {
		params["trackNumber"] = QString::number(currentSong.track);
	}
	if (currentSong.length) {
		params["duration"] = QString::number(currentSong.length);
	}
	params["sk"] = sessionKey;
	sign(params);
	DBUG << currentSong.title << currentSong.artist << currentSong.albumartist << currentSong.album << currentSong.track << currentSong.length;
	nowPlayingSent = true;
	lastNowPlaying = time(nullptr);
	if (fakeScrobbling) {
		DBUG << "MSG" << params;
	}
	else {
		QNetworkReply* job = NetworkAccessManager::self()->postFormData(QUrl(scrobblerUrl()), format(params));
		connect(job, SIGNAL(finished()), this, SLOT(handleResp()));
	}
}

void Scrobbler::handleResp()
{
	QNetworkReply* job = qobject_cast<QNetworkReply*>(sender());
	if (!job) {
		return;
	}
	job->deleteLater();
	DBUG << job->readAll();
}

void Scrobbler::scrobbleCurrent()
{
	if (!scrobbledCurrent) {
		if (songQueue.isEmpty() || songQueue.last() != currentSong) {
			songQueue.enqueue(currentSong);
		}
		scrobbledCurrent = true;
	}
	scrobbleQueued();
}

void Scrobbler::scrobbleQueued()
{
	if (!scrobblingEnabled || scrobbleViaMpd) {
		return;
	}
	if (!ensureAuthenticated() || scrobbleJob) {
		if (!retryTimer->isActive()) {
			retryTimer->start();
		}
		return;
	}

	if (!songQueue.isEmpty()) {
		QMap<QString, QString> params;
		params["method"] = "track.scrobble";
		int batchSize = qMin(constMaxBatchSize, songQueue.size());
		DBUG << "queued:" << songQueue.size() << "batchSize:" << batchSize;
		for (int i = 0; i < batchSize; ++i) {
			Track s = songQueue.takeAt(0);
			DBUG << s.artist << s.albumartist << s.album << s.title << s.track << s.length << s.timestamp;
			params[QString("track[%1]").arg(i)] = s.title;
			if (!s.album.isEmpty()) {
				params[QString("album[%1]").arg(i)] = s.album;
			}
			params[QString("artist[%1]").arg(i)] = s.artist;
			if (!s.albumartist.isEmpty() && s.albumartist != s.artist) {
				params[QString("albumArtist[%1]").arg(i)] = s.albumartist;
			}
			if (s.track) {
				params[QString("trackNumber[%1]").arg(i)] = QString::number(s.track);
			}
			if (s.length) {
				params[QString("duration[%1]").arg(i)] = QString::number(s.length);
			}
			params[QString("timestamp[%1]").arg(i)] = QString::number(s.timestamp);
			lastScrobbledSongs.append(s);
		}
		params["sk"] = sessionKey;
		sign(params);
		if (fakeScrobbling) {
			DBUG << "MSG" << params;
			lastScrobbledSongs.clear();
		}
		else {
			scrobbleJob = NetworkAccessManager::self()->postFormData(scrobblerUrl(), format(params));
			connect(scrobbleJob, SIGNAL(finished()), this, SLOT(scrobbleFinished()));
		}
	}
}

void Scrobbler::scrobbleFinished()
{
	QNetworkReply* job = qobject_cast<QNetworkReply*>(sender());

	if (!job) {
		return;
	}
	job->deleteLater();
	if (job == scrobbleJob) {
		QByteArray data = job->readAll();
		DBUG << job->errorString() << data << songQueue.size() << lastScrobbledSongs.size();
		scrobbleJob = nullptr;

		int errorCode = NoError;
		QXmlStreamReader reader(data);
		while (!reader.atEnd() && !reader.hasError()) {
			reader.readNext();
			if (reader.isStartElement()) {
				if (QLatin1String("lfm") == reader.name().toString()) {
					QString status = reader.attributes().value("status").toString().toLower();
					DBUG << status;
					if (QLatin1String("failed") == status) {
						while (!reader.atEnd() && !reader.hasError()) {
							reader.readNext();
							if (reader.isStartElement()) {
								if (QLatin1String("error") == reader.name().toString()) {
									errorCode = reader.attributes().value(QLatin1String("code")).toString().toInt();
									QString errorStr = errorString(errorCode, reader.readElementText());
									emit error(tr("%1 error: %2").arg(scrobbler).arg(errorStr));
									DBUG << errorStr;
									break;
								}
							}
						}
					}
					break;
				}
			}
		}

		switch (errorCode) {
		case NoError:
			failedCount = 0;
			DBUG << "Scrobble succeeded";
			lastScrobbledSongs.clear();
			return;
		case AuthenticationFailed:
		case InvalidSessionKey:
		case TokenNotAuthorised:
			sessionKey.clear();
			authenticate();
			failedCount = 0;
			break;
		default:
			if (++failedCount > 2 && !hardFailTimer->isActive()) {
				sessionKey.clear();
				hardFailTimer->setInterval((failedCount > 120 ? 120 : failedCount) * 60 * 1000);
				hardFailTimer->start();
			}
			break;
		}

		DBUG << "Move last scrobbled into queued";
		songQueue << lastScrobbledSongs;
		lastScrobbledSongs.clear();
	}
}

bool Scrobbler::ensureAuthenticated()
{
	if (fakeScrobbling || !sessionKey.isEmpty()) {
		return true;
	}
	authenticate();
	return false;
}

void Scrobbler::authenticate()
{
	if (fakeScrobbling) {
		return;
	}
	if (hardFailTimer->isActive() || authJob || scrobbleViaMpd) {
		DBUG << "authentication delayed";
		return;
	}

	if (!haveLoginDetails()) {
		DBUG << "no login details";
		return;
	}
	QUrl url(scrobblerUrl());
	QMap<QString, QString> params;
	params["method"] = "auth.getMobileSession";
	params["username"] = userName;

	bool supportsSsl = false;
#ifndef QT_NO_SSL
	supportsSsl = QSslSocket::supportsSsl();
#endif
	if (supportsSsl) {
		params["password"] = password;
		url.setScheme("https");// Use HTTPS to authenticate
	}
	else {
		params["authToken"] = md5(userName + md5(password));
	}
	sign(params);

	authJob = NetworkAccessManager::self()->postFormData(url, format(params));
	connect(authJob, SIGNAL(finished()), this, SLOT(authResp()));
	DBUG << url.toString();
}

void Scrobbler::authResp()
{
	QNetworkReply* job = qobject_cast<QNetworkReply*>(sender());

	if (!job) {
		return;
	}
	job->deleteLater();
	if (job != authJob) {
		return;
	}
	authJob = nullptr;
	sessionKey.clear();

	QByteArray data = job->readAll();
	DBUG << data;
	QXmlStreamReader reader(data);
	while (!reader.atEnd() && !reader.hasError()) {
		reader.readNext();
		if (reader.isStartElement()) {
			QString element = reader.name().toString();
			if (QLatin1String("session") == element) {
				while (!reader.atEnd() && !reader.hasError()) {
					reader.readNext();
					if (reader.isStartElement()) {
						element = reader.name().toString();
						if (QLatin1String("key") == element) {
							sessionKey = reader.readElementText();
							break;
						}
					}
				}
				break;
			}
			else if (QLatin1String("error") == element) {
				int code = reader.attributes().value(QLatin1String("code")).toString().toInt();
				emit error(tr("%1 error: %2").arg(scrobbler).arg(errorString(code, reader.readElementText())));
				break;
			}
		}
	}

	DBUG << "authenticated:" << !sessionKey.isEmpty();
	emit authenticated(isAuthenticated());
	Configuration cfg(constSettingsGroup);
	cfg.set("sessionKey", sessionKey);

	if (isAuthenticated()) {
		if (nowPlayingIsPending) {
			scrobbleNowPlaying();
		}
		if (lovePending) {
			love();
		}
	}
}

void Scrobbler::loadCache()
{
	QString fileName = cacheName(false);
	if (fileName.isEmpty()) {
		return;
	}
	KCompressionDevice file(fileName, KCompressionDevice::GZip);
	if (file.open(QIODevice::ReadOnly)) {
		QXmlStreamReader reader(&file);
		while (!reader.atEnd()) {
			reader.readNext();
			if (reader.isStartElement() && QLatin1String("track") == reader.name()) {
				Track t;
				t.artist = reader.attributes().value(QLatin1String("artist")).toString();
				t.album = reader.attributes().value(QLatin1String("album")).toString();
				t.albumartist = reader.attributes().value(QLatin1String("albumartist")).toString();
				t.title = reader.attributes().value(QLatin1String("title")).toString();
				t.track = reader.attributes().value(QLatin1String("track")).toString().toUInt();
				t.length = reader.attributes().value(QLatin1String("length")).toString().toUInt();
				t.timestamp = reader.attributes().value(QLatin1String("timestamp")).toString().toUInt();
				songQueue.append(t);
			}
		}
	}
	DBUG << fileName << songQueue.size();
}

void Scrobbler::saveCache()
{
	QString fileName = cacheName(true);
	DBUG << fileName << lastScrobbledSongs.count() << songQueue.count();
	if (fileName.isEmpty()) {
		return;
	}

	if (lastScrobbledSongs.isEmpty() && songQueue.isEmpty()) {
		if (QFile::exists(fileName)) {
			QFile::remove(fileName);
		}
		return;
	}

	KCompressionDevice file(fileName, KCompressionDevice::GZip);
	if (file.open(QIODevice::WriteOnly)) {
		QXmlStreamWriter writer(&file);
		writer.setAutoFormatting(false);
		writer.writeStartDocument();
		writer.writeStartElement("tracks");
		writer.writeAttribute("version", "1");

		const QQueue<Track>& queue = lastScrobbledSongs.isEmpty() ? songQueue : lastScrobbledSongs;
		for (const Track& t : queue) {
			writer.writeEmptyElement("track");
			writer.writeAttribute(QLatin1String("artist"), t.artist);
			writer.writeAttribute(QLatin1String("album"), t.album);
			writer.writeAttribute(QLatin1String("albumartist"), t.albumartist);
			writer.writeAttribute(QLatin1String("title"), t.title);
			writer.writeAttribute(QLatin1String("track"), QString::number(t.track));
			writer.writeAttribute(QLatin1String("length"), QString::number(t.length));
			writer.writeAttribute(QLatin1String("timestamp"), QString::number(t.timestamp));
		}
		writer.writeEndElement();
		writer.writeEndDocument();
	}
}

void Scrobbler::mpdStateUpdated(bool songChanged)
{
	if (isEnabled() && !scrobbleViaMpd) {
		DBUG << songChanged << lastState << MPDStatus::self()->state();
		bool stateChange = songChanged || lastState != MPDStatus::self()->state();
		bool isRepeat = !stateChange && MPDState_Playing == MPDStatus::self()->state() && MPDStatus::self()->timeElapsed() < 2;
		if (!stateChange && !isRepeat) {
			return;
		}
		lastState = MPDStatus::self()->state();
		switch (lastState) {
		case MPDState_Paused:
			scrobbleTimer->pause();
			nowPlayingTimer->pause();
			break;
		case MPDState_Playing: {
			time_t now = time(nullptr);
			currentSong.timestamp = now - MPDStatus::self()->timeElapsed();
			DBUG << "Timestamp:" << currentSong.timestamp << "scrobbledCurrent:" << scrobbledCurrent << "nowPlayingSent:" << nowPlayingSent
				 << "now:" << now << "lastNowPlaying:" << lastNowPlaying << "isRepeat:" << isRepeat;

			if (isRepeat) {
				calcScrobbleIntervals();
				nowPlayingSent = scrobbledCurrent = nowPlayingIsPending = false;
				lastNowPlaying = 0;
				scrobbleTimer->start();
				nowPlayingTimer->start();
				return;
			}
			if (!scrobbledCurrent) {
				scrobbleTimer->start();
			}
			// Send now playing if it has not already been sent, or if not scrobbled current track and its been
			// over X seconds since now paying was sent.
			if (!nowPlayingSent) {
				nowPlayingTimer->start();
			}
			else if (!scrobbledCurrent && ((now - lastNowPlaying) * 1000) > constNowPlayingInterval) {
				int remaining = (MPDStatus::self()->timeTotal() - MPDStatus::self()->timeElapsed()) * 1000;
				DBUG << "remaining:" << remaining;
				if (remaining > constNowPlayingInterval) {
					nowPlayingTimer->setInterval(constNowPlayingInterval);
					nowPlayingSent = false;
					nowPlayingTimer->start();
				}
			}
			break;
		}
		default:
			scrobbleTimer->stop();
			nowPlayingTimer->stop();
			nowPlayingTimer->setInterval(constNowPlayingInterval);
		}
	}
}

void Scrobbler::mpdStatusUpdated(const MPDStatusValues& vals)
{
	if (!vals.playlistLength) {
		currentSong.clear();
		emit songChanged(false);
	}
}

void Scrobbler::clientMessageFailed(const QString& client, const QString& msg)
{
	if (loveSent && client == scrobblerUrl() && msg == QLatin1String("love")) {
		// 'love' failed, so re-enable...
		loveSent = lovePending = false;
		emit songChanged(true);
	}
}

void Scrobbler::cancelJobs()
{
	if (authJob) {
		disconnect(authJob, SIGNAL(finished()), this, SLOT(authResp()));
		authJob->close();
		authJob->abort();
		authJob->deleteLater();
		authJob = nullptr;
	}
	if (scrobbleJob) {
		disconnect(scrobbleJob, SIGNAL(finished()), this, SLOT(scrobbleFinished()));
		authJob->close();
		authJob->abort();
		authJob->deleteLater();
		scrobbleJob = nullptr;
	}
}

void Scrobbler::reset()
{
	songQueue.clear();
	lastScrobbledSongs.clear();
	saveCache();
	cancelJobs();
}

void Scrobbler::loadScrobblers()
{
	if (scrobblers.isEmpty()) {
		QStringList files;
		QString userDir = Utils::dataDir();

		if (!userDir.isEmpty()) {
			files.append(Utils::fixPath(userDir) + QLatin1String("scrobblers.xml"));
		}

		files.append(":scrobblers.xml");

		for (const auto& f : files) {
			QFile file(f);
			if (file.open(QIODevice::ReadOnly)) {
				QXmlStreamReader doc(&file);
				while (!doc.atEnd()) {
					doc.readNext();
					if (doc.isStartElement() && QLatin1String("scrobbler") == doc.name()) {
						QString name = doc.attributes().value("name").toString();
						QString url = doc.attributes().value("url").toString();
						if (!name.isEmpty() && !url.isEmpty() && !scrobblers.contains(name)) {
							scrobblers.insert(name, url);
						}
					}
				}
			}
		}
	}
}

QString Scrobbler::scrobblerUrl()
{
	loadScrobblers();
	if (scrobblers.isEmpty()) {
		return QString();
	}

	if (!scrobblers.contains(scrobbler)) {
		return scrobblers.constBegin().value();
	}
	return scrobblers[scrobbler];
}

#include "moc_scrobbler.cpp"
