779 lines
24 KiB
C++
779 lines
24 KiB
C++
/*
|
|
* Cantata
|
|
*
|
|
* Copyright (c) 2011-2022 Craig Drummond <craig.p.drummond@gmail.com>
|
|
*
|
|
* ----
|
|
*
|
|
* This program 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 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program 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 this program; see the file COPYING. If not, write to
|
|
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include "podcastsearchdialog.h"
|
|
#include "support/pagewidget.h"
|
|
#include "support/lineedit.h"
|
|
#include "network/networkaccessmanager.h"
|
|
#include "opmlparser.h"
|
|
#include "widgets/icons.h"
|
|
#include "support/spinner.h"
|
|
#include "widgets/basicitemdelegate.h"
|
|
#include "podcastservice.h"
|
|
#include "support/utils.h"
|
|
#include "support/action.h"
|
|
#include "support/monoicon.h"
|
|
#include "widgets/textbrowser.h"
|
|
#include "support/messagewidget.h"
|
|
#include "support/flattoolbutton.h"
|
|
#include "gui/covers.h"
|
|
#include "rssparser.h"
|
|
#include "podcastservice.h"
|
|
#include "config.h"
|
|
#include <QLabel>
|
|
#include <QPushButton>
|
|
#include <QTreeWidget>
|
|
#include <QGridLayout>
|
|
#include <QBoxLayout>
|
|
#include <QHeaderView>
|
|
#include <QStringList>
|
|
#include <QFile>
|
|
#include <QFileDialog>
|
|
#include <QXmlStreamReader>
|
|
#include <QCryptographicHash>
|
|
#include <QCache>
|
|
#include <QImage>
|
|
#include <QBuffer>
|
|
#include <QCoreApplication>
|
|
#include <QDesktopServices>
|
|
#include <QNetworkRequest>
|
|
#include <QUrlQuery>
|
|
#include <QJsonDocument>
|
|
|
|
static int iCount=0;
|
|
|
|
static QCache<QUrl, QImage> imageCache(200*1024);
|
|
static int maxImageSize=-1;
|
|
static const char * constOrigUrlProperty="orig-url";
|
|
|
|
enum Roles {
|
|
IsPodcastRole = Qt::UserRole,
|
|
UrlRole,
|
|
ImageUrlRole,
|
|
DescriptionRole,
|
|
WebPageUrlRole
|
|
};
|
|
|
|
QString PodcastSearchDialog::constCacheDir=QLatin1String("podcast-directories");
|
|
QString PodcastSearchDialog::constExt=QLatin1String(".opml");
|
|
|
|
static QString generateCacheFileName(const QUrl &url, bool create)
|
|
{
|
|
QString hash=QCryptographicHash::hash(url.toString().toUtf8(), QCryptographicHash::Md5).toHex();
|
|
QString dir=Utils::cacheDir(PodcastSearchDialog::constCacheDir, create);
|
|
return dir.isEmpty() ? QString() : (dir+hash+PodcastSearchDialog::constExt);
|
|
}
|
|
|
|
class ITunesSearchPage : public PodcastSearchPage
|
|
{
|
|
public:
|
|
ITunesSearchPage(QWidget *p)
|
|
: PodcastSearchPage(p,
|
|
QLatin1String("iTunes"),
|
|
FontAwesome::apple,
|
|
QUrl(QLatin1String("http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStoreServices.woa/wa/wsSearch")),
|
|
QLatin1String("term"),
|
|
QStringList() << QLatin1String("country") << QLatin1String("US") << QLatin1String("media") << QLatin1String("podcast"))
|
|
{
|
|
}
|
|
|
|
void parse(const QVariant &data) override
|
|
{
|
|
for (const QVariant &resultVariant: data.toMap()[QLatin1String("results")].toList()) {
|
|
QVariantMap result(resultVariant.toMap());
|
|
if (result[QLatin1String("kind")].toString() != QLatin1String("podcast")) {
|
|
continue;
|
|
}
|
|
|
|
addPodcast(result[QLatin1String("trackName")].toString(),
|
|
result[QLatin1String("feedUrl")].toUrl(),
|
|
result[QLatin1String("artworkUrl100")].toUrl(),
|
|
QString(),
|
|
result[QLatin1String("collectionViewUrl")].toString(),
|
|
nullptr);
|
|
}
|
|
}
|
|
};
|
|
|
|
class GPodderSearchPage : public PodcastSearchPage
|
|
{
|
|
public:
|
|
GPodderSearchPage(QWidget *p)
|
|
: PodcastSearchPage(p,
|
|
QLatin1String("GPodder"),
|
|
FontAwesome::podcast,
|
|
QUrl(QLatin1String("http://gpodder.net/search.json")),
|
|
QLatin1String("q"))
|
|
{
|
|
}
|
|
|
|
void parse(const QVariant &data) override
|
|
{
|
|
QVariantList list=data.toList();
|
|
for (const QVariant &var: list) {
|
|
QVariantMap map=var.toMap();
|
|
addPodcast(map[QLatin1String("title")].toString(),
|
|
map[QLatin1String("url")].toUrl(),
|
|
map[QLatin1String("logo_url")].toUrl(),
|
|
map[QLatin1String("description")].toString(),
|
|
map[QLatin1String("website")].toString(),
|
|
nullptr);
|
|
}
|
|
}
|
|
};
|
|
|
|
PodcastPage::PodcastPage(QWidget *p, const QString &n)
|
|
: QWidget(p)
|
|
, pageName(n)
|
|
, job(nullptr)
|
|
, imageJob(nullptr)
|
|
{
|
|
tree = new QTreeWidget(this);
|
|
tree->setItemDelegate(new BasicItemDelegate(tree));
|
|
tree->header()->setVisible(false);
|
|
text=new TextBrowser(this);
|
|
spinner=new Spinner(this);
|
|
spinner->setWidget(tree);
|
|
imageSpinner=new Spinner(this);
|
|
imageSpinner->setWidget(text);
|
|
connect(tree, SIGNAL(itemSelectionChanged()), SLOT(selectionChanged()));
|
|
tree->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
|
|
text->setOpenLinks(false);
|
|
connect(text, SIGNAL(anchorClicked(QUrl)), SLOT(openLink(QUrl)));
|
|
updateText();
|
|
}
|
|
|
|
QUrl PodcastPage::currentRss() const
|
|
{
|
|
QList<QTreeWidgetItem *> selection=tree->selectedItems();
|
|
return selection.isEmpty() ? QUrl() : selection.at(0)->data(0, UrlRole).toUrl();
|
|
}
|
|
|
|
void PodcastPage::fetch(const QUrl &url)
|
|
{
|
|
cancel();
|
|
tree->clear();
|
|
spinner->start();
|
|
job=NetworkAccessManager::self()->get(url);
|
|
connect(job, SIGNAL(finished()), this, SLOT(jobFinished()));
|
|
}
|
|
|
|
void PodcastPage::fetchImage(const QUrl &url)
|
|
{
|
|
cancelImage();
|
|
imageSpinner->start();
|
|
imageJob=NetworkAccessManager::self()->get(url, 5000);
|
|
imageJob->setProperty(constOrigUrlProperty, url);
|
|
connect(imageJob, SIGNAL(finished()), this, SLOT(imageJobFinished()));
|
|
}
|
|
|
|
void PodcastPage::cancel()
|
|
{
|
|
spinner->stop();
|
|
if (job) {
|
|
job->cancelAndDelete();
|
|
job=nullptr;
|
|
}
|
|
}
|
|
|
|
void PodcastPage::cancelImage()
|
|
{
|
|
imageSpinner->stop();
|
|
if (imageJob) {
|
|
imageJob->cancelAndDelete();
|
|
imageJob=nullptr;
|
|
}
|
|
}
|
|
|
|
void PodcastPage::addPodcast(const QString &title, const QUrl &url, const QUrl &image, const QString &description, const QString &webPage, QTreeWidgetItem *p)
|
|
{
|
|
if (title.isEmpty() || url.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
QTreeWidgetItem *podItem=p ? new QTreeWidgetItem(p, QStringList() << title)
|
|
: new QTreeWidgetItem(tree, QStringList() << title);
|
|
|
|
podItem->setData(0, IsPodcastRole, true);
|
|
podItem->setData(0, UrlRole, url);
|
|
podItem->setData(0, ImageUrlRole, image);
|
|
podItem->setData(0, DescriptionRole, description);
|
|
podItem->setData(0, WebPageUrlRole, webPage);
|
|
podItem->setIcon(0, Icons::self()->audioListIcon);
|
|
}
|
|
|
|
void PodcastPage::addCategory(const OpmlParser::Category &cat, QTreeWidgetItem *p)
|
|
{
|
|
if (cat.categories.isEmpty() && cat.podcasts.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
QTreeWidgetItem *catItem=p ? new QTreeWidgetItem(p, QStringList() << cat.name)
|
|
: new QTreeWidgetItem(tree, QStringList() << cat.name);
|
|
|
|
catItem->setData(0, IsPodcastRole, false);
|
|
catItem->setIcon(0, Icons::self()->folderListIcon);
|
|
for (const OpmlParser::Podcast &pod: cat.podcasts) {
|
|
addPodcast(pod, catItem);
|
|
}
|
|
for (const OpmlParser::Category &subCat: cat.categories) {
|
|
addCategory(subCat, catItem);
|
|
}
|
|
}
|
|
|
|
void PodcastPage::addPodcast(const OpmlParser::Podcast &pod, QTreeWidgetItem *p)
|
|
{
|
|
addPodcast(pod.name, pod.url, pod.image, pod.description, pod.htmlUrl, p);
|
|
}
|
|
|
|
static QString encode(const QImage &img)
|
|
{
|
|
QByteArray bytes;
|
|
QBuffer buffer(&bytes);
|
|
buffer.open(QIODevice::WriteOnly);
|
|
img.save(&buffer, "PNG");
|
|
return QString("<br/><img src=\"data:image/png;base64,%1\"><br/>").arg(QString(buffer.data().toBase64()));
|
|
}
|
|
|
|
void PodcastPage::updateText()
|
|
{
|
|
QList<QTreeWidgetItem *> selection=tree->selectedItems();
|
|
if (!selection.isEmpty()) {
|
|
QTreeWidgetItem *item=selection.at(0);
|
|
if (item->data(0, IsPodcastRole).toBool()) {
|
|
QUrl url=item->data(0, UrlRole).toUrl();
|
|
QUrl imageUrl=item->data(0, ImageUrlRole).toUrl();
|
|
QString descr=item->data(0, DescriptionRole).toString();
|
|
QString web=item->data(0, WebPageUrlRole).toString();
|
|
QString str="<b>"+item->text(0)+"</b><br/>";
|
|
if (!imageUrl.isEmpty()) {
|
|
QImage *img=imageCache.object(imageUrl);
|
|
if (img) {
|
|
str+=encode(*img);
|
|
} else {
|
|
fetchImage(imageUrl);
|
|
}
|
|
}
|
|
if (!descr.isEmpty()) {
|
|
str+="<p>"+descr+"</p><br/>";
|
|
}
|
|
str+="<table><tr><td><b>"+tr("RSS:")+"</b></td><td><a href=\""+url.toString()+"\">"+url.toString()+"</a></td></tr>";
|
|
if (!web.isEmpty()) {
|
|
str+="<tr><td><b>"+tr("Website:")+"</b></td><td><a href=\""+web+"\">"+web+"</a></td></tr>";
|
|
}
|
|
str+="</table>";
|
|
text->setHtml(str);
|
|
return;
|
|
}
|
|
}
|
|
text->setHtml("<b>"+tr("Podcast details")+"</b><p><i>"+tr("Select a podcast to display its details")+"</i></p>");
|
|
}
|
|
|
|
void PodcastPage::selectionChanged()
|
|
{
|
|
cancelImage();
|
|
updateText();
|
|
QList<QTreeWidgetItem *> selection=tree->selectedItems();
|
|
emit rssSelected(selection.isEmpty() ? QUrl() : selection.at(0)->data(0, UrlRole).toUrl());
|
|
}
|
|
|
|
void PodcastPage::imageJobFinished()
|
|
{
|
|
NetworkJob *j=qobject_cast<NetworkJob *>(sender());
|
|
if (!j) {
|
|
return;
|
|
}
|
|
j->deleteLater();
|
|
if (j!=imageJob) {
|
|
return;
|
|
}
|
|
if (imageSpinner) {
|
|
imageSpinner->stop();
|
|
}
|
|
QByteArray data=imageJob->readAll();
|
|
QImage img=QImage::fromData(data, Covers::imageFormat(data));
|
|
if (!img.isNull()) {
|
|
if (img.width()>maxImageSize || img.height()>maxImageSize) {
|
|
img=img.scaled(maxImageSize, maxImageSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
}
|
|
imageCache.insert(imageJob->property(constOrigUrlProperty).toUrl(), new QImage(img), img.sizeInBytes());
|
|
updateText();
|
|
}
|
|
imageJob=nullptr;
|
|
}
|
|
|
|
void PodcastPage::jobFinished()
|
|
{
|
|
NetworkJob *j=qobject_cast<NetworkJob *>(sender());
|
|
if (!j) {
|
|
return;
|
|
}
|
|
j->deleteLater();
|
|
if (j!=job) {
|
|
return;
|
|
}
|
|
if (spinner) {
|
|
spinner->stop();
|
|
}
|
|
parseResonse(j->ok() ? j->actualJob() : nullptr);
|
|
job=nullptr;
|
|
}
|
|
|
|
void PodcastPage::openLink(const QUrl &url)
|
|
{
|
|
QDesktopServices::openUrl(url);
|
|
}
|
|
|
|
PodcastSearchPage::PodcastSearchPage(QWidget *p, const QString &n, int i, const QUrl &qu, const QString &qk, const QStringList &other)
|
|
: PodcastPage(p, n)
|
|
, queryUrl(qu)
|
|
, queryKey(qk)
|
|
, otherArgs(other)
|
|
{
|
|
QBoxLayout *searchLayout=new QBoxLayout(QBoxLayout::LeftToRight);
|
|
QBoxLayout *viewLayout=new QBoxLayout(QBoxLayout::LeftToRight);
|
|
QBoxLayout *mainLayout=new QBoxLayout(QBoxLayout::TopToBottom, this);
|
|
searchLayout->setMargin(0);
|
|
viewLayout->setMargin(0);
|
|
mainLayout->setMargin(0);
|
|
search=new LineEdit(p);
|
|
search->setPlaceholderText(tr("Enter search term..."));
|
|
searchButton=new QPushButton(tr("Search"), p);
|
|
QWidget::setTabOrder(search, searchButton);
|
|
QWidget::setTabOrder(searchButton, tree);
|
|
searchLayout->addWidget(search);
|
|
searchLayout->addWidget(searchButton);
|
|
viewLayout->addWidget(tree, 1);
|
|
viewLayout->addWidget(text, 0);
|
|
mainLayout->addLayout(searchLayout);
|
|
mainLayout->addLayout(viewLayout);
|
|
connect(search, SIGNAL(returnPressed()), SLOT(doSearch()));
|
|
connect(searchButton, SIGNAL(clicked()), SLOT(doSearch()));
|
|
icn=MonoIcon::icon((FontAwesome::icon)i, Utils::monoIconColor());
|
|
}
|
|
|
|
void PodcastSearchPage::showEvent(QShowEvent *e)
|
|
{
|
|
search->setFocus();
|
|
QWidget::showEvent(e);
|
|
}
|
|
|
|
void PodcastSearchPage::doSearch()
|
|
{
|
|
QString text=search->text().trimmed();
|
|
if (text.isEmpty() || text==currentSearch) {
|
|
return;
|
|
}
|
|
|
|
currentSearch=text;
|
|
QUrl url=queryUrl;
|
|
QUrlQuery query;
|
|
query.addQueryItem(queryKey, text);
|
|
if (otherArgs.size()>1) {
|
|
for (int i=0; i<otherArgs.size()-1; i+=2) {
|
|
query.addQueryItem(otherArgs.at(i), otherArgs.at(i+1));
|
|
}
|
|
}
|
|
url.setQuery(query);
|
|
fetch(url);
|
|
}
|
|
|
|
void PodcastSearchPage::parseResonse(QIODevice *dev)
|
|
{
|
|
if (!dev) {
|
|
emit error(tr("Failed to fetch podcasts from %1").arg(name()));
|
|
return;
|
|
}
|
|
QVariant data=QJsonDocument::fromJson(dev->readAll()).toVariant();
|
|
if (data.isNull()) {
|
|
emit error(tr("There was a problem parsing the response from %1").arg(name()));
|
|
return;
|
|
}
|
|
parse(data);
|
|
}
|
|
|
|
OpmlBrowsePage::OpmlBrowsePage(QWidget *p, const QString &n, const QIcon &i, const QUrl &u)
|
|
: PodcastPage(p, n)
|
|
, loaded(false)
|
|
, url(u)
|
|
{
|
|
QBoxLayout *mainLayout=new QBoxLayout(QBoxLayout::LeftToRight, this);
|
|
mainLayout->setMargin(0);
|
|
mainLayout->addWidget(tree, 1);
|
|
mainLayout->addWidget(text, 0);
|
|
Action *act=new Action(tr("Reload"), this);
|
|
tree->addAction(act);
|
|
connect(act, SIGNAL(triggered()), this, SLOT(reload()));
|
|
tree->setContextMenuPolicy(Qt::ActionsContextMenu);
|
|
icn=i;
|
|
}
|
|
|
|
void OpmlBrowsePage::showEvent(QShowEvent *e)
|
|
{
|
|
if (!loaded) {
|
|
QString cacheFile=generateCacheFileName(url, false);
|
|
if (!cacheFile.isEmpty() && QFile::exists(cacheFile)) {
|
|
QFile f(cacheFile);
|
|
if (f.open(QIODevice::ReadOnly)) {
|
|
parseResonse(&f);
|
|
if (tree->topLevelItemCount()>0) {
|
|
Utils::touchFile(cacheFile);
|
|
loaded=true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
fetch(url);
|
|
loaded=true;
|
|
}
|
|
QWidget::showEvent(e);
|
|
}
|
|
|
|
void OpmlBrowsePage::reload()
|
|
{
|
|
QString cacheFile=generateCacheFileName(url, false);
|
|
if (!cacheFile.isEmpty() && QFile::exists(cacheFile)) {
|
|
QFile::remove(cacheFile);
|
|
}
|
|
fetch(url);
|
|
}
|
|
|
|
void OpmlBrowsePage::parseResonse(QIODevice *dev)
|
|
{
|
|
bool isLoadingFromCache=dynamic_cast<QFile *>(dev) ? true : false;
|
|
|
|
if (!dev) {
|
|
if (!isLoadingFromCache) {
|
|
emit error(tr("Failed to download directory listing"));
|
|
}
|
|
return;
|
|
}
|
|
QByteArray data=dev->readAll();
|
|
OpmlParser::Category parsed=OpmlParser::parse(data);
|
|
if (parsed.categories.isEmpty() && parsed.podcasts.isEmpty()) {
|
|
if (!isLoadingFromCache) {
|
|
emit error(tr("Failed to parse directory listing"));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (1==parsed.categories.count() && parsed.podcasts.isEmpty()) {
|
|
parsed=parsed.categories.at(0);
|
|
}
|
|
for (const OpmlParser::Category &cat: parsed.categories) {
|
|
addCategory(cat, nullptr);
|
|
}
|
|
for (const OpmlParser::Podcast &pod: parsed.podcasts) {
|
|
addPodcast(pod, nullptr);
|
|
}
|
|
if (!isLoadingFromCache && tree->topLevelItemCount()>0) {
|
|
QString cacheFile=generateCacheFileName(url, true);
|
|
if (!cacheFile.isEmpty()) {
|
|
QFile f(cacheFile);
|
|
if (f.open(QIODevice::WriteOnly)) {
|
|
f.write(data);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
PodcastUrlPage::PodcastUrlPage(QWidget *p)
|
|
: PodcastPage(p, tr("URL"))
|
|
{
|
|
QBoxLayout *searchLayout=new QBoxLayout(QBoxLayout::LeftToRight);
|
|
QBoxLayout *viewLayout=new QBoxLayout(QBoxLayout::LeftToRight);
|
|
QBoxLayout *mainLayout=new QBoxLayout(QBoxLayout::TopToBottom, this);
|
|
searchLayout->setMargin(0);
|
|
viewLayout->setMargin(0);
|
|
mainLayout->setMargin(0);
|
|
urlEntry=new LineEdit(p);
|
|
urlEntry->setPlaceholderText(tr("Enter podcast URL..."));
|
|
QPushButton *loadButton=new QPushButton(tr("Load"), p);
|
|
FlatToolButton *pathReq=new FlatToolButton(p);
|
|
pathReq->setIcon(MonoIcon::icon(FontAwesome::foldero, Utils::monoIconColor()));
|
|
pathReq->setToolTip(tr("Load local podcast file"));
|
|
QWidget::setTabOrder(urlEntry, loadButton);
|
|
QWidget::setTabOrder(loadButton, tree);
|
|
searchLayout->addWidget(urlEntry);
|
|
searchLayout->addWidget(loadButton);
|
|
searchLayout->addWidget(pathReq);
|
|
viewLayout->addWidget(tree, 1);
|
|
viewLayout->addWidget(text, 0);
|
|
mainLayout->addWidget(new QLabel(tr("Enter podcast URL below, and press 'Load', or press the folder icon to load a local podcast file."), this));
|
|
mainLayout->addLayout(searchLayout);
|
|
mainLayout->addLayout(viewLayout);
|
|
connect(urlEntry, SIGNAL(returnPressed()), SLOT(loadUrl()));
|
|
connect(loadButton, SIGNAL(clicked()), SLOT(loadUrl()));
|
|
connect(pathReq, SIGNAL(clicked()), SLOT(openPath()));
|
|
icn=Icons::self()->rssListIcon;
|
|
}
|
|
|
|
void PodcastUrlPage::showEvent(QShowEvent *e)
|
|
{
|
|
urlEntry->setFocus();
|
|
QWidget::showEvent(e);
|
|
}
|
|
|
|
void PodcastUrlPage::loadUrl()
|
|
{
|
|
QString text=urlEntry->text().trimmed();
|
|
if (text.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
QUrl url(PodcastService::fixUrl(text));
|
|
if (url==currentUrl) {
|
|
return;
|
|
}
|
|
|
|
if (!PodcastService::isUrlOk(url)) {
|
|
emit error(tr("Invalid URL!"));
|
|
} else {
|
|
currentUrl=url;
|
|
fetch(url);
|
|
}
|
|
}
|
|
|
|
void PodcastUrlPage::openPath()
|
|
{
|
|
QString path = QFileDialog::getOpenFileName(this, QString(), QDir::homePath(), tr("Podcasts (*.xml, *.rss, *.opml"));
|
|
|
|
if (!path.isEmpty()) {
|
|
QFile f(path);
|
|
if (f.open(QIODevice::ReadOnly|QIODevice::Text)) {
|
|
parse(&f);
|
|
} else {
|
|
emit error(tr("Failed to read file!"));
|
|
}
|
|
}
|
|
}
|
|
void PodcastUrlPage::parseResonse(QIODevice *dev)
|
|
{
|
|
if (!dev) {
|
|
emit error(tr("Failed to fetch podcast!"));
|
|
return;
|
|
}
|
|
parse(dev);
|
|
}
|
|
|
|
void PodcastUrlPage::parse(QIODevice *dev)
|
|
{
|
|
auto data = dev->readAll();
|
|
QBuffer buf(&data);
|
|
buf.open(QIODevice::ReadOnly|QIODevice::Text);
|
|
RssParser::Channel ch=RssParser::parse(&buf, false, true);
|
|
if (ch.isValid()) {
|
|
if (ch.video) {
|
|
emit error(tr("Cantata only supports audio podcasts! The URL entered contains only video podcasts."));
|
|
} else {
|
|
addPodcast(ch.name, currentUrl, ch.image, ch.description, QString(), nullptr);
|
|
}
|
|
} else {
|
|
OpmlParser::Category parsed=OpmlParser::parse(data);
|
|
if (parsed.categories.isEmpty() && parsed.podcasts.isEmpty()) {
|
|
emit error(tr("Failed to parse podcast."));
|
|
} else {
|
|
if (1==parsed.categories.count() && parsed.podcasts.isEmpty()) {
|
|
parsed=parsed.categories.at(0);
|
|
}
|
|
for (const OpmlParser::Category &cat: parsed.categories) {
|
|
addCategory(cat, nullptr);
|
|
}
|
|
for (const OpmlParser::Podcast &pod: parsed.podcasts) {
|
|
addPodcast(pod, nullptr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int PodcastSearchDialog::instanceCount()
|
|
{
|
|
return iCount;
|
|
}
|
|
|
|
PodcastSearchDialog::PodcastSearchDialog(PodcastService *s, QWidget *parent)
|
|
: Dialog(parent, "PodcastSearchDialog", QSize(800, 600))
|
|
, service(s)
|
|
{
|
|
Utils::clearOldCache(constCacheDir, 2);
|
|
|
|
iCount++;
|
|
setButtons(User1|Close);
|
|
setButtonText(User1, tr("Subscribe"));
|
|
|
|
QWidget *mainWidget = new QWidget(this);
|
|
messageWidget = new MessageWidget(mainWidget);
|
|
spacer = new QWidget(this);
|
|
spacer->setFixedSize(Utils::layoutSpacing(this), 0);
|
|
QBoxLayout *layout = new QBoxLayout(QBoxLayout::TopToBottom, mainWidget);
|
|
layout->setMargin(0);
|
|
layout->setSpacing(0);
|
|
|
|
pageWidget = new PageWidget(mainWidget);
|
|
QList<PodcastPage *> pages;
|
|
|
|
layout->addWidget(messageWidget);
|
|
layout->addWidget(spacer);
|
|
layout->addWidget(pageWidget);
|
|
|
|
PodcastUrlPage *urlPage=new PodcastUrlPage(pageWidget);
|
|
pageWidget->addPage(urlPage, tr("Enter URL"), urlPage->icon(), tr("Manual podcast URL"));
|
|
pages << urlPage;
|
|
|
|
ITunesSearchPage *itunes=new ITunesSearchPage(pageWidget);
|
|
pageWidget->addPage(itunes, tr("Search %1").arg(itunes->name()), itunes->icon(), tr("Search for podcasts on %1").arg(itunes->name()));
|
|
pages << itunes;
|
|
|
|
GPodderSearchPage *gpodder=new GPodderSearchPage(pageWidget);
|
|
pageWidget->addPage(gpodder, tr("Search %1").arg(gpodder->name()), gpodder->icon(), tr("Search for podcasts on %1").arg(gpodder->name()));
|
|
pages << gpodder;
|
|
|
|
QSet<QString> loaded;
|
|
pages << loadDirectories(Utils::dataDir(), false, loaded);
|
|
pages << loadDirectories(QString(), true, loaded);
|
|
|
|
for (PodcastPage *p: pages) {
|
|
connect(p, SIGNAL(rssSelected(QUrl)), SLOT(rssSelected(QUrl)));
|
|
connect(p, SIGNAL(error(QString)), SLOT(showError(QString)));
|
|
}
|
|
|
|
setCaption(tr("Add Podcast Subscription"));
|
|
setMainWidget(mainWidget);
|
|
setAttribute(Qt::WA_DeleteOnClose);
|
|
enableButton(User1, false);
|
|
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
|
|
|
|
if (-1==maxImageSize) {
|
|
maxImageSize=fontMetrics().height()*8;
|
|
}
|
|
connect(service, SIGNAL(newError(QString)), this, SLOT(showError(QString)));
|
|
connect(messageWidget, SIGNAL(visible(bool)), SLOT(msgWidgetVisible(bool)));
|
|
connect(pageWidget, SIGNAL(currentPageChanged()), this, SLOT(pageChanged()));
|
|
messageWidget->hide();
|
|
}
|
|
|
|
PodcastSearchDialog::~PodcastSearchDialog()
|
|
{
|
|
iCount--;
|
|
imageCache.clear();
|
|
}
|
|
|
|
void PodcastSearchDialog::rssSelected(const QUrl &url)
|
|
{
|
|
currentUrl=url;
|
|
enableButton(User1, currentUrl.isValid());
|
|
}
|
|
|
|
void PodcastSearchDialog::showError(const QString &msg)
|
|
{
|
|
messageWidget->setError(msg);
|
|
}
|
|
|
|
void PodcastSearchDialog::showInfo(const QString &msg)
|
|
{
|
|
messageWidget->setInformation(msg);
|
|
}
|
|
|
|
void PodcastSearchDialog::msgWidgetVisible(bool v)
|
|
{
|
|
spacer->setFixedSize(spacer->width(), v ? spacer->width() : 0);
|
|
}
|
|
|
|
void PodcastSearchDialog::pageChanged()
|
|
{
|
|
PageWidgetItem *pwi=pageWidget->currentPage();
|
|
PodcastPage *page=pwi ? qobject_cast<PodcastPage *>(pwi->widget()) : nullptr;
|
|
rssSelected(page ? page->currentRss() : QUrl());
|
|
}
|
|
|
|
QList<PodcastPage *> PodcastSearchDialog::loadDirectories(const QString &dir, bool isSystem, QSet<QString> &loaded)
|
|
{
|
|
QList<PodcastPage *> pages;
|
|
|
|
if (dir.isEmpty() && !isSystem) {
|
|
return pages;
|
|
}
|
|
|
|
QFile file(isSystem ? QLatin1String(":podcast_directories.xml") : (dir+QLatin1String("/podcast_directories.xml")));
|
|
|
|
if (file.open(QIODevice::ReadOnly)) {
|
|
QXmlStreamReader reader(&file);
|
|
while (!reader.atEnd()) {
|
|
reader.readNext();
|
|
if (reader.isStartElement() && QLatin1String("directory")==reader.name()) {
|
|
QString url=reader.attributes().value(QLatin1String("url")).toString();
|
|
if (!loaded.contains(url)) {
|
|
QString iconName=reader.attributes().value(QLatin1String("icon")).toString();
|
|
QIcon icon;
|
|
|
|
if (iconName.isEmpty()) {
|
|
icon=MonoIcon::icon(FontAwesome::rsssquare, Utils::monoIconColor());
|
|
} else if (iconName.startsWith(":")) {
|
|
icon= MonoIcon::icon(iconName, Utils::monoIconColor());
|
|
} else {
|
|
icon.addFile(iconName);
|
|
}
|
|
|
|
if (icon.isNull()) {
|
|
icon=MonoIcon::icon(FontAwesome::rsssquare, Utils::monoIconColor());
|
|
}
|
|
|
|
OpmlBrowsePage *page=new OpmlBrowsePage(pageWidget,
|
|
reader.attributes().value(QLatin1String("name")).toString(),
|
|
icon,
|
|
QUrl(url));
|
|
pageWidget->addPage(page, tr("Browse %1").arg(page->name()), page->icon(), tr("Browse %1 podcasts").arg(page->name()));
|
|
pages << page;
|
|
loaded.insert(url);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return pages;
|
|
}
|
|
|
|
void PodcastSearchDialog::slotButtonClicked(int button)
|
|
{
|
|
switch (button) {
|
|
case User1: {
|
|
QUrl fixed=PodcastService::fixUrl(currentUrl);
|
|
if (service->subscribedToUrl(fixed)) {
|
|
showError(tr("You are already subscribed to this podcast!"));
|
|
} else {
|
|
service->addUrl(fixed);
|
|
showInfo(tr("Subscription added"));
|
|
}
|
|
break;
|
|
}
|
|
case Close:
|
|
reject();
|
|
// Need to call this - if not, when dialog is closed by window X control, it is not deleted!!!!
|
|
Dialog::slotButtonClicked(button);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
#include "moc_podcastsearchdialog.cpp"
|