/*
 * Cantata
 *
 * Copyright (c) 2011-2022 Craig Drummond <craig.p.drummond@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 "podcastservice.h"
#include "config.h"
#include "gui/settings.h"
#include "gui/stdactions.h"
#include "http/httpserver.h"
#include "models/playqueuemodel.h"
#include "models/roles.h"
#include "mpd-interface/mpdconnection.h"
#include "network/networkaccessmanager.h"
#include "podcastsettingsdialog.h"
#include "rssparser.h"
#include "support/globalstatic.h"
#include "support/utils.h"
#include "widgets/icons.h"
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QLocale>
#include <QMimeData>
#include <QSet>
#include <QTextDocument>
#include <QTimer>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include <stdio.h>
#ifdef BUNDLED_KARCHIVE
#include <kcompressiondevice.h>
#else
#include <KCompressionDevice>
#endif

GLOBAL_STATIC(PodcastService, instance)

static QString encodeName(const QString& name)
{
	QString n = name;
	n = n.replace("\\", "_");
	n = n.replace(":", "_");
	return n;
}

static inline QString episodeFileName(const QUrl& url)
{
	return url.path().split('/', CANTATA_SKIP_EMPTY).join('_').replace('~', '_');
}

PodcastService::Proxy::Proxy(QObject* parent)
	: ProxyModel(parent), unplayedOnly(false)
{
	setDynamicSortFilter(true);
	setFilterCaseSensitivity(Qt::CaseInsensitive);
	setSortCaseSensitivity(Qt::CaseInsensitive);
	setSortLocaleAware(true);
}

void PodcastService::Proxy::showUnplayedOnly(bool on)
{
	if (on != unplayedOnly) {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 10, 0))
		beginFilterChange();
		unplayedOnly = on;
		endFilterChange();
#else
		unplayedOnly = on;
		invalidateFilter();
#endif
	}
}

bool PodcastService::Proxy::lessThan(const QModelIndex& left, const QModelIndex& right) const
{
	if (left.row() < 0 || right.row() < 0) {
		return left.row() < 0;
	}

	if (!static_cast<Item*>(left.internalPointer())->isPodcast()) {
		Episode* l = static_cast<Episode*>(left.internalPointer());
		Episode* r = static_cast<Episode*>(right.internalPointer());

		if (l->publishedDate != r->publishedDate) {
			return l->publishedDate > r->publishedDate;
		}
	}

	return QSortFilterProxyModel::lessThan(left, right);
}

bool PodcastService::Proxy::filterAcceptsPodcast(const Podcast* pod) const
{
	for (const Episode* ep : pod->episodes) {
		if (filterAcceptsEpisode(ep)) {
			return true;
		}
	}

	return false;
}

bool PodcastService::Proxy::filterAcceptsEpisode(const Episode* item) const
{
	return (!unplayedOnly || (unplayedOnly && !item->played)) && matchesFilter(QStringList() << item->name << item->parent->name);
}

bool PodcastService::Proxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
	if (!filterEnabled && !unplayedOnly) {
		return true;
	}

	const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
	const PodcastService::Item* item = static_cast<const PodcastService::Item*>(index.internalPointer());

	if (filterStrings.isEmpty() && !unplayedOnly) {
		return true;
	}

	if (item->isPodcast()) {
		return filterAcceptsPodcast(static_cast<const Podcast*>(item));
	}
	return filterAcceptsEpisode(static_cast<const Episode*>(item));
}

const QLatin1String PodcastService::constName("podcasts");
static const QLatin1String constExt(".xml.gz");
static const char* constNewFeedProperty = "new-feed";
static const char* constRssUrlProperty = "rss-url";
static const char* constDestProperty = "dest";
static const QLatin1String constPartialExt(".partial");

static QString generateFileName(const QUrl& url, bool creatingNew)
{
	QString hash = QCryptographicHash::hash(url.toString().toUtf8(), QCryptographicHash::Md5).toHex();
	QString dir = Utils::dataDir(PodcastService::constName, true);
	QString fileName = dir + hash + constExt;

	if (creatingNew) {
		int i = 0;
		while (QFile::exists(fileName) && i < 100) {
			fileName = dir + hash + QChar('_') + QString::number(i) + constExt;
			i++;
		}
	}

	return fileName;
}

bool PodcastService::isPodcastFile(const QString& file)
{
	if (file.startsWith(Utils::constDirSep) && MPDConnection::self()->getDetails().isLocal()) {
		QString downloadPath = Settings::self()->podcastDownloadPath();
		if (downloadPath.isEmpty()) {
			return false;
		}
		return file.startsWith(downloadPath);
	}
	return false;
}

QUrl PodcastService::fixUrl(const QString& url)
{
	QString trimmed(url.trimmed());

	// Thanks gpodder!
	static QMap<QString, QString> prefixMap;
	if (prefixMap.isEmpty()) {
		prefixMap.insert(QLatin1String("fb:"), QLatin1String("http://feeds.feedburner.com/%1"));
		prefixMap.insert(QLatin1String("yt:"), QLatin1String("http://www.youtube.com/rss/user/%1/videos.rss"));
		prefixMap.insert(QLatin1String("sc:"), QLatin1String("http://soundcloud.com/%1"));
		prefixMap.insert(QLatin1String("fm4od:"), QLatin1String("http://onapp1.orf.at/webcam/fm4/fod/%1.xspf"));
		prefixMap.insert(QLatin1String("ytpl:"), QLatin1String("http://gdata.youtube.com/feeds/api/playlists/%1"));
	}

	QMap<QString, QString>::ConstIterator it(prefixMap.constBegin());
	QMap<QString, QString>::ConstIterator end(prefixMap.constEnd());
	for (; it != end; ++it) {
		if (trimmed.startsWith(it.key())) {
			trimmed = it.value().arg(trimmed.mid(it.key().length()));
		}
	}

	if (!trimmed.contains(QLatin1String("://"))) {
		trimmed.prepend(QLatin1String("http://"));
	}

	return fixUrl(QUrl(trimmed));
}

QUrl PodcastService::fixUrl(const QUrl& orig)
{
	QUrl u = orig;
	if (u.scheme().isEmpty() || QLatin1String("itpc") == u.scheme() || QLatin1String("pcast") == u.scheme() || QLatin1String("feed") == u.scheme() || QLatin1String("itms") == u.scheme()) {
		u.setScheme(QLatin1String("http"));
	}
	return u;
}

Song PodcastService::Episode::toSong() const
{
	Song song;
	song.title = name;
	song.file = url.toString();
	song.album = parent->name;
	song.time = duration;
	song.setExtraField(Song::OnlineImageUrl, parent->imageUrl.toString());
	song.setExtraField(Song::OnlineImageCacheName, parent->imageFile);
	if (!localFile.isEmpty()) {
		song.setLocalPath(localFile);
	}
	return song;
}

PodcastService::Podcast::Podcast(const QString& f)
	: unplayedCount(0), fileName(f), imageFile(f)
{
	imageFile = imageFile.replace(constExt, ".jpg");
}

static QLatin1String constTopTag("podcast");
static QLatin1String constImageAttribute("img");
static QLatin1String constRssAttribute("rss");
static QLatin1String constEpisodeTag("episode");
static QLatin1String constNameAttribute("name");
static QLatin1String constDescrAttribute("descr");
static QLatin1String constDateAttribute("date");
static QLatin1String constUrlAttribute("url");
static QLatin1String constTimeAttribute("time");
static QLatin1String constPlayedAttribute("played");
static QLatin1String constLocalAttribute("local");
static QLatin1String constTrue("true");

bool PodcastService::Podcast::load()
{
	if (fileName.isEmpty()) {
		return false;
	}

	KCompressionDevice file(fileName, KCompressionDevice::GZip);
	if (!file.open(QIODevice::ReadOnly)) {
		return false;
	}

	QXmlStreamReader reader(&file);
	unplayedCount = 0;

	QString podPath = Settings::self()->podcastDownloadPath();
	while (!reader.atEnd()) {
		reader.readNext();
		if (!reader.error() && reader.isStartElement()) {
			QString element = reader.name().toString();
			QXmlStreamAttributes attributes = reader.attributes();

			if (constTopTag == element) {
				imageUrl = attributes.value(constImageAttribute).toString();
				url = attributes.value(constRssAttribute).toString();
				name = attributes.value(constNameAttribute).toString();
				descr = attributes.value(constDescrAttribute).toString();
				if (url.isEmpty() || name.isEmpty()) {
					return false;
				}
				if (!podPath.isEmpty()) {
					podPath = Utils::fixPath(podPath) + Utils::fixPath(encodeName(name));
				}
			}
			else if (constEpisodeTag == element) {
				QString epName = attributes.value(constNameAttribute).toString();
				QString epUrl = attributes.value(constUrlAttribute).toString();
				if (!epName.isEmpty() && !epUrl.isEmpty()) {
					Episode* ep = new Episode(QDateTime::fromString(attributes.value(constDateAttribute).toString(), Qt::ISODate), epName, epUrl, this);
					QString localFile = attributes.value(constLocalAttribute).toString();
					QString time = attributes.value(constTimeAttribute).toString();

					ep->duration = time.isEmpty() ? 0 : time.toUInt();
					ep->played = constTrue == attributes.value(constPlayedAttribute).toString();
					ep->descr = attributes.value(constDescrAttribute).toString();
					if (!localFile.isEmpty() && QFile::exists(localFile)) {
						ep->localFile = localFile;
					}
					else if (!podPath.isEmpty()) {
						QString localPath = podPath + episodeFileName(ep->url);
						if (QFile::exists(localPath)) {
							ep->localFile = localPath;
						}
					}

					episodes.append(ep);
					if (!ep->played) {
						unplayedCount++;
					}
				}
			}
		}
	}

	return true;
}

bool PodcastService::Podcast::save() const
{
	if (fileName.isEmpty()) {
		return false;
	}

	KCompressionDevice file(fileName, KCompressionDevice::GZip);
	if (!file.open(QIODevice::WriteOnly)) {
	    return false;
	}

	QXmlStreamWriter writer(&file);
	writer.writeStartElement(constTopTag);
	writer.writeAttribute(constImageAttribute, imageUrl.toString());// ??
	writer.writeAttribute(constRssAttribute, url.toString());       // ??
	writer.writeAttribute(constNameAttribute, name);
	writer.writeAttribute(constDescrAttribute, descr);
	for (Episode* ep : episodes) {
		writer.writeStartElement(constEpisodeTag);
		writer.writeAttribute(constNameAttribute, ep->name);
		writer.writeAttribute(constDescrAttribute, ep->descr);
		writer.writeAttribute(constUrlAttribute, ep->url.toString());// ??
		if (ep->duration) {
			writer.writeAttribute(constTimeAttribute, QString::number(ep->duration));
		}
		if (ep->played) {
			writer.writeAttribute(constPlayedAttribute, constTrue);
		}
		if (ep->publishedDate.isValid()) {
			writer.writeAttribute(constDateAttribute, ep->publishedDate.toString(Qt::ISODate));
		}
		if (!ep->localFile.isEmpty()) {
			writer.writeAttribute(constLocalAttribute, ep->localFile);
		}
		writer.writeEndElement();
	}
	writer.writeEndElement();
	return true;
}

void PodcastService::Podcast::add(Episode* ep)
{
	ep->parent = this;
	episodes.append(ep);
}

void PodcastService::Podcast::add(QList<Episode*>& eps)
{
	for (Episode* ep : eps) {
		add(ep);
	}
	setUnplayedCount();
}

PodcastService::Episode* PodcastService::Podcast::getEpisode(const QUrl& epUrl) const
{
	for (Episode* episode : episodes) {
		if (episode->url == epUrl) {
			return episode;
		}
	}
	return nullptr;
}

void PodcastService::Podcast::setUnplayedCount()
{
	unplayedCount = episodes.count();
	for (Episode* episode : episodes) {
		if (episode->played) {
			unplayedCount--;
		}
	}
}

void PodcastService::Podcast::removeFiles()
{
	if (!fileName.isEmpty() && QFile::exists(fileName)) {
		QFile::remove(fileName);
	}
	if (!imageFile.isEmpty() && QFile::exists(imageFile)) {
		QFile::remove(imageFile);
	}
}

const Song& PodcastService::Podcast::coverSong()
{
	if (song.isEmpty()) {
		song.artist = constName;
		song.album = name;
		song.title = name;
		song.type = Song::OnlineSvrTrack;
		song.setIsFromOnlineService(constName);
		song.setExtraField(Song::OnlineImageUrl, imageUrl.toString());
		song.setExtraField(Song::OnlineImageCacheName, imageFile);
		song.file = url.toString();
	}
	return song;
}

PodcastService::PodcastService()
	: ActionModel(nullptr), downloadJob(nullptr), rssUpdateTimer(nullptr)
{
	QMetaObject::invokeMethod(this, "loadAll", Qt::QueuedConnection);
	icn = Icon::fa(fa::fa_solid, fa::fa_rss_square);
	useCovers(name(), true);
	clearPartialDownloads();
	connect(MPDConnection::self(), SIGNAL(currentSongUpdated(const Song&)), this, SLOT(currentMpdSong(const Song&)));
	refreshAction = new Action(Icons::self()->reloadIcon, tr("Refresh"), this);
}

QString PodcastService::name() const
{
	return constName;
}

QString PodcastService::title() const
{
	return QLatin1String("Podcasts");
}

QString PodcastService::descr() const
{
	return tr("Subscribe to RSS feeds");
}

bool PodcastService::episodeCover(const Song& s, QImage& img, QString& imgFilename) const
{
	if ((s.isFromOnlineService() && s.onlineService() == constName) || isPodcastFile(s.file)) {
		QString path = s.decodedPath();
		if (path.isEmpty()) {
			path = s.file;
		}
		for (Podcast* podcast : podcasts) {
			for (Episode* episode : podcast->episodes) {
				if (episode->url == path || episode->localFile == path) {
					imgFilename = podcast->imageFile;
					img = QImage(imgFilename);
					return true;
				}
			}
		}
	}
	return false;
}

QString PodcastService::episodeDescr(const Song& s) const
{
	if ((s.isFromOnlineService() && s.onlineService() == constName) || isPodcastFile(s.file)) {
		QString path = s.decodedPath();
		if (path.isEmpty()) {
			path = s.file;
		}
		for (Podcast* podcast : podcasts) {
			for (Episode* episode : podcast->episodes) {
				if (episode->url == path || episode->localFile == path) {
					return episode->descr.replace("<p><br></p>", "");
				}
			}
		}
	}
	return QString();
}

int PodcastService::rowCount(const QModelIndex& index) const
{
	if (index.column() > 0) {
		return 0;
	}

	if (!index.isValid()) {
		return podcasts.size();
	}

	Item* item = static_cast<Item*>(index.internalPointer());
	if (item->isPodcast()) {
		return static_cast<Podcast*>(index.internalPointer())->episodes.count();
	}
	return 0;
}

bool PodcastService::hasChildren(const QModelIndex& parent) const
{
	return !parent.isValid() || static_cast<Item*>(parent.internalPointer())->isPodcast();
}

QModelIndex PodcastService::parent(const QModelIndex& index) const
{
	if (!index.isValid()) {
		return QModelIndex();
	}

	Item* item = static_cast<Item*>(index.internalPointer());

	if (item->isPodcast()) {
		return QModelIndex();
	}
	else {
		Episode* episode = static_cast<Episode*>(item);

		if (episode->parent) {
			return createIndex(podcasts.indexOf(episode->parent), 0, episode->parent);
		}
	}

	return QModelIndex();
}

QModelIndex PodcastService::index(int row, int col, const QModelIndex& parent) const
{
	if (!hasIndex(row, col, parent)) {
		return QModelIndex();
	}

	if (parent.isValid()) {
		Item* p = static_cast<Item*>(parent.internalPointer());

		if (p->isPodcast()) {
			Podcast* podcast = static_cast<Podcast*>(p);
			return row < podcast->episodes.count() ? createIndex(row, col, podcast->episodes.at(row)) : QModelIndex();
		}
	}

	return row < podcasts.count() ? createIndex(row, col, podcasts.at(row)) : QModelIndex();
}

static QString trimDescr(QString descr, int limit = 1000)
{
	if (!descr.isEmpty()) {
		QTextDocument doc;
		doc.setHtml(descr);
		descr = doc.toPlainText().simplified();
		if (descr.length() > limit) {
			descr = descr.left(limit) + QLatin1String("...");
		}
		descr = QLatin1String("<br/>") + descr + QLatin1String("<br/><br/>");
	}
	return descr;
}

QVariant PodcastService::data(const QModelIndex& index, int role) const
{
	if (!index.isValid()) {
		switch (role) {
		case Cantata::Role_TitleText:
			return title();
		case Cantata::Role_SubText:
			return tr("%n Podcast(s)", "", podcasts.count());
		case Qt::DecorationRole:
			return icon();
		}
		return QVariant();
	}

	Item* item = static_cast<Item*>(index.internalPointer());

	if (item->isPodcast()) {
		Podcast* podcast = static_cast<Podcast*>(item);

		switch (role) {
		case Cantata::Role_Actions: {
			QVariant v;
			v.setValue(QList<Action*>() << StdActions::self()->replacePlayQueueAction << StdActions::self()->appendToPlayQueueAction << refreshAction);
			return v;
		}
		case Cantata::Role_ListImage:
			return true;
		case Cantata::Role_CoverSong: {
			QVariant v;
			v.setValue(podcast->coverSong());
			return v;
		}
		case Qt::DecorationRole:
			return Icons::self()->podcastIcon;
		case Cantata::Role_LoadCoverInUIThread:
			return true;
		case Cantata::Role_MainText:
		case Qt::DisplayRole:
			return tr("%1 (%2)", "podcast name (num unplayed episodes)").arg(podcast->name).arg(podcast->unplayedCount);
		case Cantata::Role_SubText:
			return tr("%n Episode(s)", "", podcast->episodes.count());
		case Qt::ToolTipRole:
			if (Settings::self()->infoTooltips()) {
				return podcast->name + QLatin1String("<br/>") + trimDescr(podcast->descr) + tr("%n Episode(s)", "", podcast->episodes.count());
			}
			break;
		case Qt::FontRole:
			if (podcast->unplayedCount > 0) {
				QFont f;
				f.setBold(true);
				return f;
			}
		default:
			break;
		}
	}
	else {
		Episode* episode = static_cast<Episode*>(item);

		switch (role) {
		case Qt::DecorationRole:
			if (!episode->localFile.isEmpty()) {
				return Icons::self()->savedRssListIcon;
			}
			else if (episode->downloadProg >= 0) {
				return Icons::self()->downloadIcon;
			}
			else if (Episode::QueuedForDownload == episode->downloadProg) {
				return Icons::self()->clockIcon;
			}
			else {
				return Icons::self()->rssListIcon;
			}
		case Cantata::Role_MainText:
		case Qt::DisplayRole:
			return episode->name;
		case Cantata::Role_SubText:
			if (episode->downloadProg >= 0) {
				return Utils::formatTime(episode->duration, true) + QLatin1Char(' ') + tr("(Downloading: %1%)").arg(episode->downloadProg);
			}
			return QLocale().toString(episode->publishedDate, QLocale::LongFormat) + (0 == episode->duration ? QString() : (QLatin1String(" (") + Utils::formatTime(episode->duration, true) + QLatin1Char(')')));
		case Qt::ToolTipRole:
			if (Settings::self()->infoTooltips()) {
				return QLatin1String("<b>") + episode->parent->name + QLatin1String("</b><br/>") + episode->name + QLatin1String("<br/>") + trimDescr(episode->descr) + Utils::formatTime(episode->duration, true) + QLatin1String("<br/>") + QLocale().toString(episode->publishedDate, QLocale::LongFormat);
			}
			break;
		case Qt::FontRole:
			if (!episode->played) {
				QFont f;
				f.setBold(true);
				return f;
			}
		default:
			break;
		}
	}

	return ActionModel::data(index, role);
}

Qt::ItemFlags PodcastService::flags(const QModelIndex& index) const
{
	if (index.isValid()) {
		return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
	}
	return Qt::NoItemFlags;
}

QMimeData* PodcastService::mimeData(const QModelIndexList& indexes) const
{
	QMimeData* mimeData = nullptr;
	QStringList paths = filenames(indexes);

	if (!paths.isEmpty()) {
		mimeData = new QMimeData();
		PlayQueueModel::encode(*mimeData, PlayQueueModel::constUriMimeType, paths);
	}
	return mimeData;
}

QStringList PodcastService::filenames(const QModelIndexList& indexes, bool allowPlaylists) const
{
	Q_UNUSED(allowPlaylists)
	QList<Song> songList = songs(indexes);
	QStringList files;
	for (const Song& song : songList) {
		files.append(song.file);
	}
	return files;
}

QList<Song> PodcastService::songs(const QModelIndexList& indexes, bool allowPlaylists) const
{
	Q_UNUSED(allowPlaylists)
	QList<Song> songs;
	QSet<Item*> selectedPodcasts;

	for (const QModelIndex& idx : indexes) {
		Item* item = static_cast<Item*>(idx.internalPointer());
		if (item->isPodcast()) {
			Podcast* podcast = static_cast<Podcast*>(item);
			for (const Episode* episode : podcast->episodes) {
				selectedPodcasts.insert(item);
				Song s = episode->toSong();
				songs.append(fixPath(s));
			}
		}
	}
	for (const QModelIndex& idx : indexes) {
		Item* item = static_cast<Item*>(idx.internalPointer());
		if (!item->isPodcast()) {
			Episode* episode = static_cast<Episode*>(item);
			if (!selectedPodcasts.contains(episode->parent)) {
				Song s = episode->toSong();
				songs.append(fixPath(s));
			}
		}
	}
	return songs;
}

Song& PodcastService::fixPath(Song& song) const
{
	song.setIsFromOnlineService(constName);
	song.artist = title();
	if (!song.localPath().isEmpty() && QFile::exists(song.localPath())) {
		if (HttpServer::self()->isAlive()) {
			song.file = song.localPath();
			song.file = HttpServer::self()->encodeUrl(song);
		}
		else if (MPDConnection::self()->localFilePlaybackSupported()) {
			song.file = QLatin1String("file://") + song.localPath();
		}
		return song;
	}
	return encode(song);
}

void PodcastService::clear()
{
	cancelAllJobs();
	beginResetModel();
	qDeleteAll(podcasts);
	podcasts.clear();
	endResetModel();
	emit dataChanged(QModelIndex(), QModelIndex());
}

void PodcastService::loadAll()
{
	beginResetModel();
	QString dir = Utils::dataDir(constName);

	if (!dir.isEmpty()) {
		QDir d(dir);
		QStringList entries = d.entryList(QStringList() << "*" + constExt, QDir::Files | QDir::Readable | QDir::NoDot | QDir::NoDotDot);
		for (const QString& e : entries) {
			Podcast* podcast = new Podcast(dir + e);
			if (podcast->load()) {
				podcasts.append(podcast);
			}
			else {
				delete podcast;
			}
		}
		startRssUpdateTimer();
	}
	endResetModel();
	emit dataChanged(QModelIndex(), QModelIndex());
}

void PodcastService::cancelAll()
{
	for (NetworkJob* j : rssJobs) {
		disconnect(j, SIGNAL(finished()), this, SLOT(rssJobFinished()));
		j->cancelAndDelete();
	}
	rssJobs.clear();
	cancelAllDownloads();
}

void PodcastService::rssJobFinished()
{
	NetworkJob* j = dynamic_cast<NetworkJob*>(sender());
	if (!j || !rssJobs.contains(j)) {
		return;
	}

	j->deleteLater();
	rssJobs.removeAll(j);
	bool isNew = j->property(constNewFeedProperty).toBool();

	if (j->ok()) {
		if (updateUrls.contains(j->origUrl())) {
			updateUrls.remove(j->origUrl());
			if (updateUrls.isEmpty()) {
				lastRssUpdate = QDateTime::currentDateTime();
				Settings::self()->saveLastRssUpdate(lastRssUpdate);
				startRssUpdateTimer();
			}
		}

		RssParser::Channel ch = RssParser::parse(j->actualJob());

		if (!ch.isValid()) {
			if (isNew) {
				emit newError(tr("Failed to parse %1").arg(j->origUrl().toString()));
			}
			else {
				emit error(tr("Failed to parse %1").arg(j->origUrl().toString()));
			}
			return;
		}

		if (ch.video) {
			if (isNew) {
				emit newError(tr("Cantata only supports audio podcasts! %1 contains only video podcasts.").arg(j->origUrl().toString()));
			}
			else {
				emit error(tr("Cantata only supports audio podcasts! %1 contains only video podcasts.").arg(j->origUrl().toString()));
			}
			return;
		}

		int autoDownload = Settings::self()->podcastAutoDownloadLimit();

		if (isNew) {
			Podcast* podcast = new Podcast();
			podcast->url = j->origUrl();
			podcast->fileName = podcast->imageFile = generateFileName(podcast->url, true);
			podcast->imageFile = podcast->imageFile.replace(constExt, ".jpg");
			podcast->imageUrl = ch.image.toString();
			podcast->name = ch.name;
			podcast->descr = ch.description;
			podcast->unplayedCount = ch.episodes.count();

			QString podPath = Settings::self()->podcastDownloadPath();
			if (!podPath.isEmpty()) {
				podPath = Utils::fixPath(podPath) + Utils::fixPath(encodeName(podcast->name));
			}

			for (const RssParser::Episode& ep : ch.episodes) {
				Episode* episode = new Episode(ep.publicationDate, ep.name, ep.url, podcast);
				episode->duration = ep.duration;
				episode->descr = ep.description;
				if (!podPath.isEmpty()) {
					// Check if we had subscribed to this before, and downloaded episodes...
					QString localPath = podPath + episodeFileName(episode->url);
					if (QFile::exists(localPath)) {
						episode->localFile = localPath;
					}
				}
				podcast->add(episode);
			}
			podcast->save();
			beginInsertRows(QModelIndex(), podcasts.count(), podcasts.count());
			podcasts.append(podcast);
			emit dataChanged(QModelIndex(), QModelIndex());
			if (autoDownload) {
				int ep = 0;
				for (Episode* episode : podcast->episodes) {
					downloadEpisode(podcast, QUrl(episode->url));
					if (autoDownload < 1000 && ++ep >= autoDownload) {
						break;
					}
				}
			}
			endInsertRows();
		}
		else {
			Podcast* podcast = getPodcast(j->origUrl());
			if (!podcast) {
				return;
			}
			QSet<QUrl> newEpisodes;
			QSet<QUrl> oldEpisodes;
			for (Episode* episode : podcast->episodes) {
				newEpisodes.insert(episode->url);
			}
			for (const RssParser::Episode& ep : ch.episodes) {
				oldEpisodes.insert(ep.url);
			}

			QSet<QUrl> added = oldEpisodes - newEpisodes;
			QSet<QUrl> removed = newEpisodes - oldEpisodes;
			if (added.count() || removed.count()) {
				QModelIndex podcastIndex = createIndex(podcasts.indexOf(podcast), 0, (void*)podcast);
				if (removed.count()) {
					for (const QUrl& s : removed) {
						Episode* episode = podcast->getEpisode(s);
						if (episode->localFile.isEmpty() || !QFile::exists(episode->localFile)) {
							int idx = podcast->episodes.indexOf(episode);
							if (-1 != idx) {
								beginRemoveRows(podcastIndex, idx, idx);
								podcast->episodes.removeAt(idx);
								delete episode;
								endRemoveRows();
							}
						}
					}
				}
				if (added.count()) {
					beginInsertRows(podcastIndex, podcast->episodes.count(), (podcast->episodes.count() + added.count()) - 1);

					for (const RssParser::Episode& ep : ch.episodes) {
						QString epUrl = ep.url.toString();
						if (added.contains(epUrl)) {
							Episode* episode = new Episode(ep.publicationDate, ep.name, ep.url, podcast);
							episode->duration = ep.duration;
							episode->descr = ep.description;
							podcast->add(episode);
						}
					}
					endInsertRows();
				}

				podcast->setUnplayedCount();
				podcast->save();
				emit dataChanged(podcastIndex, podcastIndex);
			}
		}
	}
	else {
		if (isNew) {
			emit newError(tr("Failed to download %1").arg(j->origUrl().toString()));
		}
		else {
			emit error(tr("Failed to download %1").arg(j->origUrl().toString()));
		}
	}
}

void PodcastService::configure(QWidget* p)
{
	PodcastSettingsDialog dlg(p);
	if (QDialog::Accepted == dlg.exec()) {
		int changes = dlg.changes();
		if (changes & PodcastSettingsDialog::RssUpdate) {
			startRssUpdateTimer();
		}
	}
}

PodcastService::Podcast* PodcastService::getPodcast(const QUrl& url) const
{
	for (Podcast* podcast : podcasts) {
		if (podcast->url == url) {
			return podcast;
		}
	}
	return nullptr;
}

void PodcastService::unSubscribe(Podcast* podcast)
{
	int row = podcasts.indexOf(podcast);
	if (row >= 0) {
		QList<Episode*> episodes;
		for (Episode* e : podcast->episodes) {
			episodes.append(e);
		}
		cancelDownloads(episodes);
		beginRemoveRows(QModelIndex(), row, row);
		podcast->removeFiles();
		delete podcasts.takeAt(row);
		endRemoveRows();
		emit dataChanged(QModelIndex(), QModelIndex());
		if (podcasts.isEmpty()) {
			stopRssUpdateTimer();
		}
	}
}

void PodcastService::refresh(const QModelIndexList& list)
{
	for (const QModelIndex& idx : list) {
		Item* itm = static_cast<Item*>(idx.internalPointer());
		if (itm->isPodcast()) {
			refreshSubscription(static_cast<Podcast*>(itm));
		}
	}
}

void PodcastService::refreshAll()
{
	for (Podcast* pod : podcasts) {
		refreshSubscription(pod);
	}
}

void PodcastService::refreshSubscription(Podcast* item)
{
	if (item) {
		QUrl url = item->url;
		if (processingUrl(url)) {
			return;
		}
		addUrl(url, false);
	}
	else {
		updateRss();
	}
}

bool PodcastService::processingUrl(const QUrl& url) const
{
	for (NetworkJob* j : rssJobs) {
		if (j->origUrl() == url) {
			return true;
		}
	}
	return false;
}

void PodcastService::addUrl(const QUrl& url, bool isNew)
{
	NetworkJob* job = NetworkAccessManager::self()->get(url);
	connect(job, SIGNAL(finished()), this, SLOT(rssJobFinished()));
	job->setProperty(constNewFeedProperty, isNew);
	rssJobs.append(job);
}

bool PodcastService::downloadingEpisode(const QUrl& url) const
{
	if (downloadJob && downloadJob->origUrl() == url) {
		return true;
	}
	return toDownload.contains(url);
}

void PodcastService::cancelAllDownloads()
{
	for (const DownloadEntry& e : toDownload) {
		updateEpisode(e.rssUrl, e.url, Episode::NotDownloading);
	}

	toDownload.clear();
	cancelDownload();
}

void PodcastService::downloadPodcasts(Podcast* pod, const QList<Episode*>& episodes)
{
	for (Episode* ep : episodes) {
		downloadEpisode(pod, QUrl(ep->url));
	}
}

void PodcastService::deleteDownloadedPodcasts(Podcast* pod, const QList<Episode*>& episodes)
{
	cancelDownloads(episodes);
	bool modified = false;
	for (Episode* ep : episodes) {
		QString fileName = ep->localFile;
		if (!fileName.isEmpty()) {
			if (QFile::exists(fileName)) {
				QFile::remove(fileName);
			}
			QString dirName = fileName.isEmpty() ? QString() : Utils::getDir(fileName);
			if (!dirName.isEmpty()) {
				QDir dir(dirName);
				if (dir.exists()) {
					dir.rmdir(dirName);
				}
			}
			ep->localFile = QString();
			ep->downloadProg = Episode::NotDownloading;
			QModelIndex idx = createIndex(pod->episodes.indexOf(ep), 0, (void*)ep);
			emit dataChanged(idx, idx);
			modified = true;
		}
	}
	if (modified) {
		QModelIndex idx = createIndex(podcasts.indexOf(pod), 0, (void*)pod);
		emit dataChanged(idx, idx);
		pod->save();
	}
}

void PodcastService::setPodcastsAsListened(Podcast* pod, const QList<Episode*>& episodes, bool listened)
{
	bool modified = false;
	for (Episode* ep : episodes) {
		if (listened != ep->played) {
			ep->played = listened;
			QModelIndex idx = createIndex(pod->episodes.indexOf(ep), 0, (void*)ep);
			emit dataChanged(idx, idx);
			modified = true;
			if (listened) {
				pod->unplayedCount--;
			}
			else {
				pod->unplayedCount++;
			}
		}
	}
	if (modified) {
		QModelIndex idx = createIndex(podcasts.indexOf(pod), 0, (void*)pod);
		emit dataChanged(idx, idx);
		pod->save();
	}
}

void PodcastService::downloadEpisode(const Podcast* podcast, const QUrl& episode)
{
	QString dest = Settings::self()->podcastDownloadPath();
	if (dest.isEmpty()) {
		return;
	}
	if (downloadingEpisode(episode)) {
		return;
	}
	dest = Utils::fixPath(dest) + Utils::fixPath(encodeName(podcast->name)) + episodeFileName(episode);
	toDownload.append(DownloadEntry(episode, podcast->url, dest));
	updateEpisode(podcast->url, episode, Episode::QueuedForDownload);
	doNextDownload();
}

void PodcastService::cancelDownloads(const QList<Episode*> episodes)
{
	bool cancelDl = false;
	for (Episode* e : episodes) {
		toDownload.removeAll(e->url);
		e->downloadProg = Episode::NotDownloading;
		QModelIndex idx = createIndex(e->parent->episodes.indexOf(e), 0, (void*)e);
		emit dataChanged(idx, idx);
		if (!cancelDl && downloadJob && downloadJob->url() == e->url) {
			cancelDl = true;
		}
	}
	if (cancelDl) {
		cancelDownload();
	}
}

void PodcastService::cancelDownload(const QUrl& url)
{
	if (downloadJob && downloadJob->origUrl() == url) {
		cancelDownload();
		doNextDownload();
	}
}

void PodcastService::cancelDownload()
{
	if (downloadJob) {
		downloadJob->cancelAndDelete();
		disconnect(downloadJob, SIGNAL(finished()), this, SLOT(downloadJobFinished()));
		disconnect(downloadJob, SIGNAL(readyRead()), this, SLOT(downloadReadyRead()));
		disconnect(downloadJob, SIGNAL(downloadPercent(int)), this, SLOT(downloadPercent(int)));

		QString dest = downloadJob->property(constDestProperty).toString();
		QString partial = dest.isEmpty() ? QString() : QString(dest + constPartialExt);
		if (!partial.isEmpty() && QFile::exists(partial)) {
			QFile::remove(partial);
		}
		updateEpisode(downloadJob->property(constRssUrlProperty).toUrl(), downloadJob->origUrl(), Episode::NotDownloading);
		downloadJob = nullptr;
	}
}

void PodcastService::doNextDownload()
{
	if (downloadJob) {
		return;
	}

	if (toDownload.isEmpty()) {
		return;
	}

	DownloadEntry entry = toDownload.takeFirst();
	downloadJob = NetworkAccessManager::self()->get(entry.url);
	connect(downloadJob, SIGNAL(finished()), this, SLOT(downloadJobFinished()));
	connect(downloadJob, SIGNAL(readyRead()), this, SLOT(downloadReadyRead()));
	connect(downloadJob, SIGNAL(downloadPercent(int)), this, SLOT(downloadPercent(int)));
	downloadJob->setProperty(constRssUrlProperty, entry.rssUrl);
	downloadJob->setProperty(constDestProperty, entry.dest);
	updateEpisode(entry.rssUrl, entry.url, 0);

	QString partial = entry.dest + constPartialExt;
	if (QFile::exists(partial)) {
		QFile::remove(partial);
	}
}

void PodcastService::updateEpisode(const QUrl& rssUrl, const QUrl& url, int pc)
{
	Podcast* pod = getPodcast(rssUrl);
	if (pod) {
		Episode* episode = pod->getEpisode(url);
		if (episode && episode->downloadProg != pc) {
			episode->downloadProg = pc;
			QModelIndex idx = createIndex(pod->episodes.indexOf(episode), 0, (void*)episode);
			emit dataChanged(idx, idx);
		}
	}
}

void PodcastService::clearPartialDownloads()
{
	QString dest = Settings::self()->podcastDownloadPath();
	if (dest.isEmpty()) {
		return;
	}

	dest = Utils::fixPath(dest);
	QStringList sub = QDir(dest).entryList(QDir::Dirs | QDir::NoDotAndDotDot);
	for (const QString& d : sub) {
		QStringList partials = QDir(dest + d).entryList(QStringList() << QLatin1Char('*') + constPartialExt, QDir::Files);
		for (const QString& p : partials) {
			QFile::remove(dest + d + Utils::constDirSep + p);
		}
	}
}

void PodcastService::downloadJobFinished()
{
	NetworkJob* job = dynamic_cast<NetworkJob*>(sender());
	if (!job || job != downloadJob) {
		return;
	}
	job->deleteLater();

	QString dest = job->property(constDestProperty).toString();
	QString partial = dest.isEmpty() ? QString() : QString(dest + constPartialExt);

	if (job->ok()) {
		QString dest = job->property(constDestProperty).toString();
		if (dest.isEmpty()) {
			return;
		}

		QString partial = dest + constPartialExt;
		if (QFile::exists(partial)) {
			if (QFile::exists(dest)) {
				QFile::remove(dest);
			}
			if (QFile::rename(partial, dest)) {
				Podcast* pod = getPodcast(job->property(constRssUrlProperty).toUrl());
				if (pod) {
					Episode* episode = pod->getEpisode(job->origUrl());
					if (episode) {
						episode->localFile = dest;
						pod->save();
						QModelIndex idx = createIndex(pod->episodes.indexOf(episode), 0, (void*)episode);
						emit dataChanged(idx, idx);
					}
				}
			}
		}
	}
	else if (!partial.isEmpty() && QFile::exists(partial)) {
		QFile::remove(partial);
	}
	updateEpisode(job->property(constRssUrlProperty).toUrl(), job->origUrl(), Episode::NotDownloading);
	downloadJob = nullptr;
	doNextDownload();
}

void PodcastService::downloadReadyRead()
{
	NetworkJob* job = dynamic_cast<NetworkJob*>(sender());
	if (!job || job != downloadJob) {
		return;
	}
	QString dest = job->property(constDestProperty).toString();
	QString partial = dest.isEmpty() ? QString() : QString(dest + constPartialExt);
	if (!partial.isEmpty()) {
		QString dir = Utils::getDir(partial);
		if (!QDir(dir).exists()) {
			QDir(dir).mkpath(dir);
		}
		if (!QDir(dir).exists()) {
			return;
		}
		QFile f(partial);
		while (true) {
			const qint64 bytes = job->bytesAvailable();
			if (bytes <= 0) {
				break;
			}
			if (!f.isOpen()) {
				if (!f.open(QIODevice::Append)) {
					return;
				}
			}
			f.write(job->read(bytes));
		}
	}
}

void PodcastService::downloadPercent(int pc)
{
	NetworkJob* job = dynamic_cast<NetworkJob*>(sender());
	if (!job || job != downloadJob) {
		return;
	}
	updateEpisode(job->property(constRssUrlProperty).toUrl(), job->origUrl(), pc);
}

void PodcastService::startRssUpdateTimer()
{
	if (0 == Settings::self()->rssUpdate() || podcasts.isEmpty()) {
		stopRssUpdateTimer();
		return;
	}
	if (!rssUpdateTimer) {
		rssUpdateTimer = new QTimer(this);
		rssUpdateTimer->setSingleShot(true);
		connect(rssUpdateTimer, SIGNAL(timeout()), this, SLOT(updateRss()));
	}
	if (!lastRssUpdate.isValid()) {
		lastRssUpdate = Settings::self()->lastRssUpdate();
	}
	if (!lastRssUpdate.isValid()) {
		updateRss();
	}
	else {
		QDateTime nextUpdate = lastRssUpdate.addSecs(Settings::self()->rssUpdate() * 60);
		int secsUntilNextUpdate = QDateTime::currentDateTime().secsTo(nextUpdate);
		if (secsUntilNextUpdate < 0) {
			// Oops, missed update time!!!
			updateRss();
		}
		else {
			rssUpdateTimer->start(secsUntilNextUpdate * 1000ll);
		}
	}
}

void PodcastService::stopRssUpdateTimer()
{
	if (rssUpdateTimer) {
		rssUpdateTimer->stop();
	}
}

bool PodcastService::exportSubscriptions(const QString& name)
{
	QFile f(name);
	if (!f.open(QIODevice::Text | QIODevice::WriteOnly)) {
		return false;
	}
	QString date = QDateTime::currentDateTime().toString(Qt::RFC2822Date);
	QXmlStreamWriter writer(&f);
	writer.writeStartDocument();
	writer.writeStartElement(QLatin1String("opml"));
	writer.writeAttribute(QLatin1String("version"), QLatin1String("1.1"));
	writer.writeStartElement(QLatin1String("head"));
	writer.writeTextElement(QLatin1String("title"), QLatin1String("Cantata Podcasts"));
	writer.writeTextElement(QLatin1String("dateCreated"), date);
	writer.writeTextElement(QLatin1String("dateModified"), date);

	writer.writeEndElement();// head
	writer.writeStartElement(QLatin1String("body"));
	for (Podcast* podcast : podcasts) {
		writer.writeStartElement(QLatin1String("outline"));
		writer.writeAttribute(QLatin1String("text"), podcast->name);
		writer.writeAttribute(QLatin1String("description"), podcast->descr);
		writer.writeAttribute(QLatin1String("type"), QLatin1String("rss"));
		writer.writeAttribute(QLatin1String("xmlUrl"), podcast->url.toEncoded());
		writer.writeAttribute(QLatin1String("imageUrl"), podcast->imageUrl.toEncoded());
		writer.writeEndElement();// outline
	}
	writer.writeEndElement();// body
	writer.writeEndElement();// opml
	writer.writeEndDocument();
	f.close();
	return true;
}

void PodcastService::updateRss()
{
	for (Podcast* podcast : podcasts) {
		const QUrl& url = podcast->url;
		updateUrls.insert(url);
		if (!processingUrl(url)) {
			addUrl(url, false);
		}
	}
}

void PodcastService::currentMpdSong(const Song& s)
{
	if ((s.isFromOnlineService() && s.onlineService() == constName) || isPodcastFile(s.file)) {
		QString path = s.decodedPath();
		if (path.isEmpty()) {
			path = s.file;
		}
		for (Podcast* podcast : podcasts) {
			for (Episode* episode : podcast->episodes) {
				if (episode->url == path || episode->localFile == path) {
					if (!episode->played) {
						episode->played = true;
						QModelIndex idx = createIndex(podcast->episodes.indexOf(episode), 0, (void*)episode);
						emit dataChanged(idx, idx);
						podcast->unplayedCount--;
						podcast->save();
						idx = createIndex(podcasts.indexOf(podcast), 0, (void*)podcast);
						emit dataChanged(idx, idx);
					}

					return;
				}
			}
		}
	}
}

#include "moc_podcastservice.cpp"
