diff --git a/src/core/songloader.cpp b/src/core/songloader.cpp index d2d48914a..b3d6df153 100644 --- a/src/core/songloader.cpp +++ b/src/core/songloader.cpp @@ -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 #include @@ -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()->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()) { diff --git a/src/core/songloader.h b/src/core/songloader.h index d41983857..a6966c3fc 100644 --- a/src/core/songloader.h +++ b/src/core/songloader.h @@ -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_; diff --git a/src/internet/internetmodel.cpp b/src/internet/internetmodel.cpp index c666ec558..a12a91e8d 100644 --- a/src/internet/internetmodel.cpp +++ b/src/internet/internetmodel.cpp @@ -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(); diff --git a/src/internet/internetmodel.h b/src/internet/internetmodel.h index 7b9d1c782..99efcd15e 100644 --- a/src/internet/internetmodel.h +++ b/src/internet/internetmodel.h @@ -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(); diff --git a/src/internet/internetservice.h b/src/internet/internetservice.h index ef97105f1..4d0bc6745 100644 --- a/src/internet/internetservice.h +++ b/src/internet/internetservice.h @@ -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(); diff --git a/src/internet/internetviewcontainer.cpp b/src/internet/internetviewcontainer.cpp index 75bb2ae98..6950bdfa9 100644 --- a/src/internet/internetviewcontainer.cpp +++ b/src/internet/internetviewcontainer.cpp @@ -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); +} diff --git a/src/internet/internetviewcontainer.h b/src/internet/internetviewcontainer.h index 0824c0b02..5da4f8844 100644 --- a/src/internet/internetviewcontainer.h +++ b/src/internet/internetviewcontainer.h @@ -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); diff --git a/src/podcasts/addpodcastbyurl.cpp b/src/podcasts/addpodcastbyurl.cpp index a199adee8..6333906ed 100644 --- a/src/podcasts/addpodcastbyurl.cpp +++ b/src/podcasts/addpodcastbyurl.cpp @@ -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(); diff --git a/src/podcasts/addpodcastbyurl.h b/src/podcasts/addpodcastbyurl.h index 94bf85c15..2da080874 100644 --- a/src/podcasts/addpodcastbyurl.h +++ b/src/podcasts/addpodcastbyurl.h @@ -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); diff --git a/src/podcasts/addpodcastdialog.cpp b/src/podcasts/addpodcastdialog.cpp index 20c97e12c..6d2f1fde7 100644 --- a/src/podcasts/addpodcastdialog.cpp +++ b/src/podcasts/addpodcastdialog.cpp @@ -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); diff --git a/src/podcasts/addpodcastdialog.h b/src/podcasts/addpodcastdialog.h index 44fc1ff7e..f4cad6a3e 100644 --- a/src/podcasts/addpodcastdialog.h +++ b/src/podcasts/addpodcastdialog.h @@ -22,8 +22,10 @@ #include +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 pages_; QList page_is_busy_; + AddPodcastByUrl* by_url_page_; WidgetFadeHelper* fader_; diff --git a/src/podcasts/opmlcontainer.h b/src/podcasts/opmlcontainer.h index 081a0f729..9bde9099d 100644 --- a/src/podcasts/opmlcontainer.h +++ b/src/podcasts/opmlcontainer.h @@ -21,6 +21,9 @@ #include "podcast.h" struct OpmlContainer { + // Only set for the top-level container + QUrl url; + QString name; QList containers; PodcastList feeds; diff --git a/src/podcasts/podcastparser.cpp b/src/podcasts/podcastparser.cpp index 4cbc2d7a6..069aba697 100644 --- a/src/podcasts/podcastparser.cpp +++ b/src/podcasts/podcastparser.cpp @@ -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("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_or_opml.value()); + 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()) { + EnsureAddPodcastDialogCreated(); + + add_podcast_dialog_->ShowWithOpml(podcast_or_opml.value()); + } +} diff --git a/src/podcasts/podcastservice.h b/src/podcasts/podcastservice.h index 12dc4a7c9..c34fe43a3 100644 --- a/src/podcasts/podcastservice.h +++ b/src/podcasts/podcastservice.h @@ -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 podcasts_by_database_id_; QMap episodes_by_database_id_; + QSet scroll_to_database_id_; + QScopedPointer add_podcast_dialog_; }; diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index af7f6272d..e499d6dac 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -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(); 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_); +} diff --git a/src/ui/mainwindow.h b/src/ui/mainwindow.h index 7adc6f93b..f81be8b81 100644 --- a/src/ui/mainwindow.h +++ b/src/ui/mainwindow.h @@ -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); diff --git a/src/widgets/fancytabwidget.cpp b/src/widgets/fancytabwidget.cpp index 30a340b1d..82b8ad348 100644 --- a/src/widgets/fancytabwidget.cpp +++ b/src/widgets/fancytabwidget.cpp @@ -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); diff --git a/src/widgets/fancytabwidget.h b/src/widgets/fancytabwidget.h index 8c13353d3..f2d12bce2 100644 --- a/src/widgets/fancytabwidget.h +++ b/src/widgets/fancytabwidget.h @@ -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)); }