Allow podcasts to be added through the normal SongLoader mechanism (dragging to the playlist, on the commandline, "Add Stream", etc.)

This commit is contained in:
David Sansome 2012-03-11 17:57:15 +00:00
parent c27b8a5e95
commit f16fc8867e
20 changed files with 178 additions and 28 deletions

View File

@ -22,11 +22,14 @@
#include "core/tagreaderclient.h"
#include "core/timeconstants.h"
#include "internet/fixlastfm.h"
#include "internet/internetmodel.h"
#include "library/librarybackend.h"
#include "library/sqlrow.h"
#include "playlistparsers/parserbase.h"
#include "playlistparsers/cueparser.h"
#include "playlistparsers/playlistparser.h"
#include "podcasts/podcastparser.h"
#include "podcasts/podcastservice.h"
#include <QBuffer>
#include <QDirIterator>
@ -50,11 +53,13 @@ SongLoader::SongLoader(LibraryBackendInterface* library, QObject *parent)
: QObject(parent),
timeout_timer_(new QTimer(this)),
playlist_parser_(new PlaylistParser(library, this)),
podcast_parser_(new PodcastParser),
cue_parser_(new CueParser(library, this)),
timeout_(kDefaultTimeout),
state_(WaitingForType),
success_(false),
parser_(NULL),
is_podcast_(false),
library_(library)
{
if (sRawUriSchemes.isEmpty()) {
@ -72,6 +77,8 @@ SongLoader::~SongLoader() {
state_ = Finished;
gst_element_set_state(pipeline_.get(), GST_STATE_NULL);
}
delete podcast_parser_;
}
SongLoader::Result SongLoader::Load(const QUrl& url) {
@ -393,6 +400,19 @@ void SongLoader::StopTypefind() {
QBuffer buf(&buffer_);
buf.open(QIODevice::ReadOnly);
songs_ = parser_->Load(&buf);
} else if (success_ && is_podcast_) {
qLog(Debug) << "Parsing" << url_ << "as a podcast";
QBuffer buf(&buffer_);
buf.open(QIODevice::ReadOnly);
QVariant result = podcast_parser_->Load(&buf, url_);
if (result.isNull()) {
qLog(Warning) << "Failed to parse podcast";
} else {
InternetModel::Service<PodcastService>()->SubscribeAndShow(result);
}
} else if (success_) {
qLog(Debug) << "Loading" << url_ << "as raw stream";
@ -463,7 +483,7 @@ void SongLoader::TypeFound(GstElement*, uint, GstCaps* caps, void* self) {
qLog(Debug) << "Mime type is" << instance->mime_type_;
if (instance->mime_type_ == "text/plain" ||
instance->mime_type_ == "text/uri-list" ||
instance->mime_type_ == "application/xml") {
instance->podcast_parser_->supported_mime_types().contains(instance->mime_type_)) {
// Yeah it might be a playlist, let's get some data and have a better look
instance->state_ = WaitingForMagic;
return;
@ -582,24 +602,34 @@ void SongLoader::EndOfStreamReached() {
void SongLoader::MagicReady() {
qLog(Debug) << Q_FUNC_INFO;
parser_ = playlist_parser_->ParserForMagic(buffer_, mime_type_);
is_podcast_ = false;
if (!parser_) {
qLog(Warning) << url_.toString() << "is text, but not a recognised playlist";
// It doesn't look like a playlist, so just finish
StopTypefindAsync(false);
return;
// Maybe it's a podcast?
if (podcast_parser_->TryMagic(buffer_)) {
is_podcast_ = true;
qLog(Debug) << "Looks like a podcast";
} else {
qLog(Warning) << url_.toString() << "is text, but not a recognised playlist";
// It doesn't look like a playlist, so just finish
StopTypefindAsync(false);
return;
}
}
// It is a playlist - we'll get more data and parse the whole thing in
// EndOfStreamReached
qLog(Debug) << "Magic says" << parser_->name();
if (parser_->name() == "ASX/INI" && url_.scheme() == "http") {
// This is actually a weird MS-WMSP stream. Changing the protocol to MMS from
// HTTP makes it playable.
parser_ = NULL;
url_.setScheme("mms");
StopTypefindAsync(true);
// We'll get more data and parse the whole thing in EndOfStreamReached
if (!is_podcast_) {
qLog(Debug) << "Magic says" << parser_->name();
if (parser_->name() == "ASX/INI" && url_.scheme() == "http") {
// This is actually a weird MS-WMSP stream. Changing the protocol to MMS from
// HTTP makes it playable.
parser_ = NULL;
url_.setScheme("mms");
StopTypefindAsync(true);
}
}
state_ = WaitingForData;
if (!IsPipelinePlaying()) {

View File

@ -32,6 +32,7 @@ class CueParser;
class LibraryBackendInterface;
class ParserBase;
class PlaylistParser;
class PodcastParser;
class SongLoader : public QObject {
Q_OBJECT
@ -113,6 +114,7 @@ private:
QTimer* timeout_timer_;
PlaylistParser* playlist_parser_;
PodcastParser* podcast_parser_;
CueParser* cue_parser_;
// For async loads
@ -121,6 +123,7 @@ private:
bool success_;
ParserBase* parser_;
QString mime_type_;
bool is_podcast_;
QByteArray buffer_;
LibraryBackendInterface* library_;

View File

@ -92,6 +92,7 @@ void InternetModel::AddService(InternetService *service) {
connect(service, SIGNAL(StreamError(QString)), SIGNAL(StreamError(QString)));
connect(service, SIGNAL(StreamMetadataFound(QUrl,Song)), SIGNAL(StreamMetadataFound(QUrl,Song)));
connect(service, SIGNAL(AddToPlaylistSignal(QMimeData*)), SIGNAL(AddToPlaylist(QMimeData*)));
connect(service, SIGNAL(ScrollToIndex(QModelIndex)), SIGNAL(ScrollToIndex(QModelIndex)));
connect(service, SIGNAL(destroyed()), SLOT(ServiceDeleted()));
service->ReloadSettings();

View File

@ -157,6 +157,7 @@ signals:
void StreamMetadataFound(const QUrl& original_url, const Song& song);
void AddToPlaylist(QMimeData* data);
void ScrollToIndex(const QModelIndex& index);
private slots:
void ServiceDeleted();

View File

@ -70,6 +70,7 @@ signals:
void StreamMetadataFound(const QUrl& original_url, const Song& song);
void AddToPlaylistSignal(QMimeData* data);
void ScrollToIndex(const QModelIndex& index);
private slots:
void AppendToPlaylist();

View File

@ -146,3 +146,9 @@ void InternetViewContainer::SetHeaderHeight(int height) {
if (header)
header->setMaximumHeight(height);
}
void InternetViewContainer::ScrollToIndex(const QModelIndex& index) {
tree()->scrollTo(index, QTreeView::PositionAtCenter);
tree()->setCurrentIndex(index);
tree()->expand(index);
}

View File

@ -43,6 +43,9 @@ class InternetViewContainer : public QWidget {
InternetView* tree() const;
public slots:
void ScrollToIndex(const QModelIndex& index);
private slots:
void Collapsed(const QModelIndex& index);
void Expanded(const QModelIndex& index);

View File

@ -38,6 +38,17 @@ AddPodcastByUrl::~AddPodcastByUrl() {
delete ui_;
}
void AddPodcastByUrl::SetUrlAndGo(const QUrl& url) {
ui_->url->setText(url.toString());
GoClicked();
}
void AddPodcastByUrl::SetOpml(const OpmlContainer& opml) {
ui_->url->setText(opml.url.toString());
model()->clear();
model()->CreateOpmlContainerItems(opml, model()->invisibleRootItem());
}
void AddPodcastByUrl::GoClicked() {
emit Busy(true);
model()->clear();

View File

@ -21,10 +21,13 @@
#include "addpodcastpage.h"
class AddPodcastPage;
class OpmlContainer;
class PodcastUrlLoader;
class PodcastUrlLoaderReply;
class Ui_AddPodcastByUrl;
class QUrl;
class AddPodcastByUrl : public AddPodcastPage {
Q_OBJECT
@ -34,6 +37,9 @@ public:
void Show();
void SetOpml(const OpmlContainer& opml);
void SetUrlAndGo(const QUrl& url);
private slots:
void GoClicked();
void RequestFinished(PodcastUrlLoaderReply* reply);

View File

@ -67,7 +67,8 @@ AddPodcastDialog::AddPodcastDialog(Application* app, QWidget* parent)
ui_->button_box->addButton(settings_button, QDialogButtonBox::ResetRole);
// Add providers
AddPage(new AddPodcastByUrl(app, this));
by_url_page_ = new AddPodcastByUrl(app, this);
AddPage(by_url_page_);
AddPage(new FixedOpmlPage(QUrl(kBbcOpmlUrl), tr("BBC Podcasts"),
QIcon(":providers/bbc.png"), app, this));
AddPage(new GPodderTopTagsPage(app, this));
@ -81,6 +82,18 @@ AddPodcastDialog::~AddPodcastDialog() {
delete ui_;
}
void AddPodcastDialog::ShowWithUrl(const QUrl& url) {
by_url_page_->SetUrlAndGo(url);
ui_->provider_list->setCurrentRow(0);
show();
}
void AddPodcastDialog::ShowWithOpml(const OpmlContainer& opml) {
by_url_page_->SetOpml(opml);
ui_->provider_list->setCurrentRow(0);
show();
}
void AddPodcastDialog::AddPage(AddPodcastPage* page) {
pages_.append(page);
page_is_busy_.append(false);

View File

@ -22,8 +22,10 @@
#include <QDialog>
class AddPodcastByUrl;
class AddPodcastPage;
class Application;
class OpmlContainer;
class WidgetFadeHelper;
class Ui_AddPodcastDialog;
@ -38,6 +40,12 @@ public:
static const char* kBbcOpmlUrl;
// Convenience methods that open the dialog at the Add By Url page and fill
// it with either a URL (which is then fetched), or a pre-fetched OPML
// container.
void ShowWithUrl(const QUrl& url);
void ShowWithOpml(const OpmlContainer& opml);
private slots:
void OpenSettingsPage();
void AddPodcast();
@ -63,6 +71,7 @@ private:
QList<AddPodcastPage*> pages_;
QList<bool> page_is_busy_;
AddPodcastByUrl* by_url_page_;
WidgetFadeHelper* fader_;

View File

@ -21,6 +21,9 @@
#include "podcast.h"
struct OpmlContainer {
// Only set for the top-level container
QUrl url;
QString name;
QList<OpmlContainer> containers;
PodcastList feeds;

View File

@ -42,6 +42,12 @@ bool PodcastParser::SupportsContentType(const QString& content_type) const {
return false;
}
bool PodcastParser::TryMagic(const QByteArray& data) const {
QString str(QString::fromUtf8(data));
return str.contains(QRegExp("<rss\\b")) ||
str.contains(QRegExp("<opml\\b"));
}
QVariant PodcastParser::Load(QIODevice* device, const QUrl& url) const {
QXmlStreamReader reader(device);
@ -62,6 +68,7 @@ QVariant PodcastParser::Load(QIODevice* device, const QUrl& url) const {
if (!ParseOpml(&reader, &container)) {
return QVariant();
} else {
container.url = url;
return QVariant::fromValue(container);
}
}

View File

@ -44,6 +44,10 @@ public:
// error occurred parsing the XML.
QVariant Load(QIODevice* device, const QUrl& url) const;
// Really quick test to see if some data might be supported. Load() might
// still return a null QVariant.
bool TryMagic(const QByteArray& data) const;
private:
bool ParseRss(QXmlStreamReader* reader, Podcast* ret) const;
void ParseChannel(QXmlStreamReader* reader, Podcast* ret) const;

View File

@ -16,6 +16,7 @@
*/
#include "addpodcastdialog.h"
#include "opmlcontainer.h"
#include "podcastbackend.h"
#include "podcastdownloader.h"
#include "podcastservice.h"
@ -249,16 +250,6 @@ QStandardItem* PodcastService::CreatePodcastEpisodeItem(const PodcastEpisode& ep
return item;
}
QModelIndexList PodcastService::FilterByType(int type, const QModelIndexList& list) const {
QModelIndexList ret;
foreach (const QModelIndex& index, list) {
if (index.data(InternetModel::Role_Type).toInt() == type) {
ret.append(index);
}
}
return ret;
}
void PodcastService::ShowContextMenu(const QPoint& global_pos) {
if (!context_menu_) {
context_menu_ = new QMenu;
@ -377,11 +368,14 @@ void PodcastService::ReloadSettings() {
// TODO: reload the podcast icons that are already loaded?
}
void PodcastService::AddPodcast() {
void PodcastService::EnsureAddPodcastDialogCreated() {
if (!add_podcast_dialog_) {
add_podcast_dialog_.reset(new AddPodcastDialog(app_));
}
}
void PodcastService::AddPodcast() {
EnsureAddPodcastDialogCreated();
add_podcast_dialog_->show();
}
@ -391,7 +385,12 @@ void PodcastService::SubscriptionAdded(const Podcast& podcast) {
return;
}
model_->appendRow(CreatePodcastItem(podcast));
QStandardItem* item = CreatePodcastItem(podcast);
model_->appendRow(item);
if (scroll_to_database_id_.remove(podcast.database_id())) {
emit ScrollToIndex(MapToMergedModel(item->index()));
}
}
void PodcastService::SubscriptionRemoved(const Podcast& podcast) {
@ -527,3 +526,34 @@ void PodcastService::SetListened(const QModelIndexList& indexes, bool listened)
backend_->UpdateEpisodes(episodes);
}
QModelIndex PodcastService::MapToMergedModel(const QModelIndex& index) const {
return model()->merged_model()->mapFromSource(proxy_->mapFromSource(index));
}
void PodcastService::SubscribeAndShow(const QVariant& podcast_or_opml) {
if (podcast_or_opml.canConvert<Podcast>()) {
Podcast podcast(podcast_or_opml.value<Podcast>());
backend_->Subscribe(&podcast);
// Lazy load the root item if it hasn't been already
if (root_->data(InternetModel::Role_CanLazyLoad).toBool()) {
root_->setData(false, InternetModel::Role_CanLazyLoad);
LazyPopulate(root_);
}
QStandardItem* item = podcasts_by_database_id_[podcast.database_id()];
if (item) {
// There will be an item already if this podcast was already there.
emit ScrollToIndex(MapToMergedModel(item->index()));
} else {
// Otherwise we can remember the podcast ID and scroll to it when the
// item is created.
scroll_to_database_id_.insert(podcast.database_id());
}
} else if (podcast_or_opml.canConvert<OpmlContainer>()) {
EnsureAddPodcastDialogCreated();
add_podcast_dialog_->ShowWithOpml(podcast_or_opml.value<OpmlContainer>());
}
}

View File

@ -59,6 +59,11 @@ public:
void ShowContextMenu(const QPoint& global_pos);
void ReloadSettings();
// Called by SongLoader when the user adds a Podcast URL directly. Adds a
// subscription to the podcast and displays it in the UI. If the QVariant
// contains an OPML file then this displays it in the Add Podcast dialog.
void SubscribeAndShow(const QVariant& podcast_or_opml);
private slots:
void AddPodcast();
void UpdateSelectedPodcast();
@ -81,6 +86,8 @@ private slots:
void CurrentSongChanged(const Song& metadata);
private:
void EnsureAddPodcastDialogCreated();
void PopulatePodcastList(QStandardItem* parent);
void UpdatePodcastText(QStandardItem* item, int unlistened_count) const;
void UpdateEpisodeText(QStandardItem* item,
@ -90,7 +97,7 @@ private:
QStandardItem* CreatePodcastItem(const Podcast& podcast);
QStandardItem* CreatePodcastEpisodeItem(const PodcastEpisode& episode);
QModelIndexList FilterByType(int type, const QModelIndexList& list) const;
QModelIndex MapToMergedModel(const QModelIndex& index) const;
void SetListened(const QModelIndexList& indexes, bool listened);
@ -125,6 +132,8 @@ private:
QMap<int, QStandardItem*> podcasts_by_database_id_;
QMap<int, QStandardItem*> episodes_by_database_id_;
QSet<int> scroll_to_database_id_;
QScopedPointer<AddPodcastDialog> add_podcast_dialog_;
};

View File

@ -511,6 +511,7 @@ MainWindow::MainWindow(Application* app,
connect(app_->internet_model(), SIGNAL(StreamError(QString)), SLOT(ShowErrorDialog(QString)));
connect(app_->internet_model(), SIGNAL(StreamMetadataFound(QUrl,Song)), app_->playlist_manager(), SLOT(SetActiveStreamMetadata(QUrl,Song)));
connect(app_->internet_model(), SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
connect(app_->internet_model(), SIGNAL(ScrollToIndex(QModelIndex)), SLOT(ScrollToInternetIndex(QModelIndex)));
#ifdef HAVE_LIBLASTFM
LastFMService* lastfm_service = InternetModel::Service<LastFMService>();
connect(lastfm_service, SIGNAL(ButtonVisibilityChanged(bool)), SLOT(LastFMButtonVisibilityChanged(bool)));
@ -2229,3 +2230,8 @@ void MainWindow::HandleNotificationPreview(OSD::Behaviour type, QString line1, Q
osd_->ShowPreview(type, line1, line2, fake);
}
}
void MainWindow::ScrollToInternetIndex(const QModelIndex& index) {
internet_view_->ScrollToIndex(index);
ui_->tabs->SetCurrentWidget(internet_view_);
}

View File

@ -245,6 +245,8 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void HandleNotificationPreview(OSD::Behaviour type, QString line1, QString line2);
void ScrollToInternetIndex(const QModelIndex& index);
private:
void ConnectInfoView(SongInfoBase* view);

View File

@ -570,6 +570,10 @@ void FancyTabWidget::SetCurrentIndex(int index) {
}
}
void FancyTabWidget::SetCurrentWidget(QWidget* widget) {
SetCurrentIndex(stack_->indexOf(widget));
}
void FancyTabWidget::ShowWidget(int index) {
stack_->setCurrentIndex(index);
emit CurrentChanged(index);

View File

@ -184,6 +184,7 @@ public:
public slots:
void SetCurrentIndex(int index);
void SetCurrentWidget(QWidget* widget);
void SetMode(Mode mode);
void SetMode(int mode) { SetMode(Mode(mode)); }