diff --git a/data/data.qrc b/data/data.qrc
index 90eef0301..587c58976 100644
--- a/data/data.qrc
+++ b/data/data.qrc
@@ -338,5 +338,7 @@
schema/schema-36.sqlgrooveshark-valicert-ca.pemschema/schema-37.sql
+ providers/podcast16.png
+ providers/podcast32.png
diff --git a/data/providers/podcast16.png b/data/providers/podcast16.png
new file mode 100755
index 000000000..1679ab05b
Binary files /dev/null and b/data/providers/podcast16.png differ
diff --git a/data/providers/podcast32.png b/data/providers/podcast32.png
new file mode 100755
index 000000000..ea50b84b7
Binary files /dev/null and b/data/providers/podcast32.png differ
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 90925fa9f..69415017c 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -227,10 +227,17 @@ set(SOURCES
playlistparsers/xmlparser.cpp
playlistparsers/xspfparser.cpp
+ podcasts/addpodcastbyurl.cpp
+ podcasts/addpodcastdialog.cpp
+ podcasts/addpodcastpage.cpp
podcasts/podcast.cpp
podcasts/podcastbackend.cpp
+ podcasts/podcastdiscoverymodel.cpp
podcasts/podcastepisode.cpp
+ podcasts/podcastinfowidget.cpp
+ podcasts/podcastservice.cpp
podcasts/podcastparser.cpp
+ podcasts/podcasturlloader.cpp
smartplaylists/generator.cpp
smartplaylists/generatorinserter.cpp
@@ -467,7 +474,14 @@ set(HEADERS
playlistparsers/plsparser.h
playlistparsers/xspfparser.h
+ podcasts/addpodcastbyurl.h
+ podcasts/addpodcastdialog.h
+ podcasts/addpodcastpage.h
podcasts/podcastbackend.h
+ podcasts/podcastdiscoverymodel.h
+ podcasts/podcastinfowidget.h
+ podcasts/podcastservice.h
+ podcasts/podcasturlloader.h
smartplaylists/generator.h
smartplaylists/generatorinserter.h
@@ -586,6 +600,10 @@ set(UI
playlist/playlistsequence.ui
playlist/queuemanager.ui
+ podcasts/addpodcastbyurl.ui
+ podcasts/addpodcastdialog.ui
+ podcasts/podcastinfowidget.ui
+
remote/remotesettingspage.ui
smartplaylists/querysearchpage.ui
diff --git a/src/core/utilities.cpp b/src/core/utilities.cpp
index 6d36e3eb1..efe10929b 100644
--- a/src/core/utilities.cpp
+++ b/src/core/utilities.cpp
@@ -450,6 +450,16 @@ QStringList Updateify(const QStringList& list) {
return ret;
}
+QString DecodeHtmlEntities(const QString& text) {
+ QString copy(text);
+ copy.replace("&", "&");
+ copy.replace(""", "\"");
+ copy.replace("'", "'");
+ copy.replace("<", "<");
+ copy.replace(">", ">");
+ return copy;
+}
+
int SetThreadIOPriority(IoPriority priority) {
#ifdef Q_OS_LINUX
return syscall(SYS_ioprio_set, IOPRIO_WHO_PROCESS, GetThreadId(),
diff --git a/src/core/utilities.h b/src/core/utilities.h
index 6f9587a9b..9fc6ed976 100644
--- a/src/core/utilities.h
+++ b/src/core/utilities.h
@@ -89,6 +89,9 @@ namespace Utilities {
// Parses a string containing an RFC822 time and date.
QDateTime ParseRFC822DateTime(const QString& text);
+ // Replaces some HTML entities with their normal characters.
+ QString DecodeHtmlEntities(const QString& text);
+
// Shortcut for getting a Qt-aware enum value as a string.
// Pass in the QMetaObject of the class that owns the enum, the string name of
// the enum and a valid value from that enum.
diff --git a/src/internet/internetmodel.cpp b/src/internet/internetmodel.cpp
index 97ec2bb31..45a34f4d6 100644
--- a/src/internet/internetmodel.cpp
+++ b/src/internet/internetmodel.cpp
@@ -27,6 +27,7 @@
#include "groovesharkservice.h"
#include "core/logging.h"
#include "core/mergedproxymodel.h"
+#include "podcasts/podcastservice.h"
#include "smartplaylists/generatormimedata.h"
#ifdef HAVE_LIBLASTFM
@@ -65,6 +66,7 @@ InternetModel::InternetModel(Application* app, QObject* parent)
#endif
AddService(new GroovesharkService(app, this));
AddService(new MagnatuneService(app, this));
+ AddService(new PodcastService(app, this));
AddService(new SavedRadio(app, this));
AddService(new SkyFmService(app, this));
AddService(new SomaFMService(app, this));
diff --git a/src/internet/magnatunedownloaddialog.cpp b/src/internet/magnatunedownloaddialog.cpp
index a431a7396..614576578 100644
--- a/src/internet/magnatunedownloaddialog.cpp
+++ b/src/internet/magnatunedownloaddialog.cpp
@@ -21,6 +21,7 @@
#include "ui_magnatunedownloaddialog.h"
#include "core/logging.h"
#include "core/network.h"
+#include "core/utilities.h"
#include "widgets/progressitemdelegate.h"
#include
@@ -176,8 +177,7 @@ void MagnatuneDownloadDialog::MetadataFinished() {
}
// Munge the URL a bit
- QString url_text = re.cap(1);
- url_text.replace("&", "&");
+ QString url_text = Utilities::DecodeHtmlEntities(re.cap(1));
QUrl url = QUrl(url_text);
url.setUserName(service_->username());
diff --git a/src/podcasts/addpodcastbyurl.cpp b/src/podcasts/addpodcastbyurl.cpp
new file mode 100644
index 000000000..c895caf7a
--- /dev/null
+++ b/src/podcasts/addpodcastbyurl.cpp
@@ -0,0 +1,70 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ 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 .
+*/
+
+#include "addpodcastbyurl.h"
+#include "podcastdiscoverymodel.h"
+#include "podcasturlloader.h"
+#include "ui_addpodcastbyurl.h"
+#include "core/closure.h"
+
+#include
+#include
+
+AddPodcastByUrl::AddPodcastByUrl(Application* app, QWidget* parent)
+ : AddPodcastPage(app, parent),
+ ui_(new Ui_AddPodcastByUrl),
+ loader_(new PodcastUrlLoader(this))
+{
+ ui_->setupUi(this);
+ connect(ui_->go, SIGNAL(clicked()), SLOT(GoClicked()));
+}
+
+AddPodcastByUrl::~AddPodcastByUrl() {
+ delete ui_;
+}
+
+void AddPodcastByUrl::GoClicked() {
+ emit Busy(true);
+ model()->clear();
+ ui_->go->setEnabled(false);
+ ui_->url->setEnabled(false);
+
+ PodcastUrlLoaderReply* reply = loader_->Load(ui_->url->text());
+ ui_->url->setText(reply->url().toString());
+
+ NewClosure(reply, SIGNAL(Finished(bool)),
+ this, SLOT(RequestFinished(PodcastUrlLoaderReply*)),
+ reply);
+}
+
+void AddPodcastByUrl::RequestFinished(PodcastUrlLoaderReply* reply) {
+ reply->deleteLater();
+
+ emit Busy(false);
+ ui_->go->setEnabled(true);
+ ui_->url->setEnabled(true);
+
+ if (!reply->is_success()) {
+ QMessageBox::warning(this, tr("Failed to load podcast"),
+ reply->error_text(), QMessageBox::Close);
+ return;
+ }
+
+ foreach (const Podcast& podcast, reply->results()) {
+ model()->appendRow(model()->CreatePodcastItem(podcast));
+ }
+}
diff --git a/src/podcasts/addpodcastbyurl.h b/src/podcasts/addpodcastbyurl.h
new file mode 100644
index 000000000..9ab8f9fb5
--- /dev/null
+++ b/src/podcasts/addpodcastbyurl.h
@@ -0,0 +1,44 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ 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 .
+*/
+
+#ifndef ADDPODCASTBYURL_H
+#define ADDPODCASTBYURL_H
+
+#include "addpodcastpage.h"
+
+class AddPodcastPage;
+class PodcastUrlLoader;
+class PodcastUrlLoaderReply;
+class Ui_AddPodcastByUrl;
+
+class AddPodcastByUrl : public AddPodcastPage {
+ Q_OBJECT
+
+public:
+ AddPodcastByUrl(Application* app, QWidget* parent = 0);
+ ~AddPodcastByUrl();
+
+private slots:
+ void GoClicked();
+ void RequestFinished(PodcastUrlLoaderReply* reply);
+
+private:
+ Ui_AddPodcastByUrl* ui_;
+ PodcastUrlLoader* loader_;
+};
+
+#endif // ADDPODCASTBYURL_H
diff --git a/src/podcasts/addpodcastbyurl.ui b/src/podcasts/addpodcastbyurl.ui
new file mode 100644
index 000000000..1ec337fdd
--- /dev/null
+++ b/src/podcasts/addpodcastbyurl.ui
@@ -0,0 +1,68 @@
+
+
+ AddPodcastByUrl
+
+
+
+ 0
+ 0
+ 431
+ 51
+
+
+
+ Enter a URL
+
+
+
+ :/providers/podcast32.png:/providers/podcast32.png
+
+
+
+ 0
+
+
+
+
+ If you know the URL of a podcast, enter it below and press Go.
+
+
+
+
+
+
+
+
+
+
+
+ Go
+
+
+
+
+
+
+
+
+
+
+
+
+ url
+ returnPressed()
+ go
+ click()
+
+
+ 109
+ 24
+
+
+ 429
+ 49
+
+
+
+
+
diff --git a/src/podcasts/addpodcastdialog.cpp b/src/podcasts/addpodcastdialog.cpp
new file mode 100644
index 000000000..a386a821b
--- /dev/null
+++ b/src/podcasts/addpodcastdialog.cpp
@@ -0,0 +1,102 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ 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 .
+*/
+
+#include "addpodcastdialog.h"
+#include "addpodcastbyurl.h"
+#include "podcastdiscoverymodel.h"
+#include "ui_addpodcastdialog.h"
+#include "core/application.h"
+#include "ui/iconloader.h"
+
+#include
+
+AddPodcastDialog::AddPodcastDialog(Application* app, QWidget* parent)
+ : QDialog(parent),
+ ui_(new Ui_AddPodcastDialog)
+{
+ ui_->setupUi(this);
+ ui_->details->SetApplication(app);
+ ui_->results_stack->setCurrentWidget(ui_->results_page);
+
+ connect(ui_->provider_list, SIGNAL(currentRowChanged(int)), SLOT(ChangePage(int)));
+
+ // Create an Add Podcast button
+ add_button_ = new QPushButton(IconLoader::Load("list-add"), tr("Add Podcast"), this);
+ add_button_->setEnabled(false);
+ connect(add_button_, SIGNAL(clicked()), SLOT(AddPodcast()));
+ ui_->button_box->addButton(add_button_, QDialogButtonBox::AcceptRole);
+
+ // Add providers
+ AddPage(new AddPodcastByUrl(app, this));
+
+ ui_->provider_list->setCurrentRow(0);
+}
+
+AddPodcastDialog::~AddPodcastDialog() {
+ delete ui_;
+}
+
+void AddPodcastDialog::AddPage(AddPodcastPage* page) {
+ pages_.append(page);
+ page_is_busy_.append(false);
+
+ ui_->stack->addWidget(page);
+ new QListWidgetItem(page->windowIcon(), page->windowTitle(), ui_->provider_list);
+
+ connect(page, SIGNAL(Busy(bool)), SLOT(PageBusyChanged(bool)));
+}
+
+void AddPodcastDialog::ChangePage(int index) {
+ AddPodcastPage* page = pages_[index];
+
+ ui_->stack->setCurrentIndex(index);
+ ui_->results->setModel(page->model());
+ ui_->results->setRootIsDecorated(page->model()->is_tree());
+
+ ui_->results_stack->setCurrentWidget(
+ page_is_busy_[index] ? ui_->busy_page : ui_->results_page);
+
+ connect(ui_->results->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)),
+ SLOT(ChangePodcast(QModelIndex)));
+ ChangePodcast(QModelIndex());
+ PageBusyChanged(page_is_busy_[index]);
+}
+
+void AddPodcastDialog::ChangePodcast(const QModelIndex& current) {
+ if (!current.isValid()) {
+ ui_->details->hide();
+ return;
+ }
+
+ ui_->details->show();
+ ui_->details->SetPodcast(current.data(PodcastDiscoveryModel::Role_Podcast).value());
+}
+
+void AddPodcastDialog::PageBusyChanged(bool busy) {
+ const int index = pages_.indexOf(qobject_cast(sender()));
+ if (index == -1)
+ return;
+
+ page_is_busy_[index] = busy;
+
+ if (index == ui_->provider_list->currentRow()) {
+ ui_->results_stack->setCurrentWidget(busy ? ui_->busy_page : ui_->results_page);
+ }
+}
+
+void AddPodcastDialog::AddPodcast() {
+}
diff --git a/src/podcasts/addpodcastdialog.h b/src/podcasts/addpodcastdialog.h
new file mode 100644
index 000000000..76b6903e1
--- /dev/null
+++ b/src/podcasts/addpodcastdialog.h
@@ -0,0 +1,54 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ 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 .
+*/
+
+#ifndef ADDPODCASTDIALOG_H
+#define ADDPODCASTDIALOG_H
+
+#include
+
+class AddPodcastPage;
+class Application;
+class Ui_AddPodcastDialog;
+
+class QModelIndex;
+
+class AddPodcastDialog : public QDialog {
+ Q_OBJECT
+
+public:
+ AddPodcastDialog(Application* app, QWidget* parent = 0);
+ ~AddPodcastDialog();
+
+private slots:
+ void AddPodcast();
+ void ChangePage(int index);
+ void ChangePodcast(const QModelIndex& current);
+
+ void PageBusyChanged(bool busy);
+
+private:
+ void AddPage(AddPodcastPage* page);
+
+private:
+ Ui_AddPodcastDialog* ui_;
+ QPushButton* add_button_;
+
+ QList pages_;
+ QList page_is_busy_;
+};
+
+#endif // ADDPODCASTDIALOG_H
diff --git a/src/podcasts/addpodcastdialog.ui b/src/podcasts/addpodcastdialog.ui
new file mode 100644
index 000000000..a6ad9b50b
--- /dev/null
+++ b/src/podcasts/addpodcastdialog.ui
@@ -0,0 +1,179 @@
+
+
+ AddPodcastDialog
+
+
+
+ 0
+ 0
+ 887
+ 447
+
+
+
+ Dialog
+
+
+
+
+
+
+
+
+ 200
+ 16777215
+
+
+
+ Qt::ScrollBarAlwaysOff
+
+
+
+ 32
+ 32
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ 1
+
+
+
+
+ 0
+
+
+ 0
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+ Loading...
+
+
+
+
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 367
+
+
+
+
+
+
+
+
+
+
+
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Raised
+
+
+
+
+
+
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Close
+
+
+
+
+
+
+
+ BusyIndicator
+ QWidget
+ widgets/busyindicator.h
+ 1
+
+
+ PodcastInfoWidget
+ QFrame
+ podcasts/podcastinfowidget.h
+ 1
+
+
+
+
+
+ button_box
+ accepted()
+ AddPodcastDialog
+ accept()
+
+
+ 827
+ 436
+
+
+ 157
+ 274
+
+
+
+
+ button_box
+ rejected()
+ AddPodcastDialog
+ reject()
+
+
+ 876
+ 436
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/podcasts/addpodcastpage.cpp b/src/podcasts/addpodcastpage.cpp
new file mode 100644
index 000000000..ecf6d5ef2
--- /dev/null
+++ b/src/podcasts/addpodcastpage.cpp
@@ -0,0 +1,25 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ 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 .
+*/
+
+#include "addpodcastpage.h"
+#include "podcastdiscoverymodel.h"
+
+AddPodcastPage::AddPodcastPage(Application* app, QWidget* parent)
+ : QWidget(parent),
+ model_(new PodcastDiscoveryModel(app, this))
+{
+}
diff --git a/src/podcasts/addpodcastpage.h b/src/podcasts/addpodcastpage.h
new file mode 100644
index 000000000..7d7830329
--- /dev/null
+++ b/src/podcasts/addpodcastpage.h
@@ -0,0 +1,41 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ 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 .
+*/
+
+#ifndef ADDPODCASTPAGE_H
+#define ADDPODCASTPAGE_H
+
+#include
+
+class Application;
+class PodcastDiscoveryModel;
+
+class AddPodcastPage : public QWidget {
+ Q_OBJECT
+
+public:
+ AddPodcastPage(Application* app, QWidget* parent = 0);
+
+ PodcastDiscoveryModel* model() const { return model_; }
+
+signals:
+ void Busy(bool busy);
+
+private:
+ PodcastDiscoveryModel* model_;
+};
+
+#endif // ADDPODCASTPAGE_H
diff --git a/src/podcasts/podcastdiscoverymodel.cpp b/src/podcasts/podcastdiscoverymodel.cpp
new file mode 100644
index 000000000..ff8564663
--- /dev/null
+++ b/src/podcasts/podcastdiscoverymodel.cpp
@@ -0,0 +1,67 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ 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 .
+*/
+
+#include "podcast.h"
+#include "podcastdiscoverymodel.h"
+#include "core/application.h"
+#include "covers/albumcoverloader.h"
+
+#include
+#include
+
+PodcastDiscoveryModel::PodcastDiscoveryModel(Application* app, QObject* parent)
+ : QStandardItemModel(parent),
+ app_(app),
+ is_tree_(false),
+ default_icon_(":providers/podcast32.png")
+{
+ cover_options_.desired_height_ = 32;
+
+ connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64,QImage)),
+ SLOT(ImageLoaded(quint64,QImage)));
+ connect(this, SIGNAL(modelAboutToBeReset()), SLOT(CancelPendingImages()));
+}
+
+QStandardItem* PodcastDiscoveryModel::CreatePodcastItem(const Podcast& podcast) {
+ QStandardItem* item = new QStandardItem;
+ item->setIcon(default_icon_);
+ item->setText(podcast.title());
+ item->setData(QVariant::fromValue(podcast), Role_Podcast);
+ item->setData(Type_Podcast, Role_Type);
+
+ if (podcast.image_url().isValid()) {
+ // Start loading an image for this item.
+ quint64 id = app_->album_cover_loader()->LoadImageAsync(
+ cover_options_, podcast.image_url().toString(), QString());
+ pending_covers_[id] = item;
+ }
+
+ return item;
+}
+
+void PodcastDiscoveryModel::ImageLoaded(quint64 id, const QImage& image) {
+ QStandardItem* item = pending_covers_.take(id);
+ if (!item)
+ return;
+
+ item->setIcon(QIcon(QPixmap::fromImage(image)));
+}
+
+void PodcastDiscoveryModel::CancelPendingImages() {
+ app_->album_cover_loader()->CancelTasks(QSet::fromList(pending_covers_.keys()));
+ pending_covers_.clear();
+}
diff --git a/src/podcasts/podcastdiscoverymodel.h b/src/podcasts/podcastdiscoverymodel.h
new file mode 100644
index 000000000..808ff0446
--- /dev/null
+++ b/src/podcasts/podcastdiscoverymodel.h
@@ -0,0 +1,63 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ 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 .
+*/
+
+#ifndef PODCASTDISCOVERYMODEL_H
+#define PODCASTDISCOVERYMODEL_H
+
+#include "covers/albumcoverloaderoptions.h"
+
+#include
+
+class Application;
+class Podcast;
+
+class PodcastDiscoveryModel : public QStandardItemModel {
+ Q_OBJECT
+
+public:
+ PodcastDiscoveryModel(Application* app, QObject* parent = 0);
+
+ enum Type {
+ Type_Folder,
+ Type_Podcast
+ };
+
+ enum Role {
+ Role_Podcast = Qt::UserRole,
+ Role_Type
+ };
+
+ bool is_tree() const { return is_tree_; }
+ void set_is_tree(bool v) { is_tree_ = v; }
+
+ QStandardItem* CreatePodcastItem(const Podcast& podcast);
+
+private slots:
+ void CancelPendingImages();
+ void ImageLoaded(quint64 id, const QImage& image);
+
+private:
+ Application* app_;
+
+ bool is_tree_;
+
+ AlbumCoverLoaderOptions cover_options_;
+ QIcon default_icon_;
+ QMap pending_covers_;
+};
+
+#endif // PODCASTDISCOVERYMODEL_H
diff --git a/src/podcasts/podcastinfowidget.cpp b/src/podcasts/podcastinfowidget.cpp
new file mode 100644
index 000000000..530a82743
--- /dev/null
+++ b/src/podcasts/podcastinfowidget.cpp
@@ -0,0 +1,106 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ 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 .
+*/
+
+#include "podcastinfowidget.h"
+#include "ui_podcastinfowidget.h"
+#include "core/application.h"
+#include "covers/albumcoverloader.h"
+
+PodcastInfoWidget::PodcastInfoWidget(QWidget* parent)
+ : QFrame(parent),
+ ui_(new Ui_PodcastInfoWidget),
+ app_(NULL),
+ image_id_(0)
+{
+ ui_->setupUi(this);
+
+ setFrameShape(QFrame::StyledPanel);
+
+ setMaximumWidth(220);
+ cover_options_.desired_height_ = 200;
+ ui_->image->setFixedSize(cover_options_.desired_height_,
+ cover_options_.desired_height_);
+
+ // Set the colour of all the labels
+ const bool light = palette().color(QPalette::Base).value() > 128;
+ const QColor color = palette().color(QPalette::Dark);
+ QPalette label_palette(palette());
+ label_palette.setColor(QPalette::WindowText, light ? color.darker(150) : color.lighter(125));
+
+ foreach (QLabel* label, findChildren()) {
+ if (label->property("field_label").toBool()) {
+ label->setPalette(label_palette);
+ }
+ }
+}
+
+PodcastInfoWidget::~PodcastInfoWidget() {
+}
+
+void PodcastInfoWidget::SetApplication(Application* app) {
+ app_ = app;
+ connect(app_->album_cover_loader(), SIGNAL(ImageLoaded(quint64,QImage)),
+ SLOT(ImageLoaded(quint64,QImage)));
+}
+
+void PodcastInfoWidget::SetPodcast(const Podcast& podcast) {
+ if (image_id_ != 0) {
+ app_->album_cover_loader()->CancelTask(image_id_);
+ }
+
+ podcast_ = podcast;
+
+ if (podcast.image_url().isValid()) {
+ // Start loading an image for this item.
+ image_id_ = app_->album_cover_loader()->LoadImageAsync(
+ cover_options_, podcast.image_url().toString(), QString());
+ }
+
+ ui_->image->hide();
+
+ SetText(podcast.title(), ui_->title);
+ SetText(podcast.description(), ui_->description);
+ SetText(podcast.copyright(), ui_->copyright, ui_->copyright_label);
+ SetText(podcast.author(), ui_->author, ui_->author_label);
+ SetText(podcast.owner_name(), ui_->owner, ui_->owner_label);
+ SetText(podcast.link().toString(), ui_->website, ui_->website_label);
+}
+
+void PodcastInfoWidget::SetText(const QString& value, QLabel* label, QLabel* buddy_label) {
+ const bool visible = !value.isEmpty();
+
+ label->setVisible(visible);
+ if (buddy_label) {
+ buddy_label->setVisible(visible);
+ }
+
+ if (visible) {
+ label->setText(value);
+ }
+}
+
+void PodcastInfoWidget::ImageLoaded(quint64 id, const QImage& image) {
+ if (id != image_id_) {
+ return;
+ }
+ image_id_ = 0;
+
+ if (!image.isNull()) {
+ ui_->image->setPixmap(QPixmap::fromImage(image));
+ ui_->image->show();
+ }
+}
diff --git a/src/podcasts/podcastinfowidget.h b/src/podcasts/podcastinfowidget.h
new file mode 100644
index 000000000..837abb7f7
--- /dev/null
+++ b/src/podcasts/podcastinfowidget.h
@@ -0,0 +1,58 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ 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 .
+*/
+
+#ifndef PODCASTINFOWIDGET_H
+#define PODCASTINFOWIDGET_H
+
+#include "podcast.h"
+#include "covers/albumcoverloaderoptions.h"
+
+#include
+
+class Application;
+class Ui_PodcastInfoWidget;
+
+class QLabel;
+
+class PodcastInfoWidget : public QFrame {
+ Q_OBJECT
+
+public:
+ PodcastInfoWidget(QWidget* parent = 0);
+ ~PodcastInfoWidget();
+
+ void SetApplication(Application* app);
+
+ void SetPodcast(const Podcast& podcast);
+
+private slots:
+ void ImageLoaded(quint64 id, const QImage& image);
+
+private:
+ void SetText(const QString& value, QLabel* label, QLabel* buddy_label = NULL);
+
+private:
+ Ui_PodcastInfoWidget* ui_;
+
+ AlbumCoverLoaderOptions cover_options_;
+
+ Application* app_;
+ Podcast podcast_;
+ quint64 image_id_;
+};
+
+#endif // PODCASTINFOWIDGET_H
diff --git a/src/podcasts/podcastinfowidget.ui b/src/podcasts/podcastinfowidget.ui
new file mode 100644
index 000000000..a044dd2fa
--- /dev/null
+++ b/src/podcasts/podcastinfowidget.ui
@@ -0,0 +1,163 @@
+
+
+ PodcastInfoWidget
+
+
+
+ 0
+ 0
+ 398
+ 551
+
+
+
+ Form
+
+
+ #title {
+ font-weight: bold;
+}
+
+#description {
+ font-size: smaller;
+}
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ true
+
+
+ Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse
+
+
+
+
+
+
+ true
+
+
+ Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse
+
+
+
+
+
+
+
+
+ Copyright
+
+
+ true
+
+
+
+
+
+
+ Owner
+
+
+ true
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse
+
+
+
+
+
+
+ Author
+
+
+ true
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse
+
+
+
+
+
+
+ Website
+
+
+ true
+
+
+
+
+
+
+ Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse
+
+
+
+
+
+
+
+
+ Qt::Vertical
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+
+
diff --git a/src/podcasts/podcastparser.cpp b/src/podcasts/podcastparser.cpp
index 526f59f35..4ef51e80c 100644
--- a/src/podcasts/podcastparser.cpp
+++ b/src/podcasts/podcastparser.cpp
@@ -25,20 +25,31 @@ const char* PodcastParser::kAtomNamespace = "http://www.w3.org/2005/Atom";
const char* PodcastParser::kItunesNamespace = "http://www.itunes.com/dtds/podcast-1.0.dtd";
PodcastParser::PodcastParser() {
+ supported_mime_types_ << "application/rss+xml"
+ << "application/xml"
+ << "text/xml";
}
-Podcast PodcastParser::Load(QIODevice* device, const QUrl& url) const {
- Podcast ret;
- ret.set_url(url);
+bool PodcastParser::SupportsContentType(const QString& content_type) const {
+ foreach (const QString& mime_type, supported_mime_types()) {
+ if (content_type.contains(mime_type)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool PodcastParser::Load(QIODevice* device, const QUrl& url, Podcast* ret) const {
+ ret->set_url(url);
QXmlStreamReader reader(device);
if (!Utilities::ParseUntilElement(&reader, "rss") ||
!Utilities::ParseUntilElement(&reader, "channel")) {
- return ret;
+ return false;
}
- ParseChannel(&reader, &ret);
- return ret;
+ ParseChannel(&reader, ret);
+ return true;
}
void PodcastParser::ParseChannel(QXmlStreamReader* reader, Podcast* ret) const {
diff --git a/src/podcasts/podcastparser.h b/src/podcasts/podcastparser.h
index b27f50dcb..3e898afee 100644
--- a/src/podcasts/podcastparser.h
+++ b/src/podcasts/podcastparser.h
@@ -18,6 +18,8 @@
#ifndef PODCASTPARSER_H
#define PODCASTPARSER_H
+#include
+
#include "podcast.h"
class QXmlStreamReader;
@@ -30,13 +32,19 @@ public:
static const char* kAtomNamespace;
static const char* kItunesNamespace;
- Podcast Load(QIODevice* device, const QUrl& url) const;
+ const QStringList& supported_mime_types() const { return supported_mime_types_; }
+ bool SupportsContentType(const QString& content_type) const;
+
+ bool Load(QIODevice* device, const QUrl& url, Podcast* ret) const;
private:
void ParseChannel(QXmlStreamReader* reader, Podcast* ret) const;
void ParseImage(QXmlStreamReader* reader, Podcast* ret) const;
void ParseItunesOwner(QXmlStreamReader* reader, Podcast* ret) const;
void ParseItem(QXmlStreamReader* reader, Podcast* ret) const;
+
+private:
+ QStringList supported_mime_types_;
};
#endif // PODCASTPARSER_H
diff --git a/src/podcasts/podcastservice.cpp b/src/podcasts/podcastservice.cpp
new file mode 100644
index 000000000..77fcb180c
--- /dev/null
+++ b/src/podcasts/podcastservice.cpp
@@ -0,0 +1,67 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ 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 .
+*/
+
+#include "addpodcastdialog.h"
+#include "podcastservice.h"
+#include "internet/internetmodel.h"
+#include "ui/iconloader.h"
+
+#include
+
+const char* PodcastService::kServiceName = "Podcasts";
+const char* PodcastService::kSettingsGroup = "Podcasts";
+
+PodcastService::PodcastService(Application* app, InternetModel* parent)
+ : InternetService(kServiceName, app, parent, parent),
+ context_menu_(NULL),
+ root_(NULL)
+{
+}
+
+PodcastService::~PodcastService() {
+}
+
+QStandardItem* PodcastService::CreateRootItem() {
+ root_ = new QStandardItem(QIcon(":providers/podcast16.png"), tr("Podcasts"));
+ return root_;
+}
+
+void PodcastService::LazyPopulate(QStandardItem* parent) {
+}
+
+void PodcastService::ShowContextMenu(const QModelIndex& index,
+ const QPoint& global_pos) {
+ if (!context_menu_) {
+ context_menu_ = new QMenu;
+ context_menu_->addAction(IconLoader::Load("list-add"), tr("Add podcast..."),
+ this, SLOT(AddPodcast()));
+ }
+
+ context_menu_->popup(global_pos);
+}
+
+QModelIndex PodcastService::GetCurrentIndex() {
+ return QModelIndex();
+}
+
+void PodcastService::AddPodcast() {
+ if (!add_podcast_dialog_) {
+ add_podcast_dialog_.reset(new AddPodcastDialog(app_));
+ }
+
+ add_podcast_dialog_->show();
+}
diff --git a/src/podcasts/podcastservice.h b/src/podcasts/podcastservice.h
new file mode 100644
index 000000000..73c5e634e
--- /dev/null
+++ b/src/podcasts/podcastservice.h
@@ -0,0 +1,56 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ 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 .
+*/
+
+#ifndef PODCASTSERVICE_H
+#define PODCASTSERVICE_H
+
+#include "internet/internetservice.h"
+
+#include
+
+class AddPodcastDialog;
+
+
+class PodcastService : public InternetService {
+ Q_OBJECT
+
+public:
+ PodcastService(Application* app, InternetModel* parent);
+ ~PodcastService();
+
+ static const char* kServiceName;
+ static const char* kSettingsGroup;
+
+ QStandardItem* CreateRootItem();
+ void LazyPopulate(QStandardItem* parent);
+
+ void ShowContextMenu(const QModelIndex& index, const QPoint& global_pos);
+
+protected:
+ QModelIndex GetCurrentIndex();
+
+private slots:
+ void AddPodcast();
+
+private:
+ QMenu* context_menu_;
+ QStandardItem* root_;
+
+ QScopedPointer add_podcast_dialog_;
+};
+
+#endif // PODCASTSERVICE_H
diff --git a/src/podcasts/podcasturlloader.cpp b/src/podcasts/podcasturlloader.cpp
new file mode 100644
index 000000000..202c6a4e4
--- /dev/null
+++ b/src/podcasts/podcasturlloader.cpp
@@ -0,0 +1,204 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ 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 .
+*/
+
+#include "podcastparser.h"
+#include "podcasturlloader.h"
+#include "core/closure.h"
+#include "core/network.h"
+#include "core/utilities.h"
+
+#include
+
+const int PodcastUrlLoader::kMaxRedirects = 5;
+
+
+PodcastUrlLoader::PodcastUrlLoader(QObject* parent)
+ : QObject(parent),
+ network_(new NetworkAccessManager(this)),
+ parser_(new PodcastParser),
+ html_link_re_(""),
+ html_link_rel_re_("rel\\s*=\\s*['\"]?\\s*alternate"),
+ html_link_type_re_("type\\s*=\\s*['\"]?([^'\" ]+)"),
+ html_link_href_re_("href\\s*=\\s*['\"]?([^'\" ]+)")
+{
+ html_link_re_.setMinimal(true);
+ html_link_re_.setCaseSensitivity(Qt::CaseInsensitive);
+
+ // Thanks gpodder!
+ quick_prefixes_ << QuickPrefix("fb:", "http://feeds.feedburner.com/%1")
+ << QuickPrefix("yt:", "http://www.youtube.com/rss/user/%1/videos.rss")
+ << QuickPrefix("sc:", "http://soundcloud.com/%1")
+ << QuickPrefix("fm4od:", "http://onapp1.orf.at/webcam/fm4/fod/%1.xspf")
+ << QuickPrefix("ytpl:", "http://gdata.youtube.com/feeds/api/playlists/%1");
+}
+
+PodcastUrlLoader::~PodcastUrlLoader() {
+ delete parser_;
+}
+
+QUrl PodcastUrlLoader::FixPodcastUrl(const QString& url_text) const {
+ QString url_text_copy(url_text);
+
+ // Check if it matches one of the quick prefixes.
+ for (QuickPrefixList::const_iterator it = quick_prefixes_.constBegin() ;
+ it != quick_prefixes_.constEnd() ; ++it) {
+ if (url_text_copy.startsWith(it->first)) {
+ url_text_copy = it->second.arg(url_text_copy.mid(it->first.length()));
+ }
+ }
+
+ if (!url_text_copy.contains("://")) {
+ url_text_copy.prepend("http://");
+ }
+
+ // Replace schemes
+ QUrl url(url_text_copy);
+ if (url.scheme().isEmpty() || url.scheme() == "feed" ||
+ url.scheme() == "itpc" || url.scheme() == "itms") {
+ url.setScheme("http");
+ } else if (url.scheme() == "zune" && url.host() == "subscribe" &&
+ !url.queryItems().isEmpty()) {
+ url = QUrl(url.queryItems()[0].second);
+ }
+
+ return url;
+}
+
+PodcastUrlLoaderReply* PodcastUrlLoader::Load(const QString& url_text) {
+ QUrl url(FixPodcastUrl(url_text));
+
+ // Create a reply
+ PodcastUrlLoaderReply* reply = new PodcastUrlLoaderReply(url, this);
+
+ // Create a state object to track this request
+ RequestState* state = new RequestState;
+ state->redirects_remaining_ = kMaxRedirects + 1;
+ state->reply_ = reply;
+
+ // Start the first request
+ NextRequest(url, state);
+
+ return reply;
+}
+
+void PodcastUrlLoader::SendErrorAndDelete(const QString& error_text, RequestState* state) {
+ state->reply_->SetFinished(error_text);
+ delete state;
+}
+
+void PodcastUrlLoader::NextRequest(const QUrl& url, RequestState* state) {
+ // Stop the request if there have been too many redirects already.
+ if (state->redirects_remaining_-- == 0) {
+ SendErrorAndDelete(tr("Too many redirects"), state);
+ return;
+ }
+
+ qLog(Debug) << "Loading URL" << url;
+
+ QNetworkRequest req(url);
+ req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
+ QNetworkReply* network_reply = network_->get(req);
+
+ NewClosure(network_reply, SIGNAL(finished()),
+ this, SLOT(RequestFinished(RequestState*, QNetworkReply*)),
+ state, network_reply);
+}
+
+void PodcastUrlLoader::RequestFinished(RequestState* state, QNetworkReply* reply) {
+ reply->deleteLater();
+
+ if (reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid()) {
+ const QUrl next_url = reply->url().resolved(
+ reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl());
+
+ NextRequest(next_url, state);
+ return;
+ }
+
+ // Check for errors.
+ if (reply->error() != QNetworkReply::NoError) {
+ SendErrorAndDelete(reply->errorString(), state);
+ return;
+ }
+
+ if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
+ SendErrorAndDelete(QString("HTTP %1: %2").arg(
+ reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toString(),
+ reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString()), state);
+ return;
+ }
+
+ // Check the mime type.
+ const QString content_type = reply->header(QNetworkRequest::ContentTypeHeader).toString();
+ if (parser_->SupportsContentType(content_type)) {
+ Podcast podcast;
+ if (!parser_->Load(reply, reply->url(), &podcast)) {
+ SendErrorAndDelete(tr("Failed to parse the XML for this RSS feed"), state);
+ return;
+ }
+
+ state->reply_->SetFinished(PodcastList() << podcast);
+ delete state;
+ return;
+ } else if (content_type.contains("text/html")) {
+ // I don't want a full HTML parser here, so do this the dirty way.
+ const QString page_text = QString::fromUtf8(reply->readAll());
+ int pos = 0;
+ while ((pos = html_link_re_.indexIn(page_text, pos)) != -1) {
+ const QString link = html_link_re_.cap(1).toLower();
+ pos += html_link_re_.matchedLength();
+
+ if (html_link_rel_re_.indexIn(link) == -1 ||
+ html_link_type_re_.indexIn(link) == -1 ||
+ html_link_href_re_.indexIn(link) == -1) {
+ continue;
+ }
+
+ const QString link_type = html_link_type_re_.cap(1);
+ const QString href = Utilities::DecodeHtmlEntities(html_link_href_re_.cap(1));
+
+ if (parser_->supported_mime_types().contains(link_type)) {
+ NextRequest(QUrl(href), state);
+ return;
+ }
+ }
+
+ SendErrorAndDelete(tr("HTML page did not contain any RSS feeds"), state);
+ } else {
+ SendErrorAndDelete(tr("Unknown content-type") + ": " + content_type, state);
+ }
+}
+
+
+PodcastUrlLoaderReply::PodcastUrlLoaderReply(const QUrl& url, QObject* parent)
+ : QObject(parent),
+ url_(url),
+ finished_(false)
+{
+}
+
+void PodcastUrlLoaderReply::SetFinished(const PodcastList& results) {
+ results_ = results;
+ finished_ = true;
+ emit Finished(true);
+}
+
+void PodcastUrlLoaderReply::SetFinished(const QString& error_text) {
+ error_text_ = error_text;
+ finished_ = true;
+ emit Finished(false);
+}
diff --git a/src/podcasts/podcasturlloader.h b/src/podcasts/podcasturlloader.h
new file mode 100644
index 000000000..9e352af2b
--- /dev/null
+++ b/src/podcasts/podcasturlloader.h
@@ -0,0 +1,98 @@
+/* This file is part of Clementine.
+ Copyright 2012, David Sansome
+
+ 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 .
+*/
+
+#ifndef PODCASTURLLOADER_H
+#define PODCASTURLLOADER_H
+
+#include
+#include
+
+#include "podcast.h"
+
+class PodcastParser;
+
+class QNetworkAccessManager;
+class QNetworkReply;
+
+class PodcastUrlLoaderReply : public QObject {
+ Q_OBJECT
+
+public:
+ PodcastUrlLoaderReply(const QUrl& url, QObject* parent);
+
+ const QUrl& url() const { return url_; }
+ bool is_finished() const { return finished_; }
+ bool is_success() const { return error_text_.isEmpty(); }
+ const QString& error_text() const { return error_text_; }
+ const PodcastList& results() const { return results_; }
+
+ void SetFinished(const QString& error_text);
+ void SetFinished(const PodcastList& results);
+
+signals:
+ void Finished(bool success);
+
+private:
+ QUrl url_;
+ bool finished_;
+ QString error_text_;
+ PodcastList results_;
+};
+
+
+class PodcastUrlLoader : public QObject {
+ Q_OBJECT
+
+public:
+ PodcastUrlLoader(QObject* parent = 0);
+ ~PodcastUrlLoader();
+
+ static const int kMaxRedirects;
+
+ PodcastUrlLoaderReply* Load(const QString& url_text);
+
+private:
+ struct RequestState {
+ int redirects_remaining_;
+ PodcastUrlLoaderReply* reply_;
+ };
+
+ typedef QPair QuickPrefix;
+ typedef QList QuickPrefixList;
+
+private slots:
+ void RequestFinished(RequestState* state, QNetworkReply* reply);
+
+private:
+ QUrl FixPodcastUrl(const QString& url_text) const;
+ void SendErrorAndDelete(const QString& error_text, RequestState* state);
+ void NextRequest(const QUrl& url, RequestState* state);
+
+private:
+ QNetworkAccessManager* network_;
+ PodcastParser* parser_;
+
+ QuickPrefixList quick_prefixes_;
+
+ QRegExp html_link_re_;
+ QRegExp whitespace_re_;
+ QRegExp html_link_rel_re_;
+ QRegExp html_link_type_re_;
+ QRegExp html_link_href_re_;
+};
+
+#endif // PODCASTURLLOADER_H