Add an Add Podcast dialog

This commit is contained in:
David Sansome 2012-03-05 18:15:45 +00:00
parent 90bbed1ec9
commit c91acdb3f1
27 changed files with 1528 additions and 9 deletions

View File

@ -338,5 +338,7 @@
<file>schema/schema-36.sql</file>
<file>grooveshark-valicert-ca.pem</file>
<file>schema/schema-37.sql</file>
<file>providers/podcast16.png</file>
<file>providers/podcast32.png</file>
</qresource>
</RCC>

BIN
data/providers/podcast16.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

BIN
data/providers/podcast32.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

View File

@ -450,6 +450,16 @@ QStringList Updateify(const QStringList& list) {
return ret;
}
QString DecodeHtmlEntities(const QString& text) {
QString copy(text);
copy.replace("&amp;", "&");
copy.replace("&quot;", "\"");
copy.replace("&apos;", "'");
copy.replace("&lt;", "<");
copy.replace("&gt;", ">");
return copy;
}
int SetThreadIOPriority(IoPriority priority) {
#ifdef Q_OS_LINUX
return syscall(SYS_ioprio_set, IOPRIO_WHO_PROCESS, GetThreadId(),

View File

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

View File

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

View File

@ -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 <QCloseEvent>
@ -176,8 +177,7 @@ void MagnatuneDownloadDialog::MetadataFinished() {
}
// Munge the URL a bit
QString url_text = re.cap(1);
url_text.replace("&amp;", "&");
QString url_text = Utilities::DecodeHtmlEntities(re.cap(1));
QUrl url = QUrl(url_text);
url.setUserName(service_->username());

View File

@ -0,0 +1,70 @@
/* 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 "addpodcastbyurl.h"
#include "podcastdiscoverymodel.h"
#include "podcasturlloader.h"
#include "ui_addpodcastbyurl.h"
#include "core/closure.h"
#include <QNetworkReply>
#include <QMessageBox>
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));
}
}

View File

@ -0,0 +1,44 @@
/* 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 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

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddPodcastByUrl</class>
<widget class="QWidget" name="AddPodcastByUrl">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>431</width>
<height>51</height>
</rect>
</property>
<property name="windowTitle">
<string>Enter a URL</string>
</property>
<property name="windowIcon">
<iconset resource="../../data/data.qrc">
<normaloff>:/providers/podcast32.png</normaloff>:/providers/podcast32.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>If you know the URL of a podcast, enter it below and press Go.</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="url"/>
</item>
<item>
<widget class="QPushButton" name="go">
<property name="text">
<string>Go</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="../../data/data.qrc"/>
</resources>
<connections>
<connection>
<sender>url</sender>
<signal>returnPressed()</signal>
<receiver>go</receiver>
<slot>click()</slot>
<hints>
<hint type="sourcelabel">
<x>109</x>
<y>24</y>
</hint>
<hint type="destinationlabel">
<x>429</x>
<y>49</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,102 @@
/* 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 "addpodcastdialog.h"
#include "addpodcastbyurl.h"
#include "podcastdiscoverymodel.h"
#include "ui_addpodcastdialog.h"
#include "core/application.h"
#include "ui/iconloader.h"
#include <QPushButton>
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<Podcast>());
}
void AddPodcastDialog::PageBusyChanged(bool busy) {
const int index = pages_.indexOf(qobject_cast<AddPodcastPage*>(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() {
}

View File

@ -0,0 +1,54 @@
/* 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 ADDPODCASTDIALOG_H
#define ADDPODCASTDIALOG_H
#include <QDialog>
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<AddPodcastPage*> pages_;
QList<bool> page_is_busy_;
};
#endif // ADDPODCASTDIALOG_H

View File

@ -0,0 +1,179 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddPodcastDialog</class>
<widget class="QDialog" name="AddPodcastDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>887</width>
<height>447</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QListWidget" name="provider_list">
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="uniformItemSizes">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QStackedWidget" name="stack">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="results_stack">
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="results_page">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QTreeView" name="results">
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="busy_page">
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="BusyIndicator" name="widget" native="true">
<property name="text" stdset="0">
<string>Loading...</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>367</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</item>
<item>
<widget class="PodcastInfoWidget" name="details">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="button_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>BusyIndicator</class>
<extends>QWidget</extends>
<header>widgets/busyindicator.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>PodcastInfoWidget</class>
<extends>QFrame</extends>
<header>podcasts/podcastinfowidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>button_box</sender>
<signal>accepted()</signal>
<receiver>AddPodcastDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>827</x>
<y>436</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>button_box</sender>
<signal>rejected()</signal>
<receiver>AddPodcastDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>876</x>
<y>436</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,25 @@
/* 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 "addpodcastpage.h"
#include "podcastdiscoverymodel.h"
AddPodcastPage::AddPodcastPage(Application* app, QWidget* parent)
: QWidget(parent),
model_(new PodcastDiscoveryModel(app, this))
{
}

View File

@ -0,0 +1,41 @@
/* 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 ADDPODCASTPAGE_H
#define ADDPODCASTPAGE_H
#include <QWidget>
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

View File

@ -0,0 +1,67 @@
/* 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 "podcast.h"
#include "podcastdiscoverymodel.h"
#include "core/application.h"
#include "covers/albumcoverloader.h"
#include <QIcon>
#include <QSet>
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<quint64>::fromList(pending_covers_.keys()));
pending_covers_.clear();
}

View File

@ -0,0 +1,63 @@
/* 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 PODCASTDISCOVERYMODEL_H
#define PODCASTDISCOVERYMODEL_H
#include "covers/albumcoverloaderoptions.h"
#include <QStandardItemModel>
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<quint64, QStandardItem*> pending_covers_;
};
#endif // PODCASTDISCOVERYMODEL_H

View File

@ -0,0 +1,106 @@
/* 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 "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<QLabel*>()) {
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();
}
}

View File

@ -0,0 +1,58 @@
/* 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 PODCASTINFOWIDGET_H
#define PODCASTINFOWIDGET_H
#include "podcast.h"
#include "covers/albumcoverloaderoptions.h"
#include <QFrame>
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

View File

@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PodcastInfoWidget</class>
<widget class="QWidget" name="PodcastInfoWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>398</width>
<height>551</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="styleSheet">
<string notr="true">#title {
font-weight: bold;
}
#description {
font-size: smaller;
}</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="image">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="title">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="description">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="copyright_label">
<property name="text">
<string>Copyright</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="owner_label">
<property name="text">
<string>Owner</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="copyright">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="author_label">
<property name="text">
<string>Author</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="author">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="owner">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="website_label">
<property name="text">
<string>Website</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLabel" name="website">
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

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

View File

@ -18,6 +18,8 @@
#ifndef PODCASTPARSER_H
#define PODCASTPARSER_H
#include <QStringList>
#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

View File

@ -0,0 +1,67 @@
/* 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 "addpodcastdialog.h"
#include "podcastservice.h"
#include "internet/internetmodel.h"
#include "ui/iconloader.h"
#include <QMenu>
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();
}

View File

@ -0,0 +1,56 @@
/* 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 PODCASTSERVICE_H
#define PODCASTSERVICE_H
#include "internet/internetservice.h"
#include <QScopedPointer>
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<AddPodcastDialog> add_podcast_dialog_;
};
#endif // PODCASTSERVICE_H

View File

@ -0,0 +1,204 @@
/* 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 "podcastparser.h"
#include "podcasturlloader.h"
#include "core/closure.h"
#include "core/network.h"
#include "core/utilities.h"
#include <QNetworkReply>
const int PodcastUrlLoader::kMaxRedirects = 5;
PodcastUrlLoader::PodcastUrlLoader(QObject* parent)
: QObject(parent),
network_(new NetworkAccessManager(this)),
parser_(new PodcastParser),
html_link_re_("<link (.*)>"),
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);
}

View File

@ -0,0 +1,98 @@
/* 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 PODCASTURLLOADER_H
#define PODCASTURLLOADER_H
#include <QObject>
#include <QRegExp>
#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<QString, QString> QuickPrefix;
typedef QList<QuickPrefix> 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