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.sql grooveshark-valicert-ca.pem schema/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