Add an iTunes podcast search page

This commit is contained in:
David Sansome 2012-03-07 12:27:31 +00:00
parent 628820917d
commit 17dfc99462
14 changed files with 242 additions and 21 deletions

View File

@ -341,5 +341,6 @@
<file>providers/podcast16.png</file>
<file>providers/podcast32.png</file>
<file>providers/mygpo32.png</file>
<file>providers/itunes.png</file>
</qresource>
</RCC>

BIN
data/providers/itunes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -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,

View File

@ -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

View File

@ -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);
}

View File

@ -0,0 +1,103 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
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 <http://www.gnu.org/licenses/>.
*/
#include "itunessearchpage.h"
#include "podcast.h"
#include "podcastdiscoverymodel.h"
#include "ui_itunessearchpage.h"
#include "core/closure.h"
#include "core/network.h"
#include <qjson/parser.h>
#include <QMessageBox>
#include <QNetworkReply>
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));
}
}

View File

@ -0,0 +1,47 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
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 <http://www.gnu.org/licenses/>.
*/
#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

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ITunesSearchPage</class>
<widget class="QWidget" name="ITunesSearchPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>516</width>
<height>69</height>
</rect>
</property>
<property name="windowTitle">
<string>Search iTunes</string>
</property>
<property name="windowIcon">
<iconset resource="../../data/data.qrc">
<normaloff>:/providers/itunes.png</normaloff>:/providers/itunes.png</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Enter search terms below to find podcasts in the iTunes Store</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="query"/>
</item>
<item>
<widget class="QPushButton" name="search">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="../../data/data.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -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());

View File

@ -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;

View File

@ -72,8 +72,8 @@ void PodcastDiscoveryModel::LazyLoadImage(const QModelIndex& index) {
Podcast podcast = index.data(Role_Podcast).value<Podcast>();
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);
}
}

View File

@ -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();

View File

@ -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);
}

View File

@ -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;