diff --git a/data/data.qrc b/data/data.qrc index 47ebe1680..764267795 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -341,5 +341,6 @@ providers/podcast16.png providers/podcast32.png providers/mygpo32.png + providers/itunes.png diff --git a/data/providers/itunes.png b/data/providers/itunes.png new file mode 100644 index 000000000..656b12ed7 Binary files /dev/null and b/data/providers/itunes.png differ diff --git a/data/schema/schema-37.sql b/data/schema/schema-37.sql index c83995408..0bb53957c 100644 --- a/data/schema/schema-37.sql +++ b/data/schema/schema-37.sql @@ -4,7 +4,8 @@ CREATE TABLE podcasts ( description TEXT, copyright TEXT, link TEXT, - image_url TEXT, + image_url_large TEXT, + image_url_small TEXT, author TEXT, owner_name TEXT, owner_email TEXT, diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fe926ed99..a2770279d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -234,6 +234,7 @@ set(SOURCES podcasts/gpoddersearchpage.cpp podcasts/gpoddertoptagsmodel.cpp podcasts/gpoddertoptagspage.cpp + podcasts/itunessearchpage.cpp podcasts/podcast.cpp podcasts/podcastbackend.cpp podcasts/podcastdiscoverymodel.cpp @@ -485,6 +486,7 @@ set(HEADERS podcasts/gpoddersearchpage.h podcasts/gpoddertoptagsmodel.h podcasts/gpoddertoptagspage.h + podcasts/itunessearchpage.h podcasts/podcastbackend.h podcasts/podcastdiscoverymodel.h podcasts/podcastinfowidget.h @@ -612,6 +614,7 @@ set(UI podcasts/addpodcastbyurl.ui podcasts/addpodcastdialog.ui podcasts/gpoddersearchpage.ui + podcasts/itunessearchpage.ui podcasts/podcastinfowidget.ui remote/remotesettingspage.ui diff --git a/src/podcasts/addpodcastdialog.cpp b/src/podcasts/addpodcastdialog.cpp index 62d1f1ace..c9029a730 100644 --- a/src/podcasts/addpodcastdialog.cpp +++ b/src/podcasts/addpodcastdialog.cpp @@ -19,6 +19,7 @@ #include "addpodcastbyurl.h" #include "gpoddersearchpage.h" #include "gpoddertoptagspage.h" +#include "itunessearchpage.h" #include "podcastbackend.h" #include "podcastdiscoverymodel.h" #include "ui_addpodcastdialog.h" @@ -58,6 +59,7 @@ AddPodcastDialog::AddPodcastDialog(Application* app, QWidget* parent) AddPage(new AddPodcastByUrl(app, this)); AddPage(new GPodderTopTagsPage(app, this)); AddPage(new GPodderSearchPage(app, this)); + AddPage(new ITunesSearchPage(app, this)); ui_->provider_list->setCurrentRow(0); } diff --git a/src/podcasts/itunessearchpage.cpp b/src/podcasts/itunessearchpage.cpp new file mode 100644 index 000000000..4df8a64c8 --- /dev/null +++ b/src/podcasts/itunessearchpage.cpp @@ -0,0 +1,103 @@ +/* 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 "itunessearchpage.h" +#include "podcast.h" +#include "podcastdiscoverymodel.h" +#include "ui_itunessearchpage.h" +#include "core/closure.h" +#include "core/network.h" + +#include + +#include +#include + +const char* ITunesSearchPage::kUrlBase = + "http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStoreServices.woa/wa/wsSearch?country=US&media=podcast"; + +ITunesSearchPage::ITunesSearchPage(Application* app, QWidget* parent) + : AddPodcastPage(app, parent), + ui_(new Ui_ITunesSearchPage), + network_(new NetworkAccessManager(this)) +{ + ui_->setupUi(this); + connect(ui_->search, SIGNAL(clicked()), SLOT(SearchClicked())); +} + +ITunesSearchPage::~ITunesSearchPage() { + delete ui_; +} + +void ITunesSearchPage::SearchClicked() { + emit Busy(true); + + QUrl url(QUrl::fromEncoded(kUrlBase)); + url.addQueryItem("term", ui_->query->text()); + + QNetworkReply* reply = network_->get(QNetworkRequest(url)); + NewClosure(reply, SIGNAL(finished()), + this, SLOT(SearchFinished(QNetworkReply*)), + reply); +} + +void ITunesSearchPage::SearchFinished(QNetworkReply* reply) { + reply->deleteLater(); + emit Busy(false); + + model()->clear(); + + // Was there a network error? + if (reply->error() != QNetworkReply::NoError) { + QMessageBox::warning(this, tr("Failed to fetch podcasts"), reply->errorString()); + return; + } + + QJson::Parser parser; + QVariant data = parser.parse(reply); + + // Was it valid JSON? + if (data.isNull()) { + QMessageBox::warning(this, tr("Failed to fetch podcasts"), + tr("There was a problem parsing the response from the iTunes Store")); + return; + } + + // Was there an error message in the JSON? + if (data.toMap().contains("errorMessage")) { + QMessageBox::warning(this, tr("Failed to fetch podcasts"), + data.toMap()["errorMessage"].toString()); + return; + } + + foreach (const QVariant& result_variant, data.toMap()["results"].toList()) { + QVariantMap result(result_variant.toMap()); + if (result["kind"].toString() != "podcast") { + continue; + } + + Podcast podcast; + podcast.set_author(result["artistName"].toString()); + podcast.set_title(result["trackName"].toString()); + podcast.set_url(result["feedUrl"].toUrl()); + podcast.set_link(result["trackViewUrl"].toUrl()); + podcast.set_image_url_small(result["artworkUrl30"].toString()); + podcast.set_image_url_large(result["artworkUrl100"].toString()); + + model()->appendRow(model()->CreatePodcastItem(podcast)); + } +} diff --git a/src/podcasts/itunessearchpage.h b/src/podcasts/itunessearchpage.h new file mode 100644 index 000000000..c976e0b4a --- /dev/null +++ b/src/podcasts/itunessearchpage.h @@ -0,0 +1,47 @@ +/* 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 ITUNESSEARCHPAGE_H +#define ITUNESSEARCHPAGE_H + +#include "addpodcastpage.h" + +class Ui_ITunesSearchPage; + +class QNetworkAccessManager; +class QNetworkReply; + +class ITunesSearchPage : public AddPodcastPage { + Q_OBJECT + +public: + ITunesSearchPage(Application* app, QWidget* parent); + ~ITunesSearchPage(); + + static const char* kUrlBase; + +private slots: + void SearchClicked(); + void SearchFinished(QNetworkReply* reply); + +private: + Ui_ITunesSearchPage* ui_; + + QNetworkAccessManager* network_; +}; + +#endif // ITUNESSEARCHPAGE_H diff --git a/src/podcasts/itunessearchpage.ui b/src/podcasts/itunessearchpage.ui new file mode 100644 index 000000000..ed82bb339 --- /dev/null +++ b/src/podcasts/itunessearchpage.ui @@ -0,0 +1,51 @@ + + + ITunesSearchPage + + + + 0 + 0 + 516 + 69 + + + + Search iTunes + + + + :/providers/itunes.png:/providers/itunes.png + + + + 0 + + + + + Enter search terms below to find podcasts in the iTunes Store + + + + + + + + + + + + Search + + + + + + + + + + + + diff --git a/src/podcasts/podcast.cpp b/src/podcasts/podcast.cpp index f5971b86d..a095cf353 100644 --- a/src/podcasts/podcast.cpp +++ b/src/podcasts/podcast.cpp @@ -24,7 +24,8 @@ const QStringList Podcast::kColumns = QStringList() << "url" << "title" << "description" << "copyright" << "link" - << "image_url" << "author" << "owner_name" << "owner_email" << "extra"; + << "image_url_large" << "image_url_small" << "author" << "owner_name" + << "owner_email" << "extra"; const QString Podcast::kColumnSpec = Podcast::kColumns.join(", "); const QString Podcast::kJoinSpec = Utilities::Prepend("p.", Podcast::kColumns).join(", "); @@ -42,7 +43,8 @@ struct Podcast::Private : public QSharedData { QString description_; QString copyright_; QUrl link_; - QUrl image_url_; + QUrl image_url_large_; + QUrl image_url_small_; // iTunes extensions QString author_; @@ -85,7 +87,8 @@ const QString& Podcast::title() const { return d->title_; } const QString& Podcast::description() const { return d->description_; } const QString& Podcast::copyright() const { return d->copyright_; } const QUrl& Podcast::link() const { return d->link_; } -const QUrl& Podcast::image_url() const { return d->image_url_; } +const QUrl& Podcast::image_url_large() const { return d->image_url_large_; } +const QUrl& Podcast::image_url_small() const { return d->image_url_small_; } const QString& Podcast::author() const { return d->author_; } const QString& Podcast::owner_name() const { return d->owner_name_; } const QString& Podcast::owner_email() const { return d->owner_email_; } @@ -98,7 +101,8 @@ void Podcast::set_title(const QString& v) { d->title_ = v; } void Podcast::set_description(const QString& v) { d->description_ = v; } void Podcast::set_copyright(const QString& v) { d->copyright_ = v; } void Podcast::set_link(const QUrl& v) { d->link_ = v; } -void Podcast::set_image_url(const QUrl& v) { d->image_url_ = v; } +void Podcast::set_image_url_large(const QUrl& v) { d->image_url_large_ = v; } +void Podcast::set_image_url_small(const QUrl& v) { d->image_url_small_ = v; } void Podcast::set_author(const QString& v) { d->author_ = v; } void Podcast::set_owner_name(const QString& v) { d->owner_name_ = v; } void Podcast::set_owner_email(const QString& v) { d->owner_email_ = v; } @@ -117,12 +121,13 @@ void Podcast::InitFromQuery(const QSqlQuery& query) { d->description_ = query.value(3).toString(); d->copyright_ = query.value(4).toString(); d->link_ = QUrl::fromEncoded(query.value(5).toByteArray()); - d->image_url_ = QUrl::fromEncoded(query.value(6).toByteArray()); - d->author_ = query.value(7).toString(); - d->owner_name_ = query.value(8).toString(); - d->owner_email_ = query.value(9).toString(); + d->image_url_large_ = QUrl::fromEncoded(query.value(6).toByteArray()); + d->image_url_small_ = QUrl::fromEncoded(query.value(7).toByteArray()); + d->author_ = query.value(8).toString(); + d->owner_name_ = query.value(9).toString(); + d->owner_email_ = query.value(10).toString(); - QDataStream extra_stream(query.value(10).toByteArray()); + QDataStream extra_stream(query.value(11).toByteArray()); extra_stream >> d->extra_; } @@ -132,7 +137,8 @@ void Podcast::BindToQuery(QSqlQuery* query) const { query->bindValue(":description", d->description_); query->bindValue(":copyright", d->copyright_); query->bindValue(":link", d->link_.toEncoded()); - query->bindValue(":image_url", d->image_url_.toEncoded()); + query->bindValue(":image_url_large", d->image_url_large_.toEncoded()); + query->bindValue(":image_url_small", d->image_url_small_.toEncoded()); query->bindValue(":author", d->author_); query->bindValue(":owner_name", d->owner_name_); query->bindValue(":owner_email", d->owner_email_); @@ -149,7 +155,7 @@ void Podcast::InitFromGpo(const mygpo::Podcast* podcast) { d->title_ = podcast->title(); d->description_ = podcast->description(); d->link_ = podcast->website(); - d->image_url_ = podcast->logoUrl(); + d->image_url_large_ = podcast->logoUrl(); set_extra("gpodder:subscribers", podcast->subscribers()); set_extra("gpodder:subscribers_last_week", podcast->subscribersLastWeek()); diff --git a/src/podcasts/podcast.h b/src/podcasts/podcast.h index 469772106..47a2f5f96 100644 --- a/src/podcasts/podcast.h +++ b/src/podcasts/podcast.h @@ -54,7 +54,8 @@ public: const QString& description() const; const QString& copyright() const; const QUrl& link() const; - const QUrl& image_url() const; + const QUrl& image_url_large() const; + const QUrl& image_url_small() const; const QString& author() const; const QString& owner_name() const; const QString& owner_email() const; @@ -67,13 +68,19 @@ public: void set_description(const QString& v); void set_copyright(const QString& v); void set_link(const QUrl& v); - void set_image_url(const QUrl& v); + void set_image_url_large(const QUrl& v); + void set_image_url_small(const QUrl& v); void set_author(const QString& v); void set_owner_name(const QString& v); void set_owner_email(const QString& v); void set_extra(const QVariantMap& v); void set_extra(const QString& key, const QVariant& value); + // Small images are suitable for 16x16 icons in lists. Large images are + // used in detailed information displays. + const QUrl& ImageUrlLarge() const { return image_url_large().isValid() ? image_url_large() : image_url_small(); } + const QUrl& ImageUrlSmall() const { return image_url_small().isValid() ? image_url_small() : image_url_large(); } + // These are stored in a different database table, and aren't loaded or // persisted by InitFromQuery or BindToQuery. const PodcastEpisodeList& episodes() const; diff --git a/src/podcasts/podcastdiscoverymodel.cpp b/src/podcasts/podcastdiscoverymodel.cpp index e124272db..1012f0d07 100644 --- a/src/podcasts/podcastdiscoverymodel.cpp +++ b/src/podcasts/podcastdiscoverymodel.cpp @@ -72,8 +72,8 @@ void PodcastDiscoveryModel::LazyLoadImage(const QModelIndex& index) { Podcast podcast = index.data(Role_Podcast).value(); - if (podcast.image_url().isValid()) { - icon_loader_->LoadIcon(podcast.image_url().toString(), QString(), item); + if (podcast.ImageUrlSmall().isValid()) { + icon_loader_->LoadIcon(podcast.ImageUrlSmall().toString(), QString(), item); } } diff --git a/src/podcasts/podcastinfowidget.cpp b/src/podcasts/podcastinfowidget.cpp index 0a5287d01..2669aad14 100644 --- a/src/podcasts/podcastinfowidget.cpp +++ b/src/podcasts/podcastinfowidget.cpp @@ -78,10 +78,10 @@ void PodcastInfoWidget::SetPodcast(const Podcast& podcast) { podcast_ = podcast; - if (podcast.image_url().isValid()) { + if (podcast.ImageUrlLarge().isValid()) { // Start loading an image for this item. image_id_ = app_->album_cover_loader()->LoadImageAsync( - cover_options_, podcast.image_url().toString(), QString()); + cover_options_, podcast.ImageUrlLarge().toString(), QString()); } ui_->image->hide(); diff --git a/src/podcasts/podcastparser.cpp b/src/podcasts/podcastparser.cpp index 4ef51e80c..5d510714d 100644 --- a/src/podcasts/podcastparser.cpp +++ b/src/podcasts/podcastparser.cpp @@ -96,7 +96,7 @@ void PodcastParser::ParseImage(QXmlStreamReader* reader, Podcast* ret) const { case QXmlStreamReader::StartElement: { const QStringRef name = reader->name(); if (name == "url") { - ret->set_image_url(QUrl(reader->readElementText())); + ret->set_image_url_large(QUrl(reader->readElementText())); } else { Utilities::ConsumeCurrentElement(reader); } diff --git a/src/podcasts/podcastservice.cpp b/src/podcasts/podcastservice.cpp index 7e1f328d8..61a0e33e8 100644 --- a/src/podcasts/podcastservice.cpp +++ b/src/podcasts/podcastservice.cpp @@ -100,8 +100,8 @@ QStandardItem* PodcastService::CreatePodcastItem(const Podcast& podcast) { item->setData(QVariant::fromValue(podcast), Role_Podcast); // Load the podcast's image if it has one - if (podcast.image_url().isValid()) { - icon_loader_->LoadIcon(podcast.image_url().toString(), QString(), item); + if (podcast.ImageUrlSmall().isValid()) { + icon_loader_->LoadIcon(podcast.ImageUrlSmall().toString(), QString(), item); } return item;