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:
parent
c27b8a5e95
commit
f16fc8867e
@ -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()) {
|
||||
|
@ -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_;
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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_;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>());
|
||||
}
|
||||
}
|
||||
|
@ -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_;
|
||||
};
|
||||
|
||||
|
@ -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_);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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)); }
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user