- Add the ability to parse the RSS feeds from reddit groups (in particular the image feed groups like EarthPorn) and display them

This commit is contained in:
Alfred Reynolds
2021-08-22 15:10:26 +12:00
parent 3557b6041f
commit 833e7ef915
9 changed files with 271 additions and 15 deletions

View File

@@ -180,6 +180,10 @@ QVector<PathEntry> parsePathEntry(QJsonObject &jsonMainDoc, bool baseRecursive,
if(!imageListString.empty()) { if(!imageListString.empty()) {
entry.imageList = imageListString; entry.imageList = imageListString;
} }
std::string rssFeedURLString = ParseJSONString(schedulerJson, "redditrss");
if(!rssFeedURLString.empty()) {
entry.rssFeedURL = rssFeedURLString;
}
SetJSONBool(entry.exclusive, schedulerJson, "exclusive"); SetJSONBool(entry.exclusive, schedulerJson, "exclusive");
@@ -229,7 +233,7 @@ AppConfig loadAppConfiguration(const AppConfig &commandLineConfig) {
QJsonDocument d = QJsonDocument::fromJson(val.toUtf8()); QJsonDocument d = QJsonDocument::fromJson(val.toUtf8());
QJsonObject jsonDoc = d.object(); QJsonObject jsonDoc = d.object();
bool baseRecursive, baseShuffle, baseSorted; bool baseRecursive = false, baseShuffle = false, baseSorted = false;
SetJSONBool(baseRecursive, jsonDoc, "recursive"); SetJSONBool(baseRecursive, jsonDoc, "recursive");
SetJSONBool(baseShuffle, jsonDoc, "shuffle"); SetJSONBool(baseShuffle, jsonDoc, "shuffle");
SetJSONBool(baseSorted, jsonDoc, "sorted"); SetJSONBool(baseSorted, jsonDoc, "sorted");
@@ -258,6 +262,11 @@ AppConfig loadAppConfiguration(const AppConfig &commandLineConfig) {
{ {
entry.imageList = imageListString; entry.imageList = imageListString;
} }
std::string rssFeedURLString = ParseJSONString(jsonDoc, "redditrss");
if(!rssFeedURLString.empty())
{
entry.rssFeedURL = rssFeedURLString;
}
loadedConfig.paths.append(entry); loadedConfig.paths.append(entry);
} }
loadedConfig.configPath = commandLineConfig.configPath; loadedConfig.configPath = commandLineConfig.configPath;

View File

@@ -18,6 +18,7 @@ struct Config {
struct PathEntry { struct PathEntry {
std::string path = ""; std::string path = "";
std::string imageList = ""; std::string imageList = "";
std::string rssFeedURL = "";
bool exclusive = false; // only use this entry when it is valid, skip others bool exclusive = false; // only use this entry when it is valid, skip others
bool recursive = false; bool recursive = false;
@@ -37,7 +38,7 @@ struct PathEntry {
return true; return true;
if(b.baseDisplayOptions.fitAspectAxisToWindow != baseDisplayOptions.fitAspectAxisToWindow) if(b.baseDisplayOptions.fitAspectAxisToWindow != baseDisplayOptions.fitAspectAxisToWindow)
return true; return true;
if (b.path != path || b.imageList != imageList) if (b.path != path || b.imageList != imageList || b.rssFeedURL != rssFeedURL)
return true; return true;
if (b.baseDisplayOptions.timeWindows.count() != baseDisplayOptions.timeWindows.count()) if (b.baseDisplayOptions.timeWindows.count() != baseDisplayOptions.timeWindows.count())
return true; return true;

View File

@@ -146,6 +146,9 @@ bool ImageSelector::imageInsideTimeWindow(const QVector<DisplayTimeWindow> &time
bool ImageSelector::imageMatchesFilter(const ImageDetails& imageDetails) bool ImageSelector::imageMatchesFilter(const ImageDetails& imageDetails)
{ {
if(imageDetails.filename.find("https://") != std::string::npos)
return imageInsideTimeWindow(imageDetails.options.timeWindows);
if(!QFileInfo::exists(QString(imageDetails.filename.c_str()))) if(!QFileInfo::exists(QString(imageDetails.filename.c_str())))
{ {
if(debugMode) if(debugMode)
@@ -245,9 +248,16 @@ const ImageDetails ShuffleImageSelector::getNextImage(const ImageDisplayOptions
{ {
return imageDetails; return imageDetails;
} }
bool bReloadedImages = false;
imageDetails = populateImageDetails(pathTraverser->getImagePath(images.at(current_image_shuffle).toStdString()), baseOptions); imageDetails = populateImageDetails(pathTraverser->getImagePath(images.at(current_image_shuffle).toStdString()), baseOptions);
current_image_shuffle = current_image_shuffle + 1; // ignore and move to next image current_image_shuffle = current_image_shuffle + 1; // ignore and move to next image
while(!imageMatchesFilter(imageDetails)) { while(!imageMatchesFilter(imageDetails)) {
if(current_image_shuffle >= images.size()) {
// don't keep looping
if(bReloadedImages == true)
return ImageDetails();
bReloadedImages = true;
}
reloadImagesIfNoneLeft(); reloadImagesIfNoneLeft();
imageDetails = populateImageDetails(pathTraverser->getImagePath(images.at(current_image_shuffle).toStdString()),baseOptions); imageDetails = populateImageDetails(pathTraverser->getImagePath(images.at(current_image_shuffle).toStdString()),baseOptions);
current_image_shuffle = current_image_shuffle + 1; // ignore and move to next image current_image_shuffle = current_image_shuffle + 1; // ignore and move to next image
@@ -300,8 +310,16 @@ const ImageDetails SortedImageSelector::getNextImage(const ImageDisplayOptions &
{ {
return imageDetails; return imageDetails;
} }
bool bReloadedImages = false;
imageDetails = populateImageDetails(pathTraverser->getImagePath(images.takeFirst().toStdString()), baseOptions); imageDetails = populateImageDetails(pathTraverser->getImagePath(images.takeFirst().toStdString()), baseOptions);
while(!imageMatchesFilter(imageDetails)) { while(!imageMatchesFilter(imageDetails)) {
if (images.size() == 0) {
// don't keep looping
if(bReloadedImages == true)
return ImageDetails();
bReloadedImages = true;
}
reloadImagesIfEmpty(); reloadImagesIfEmpty();
imageDetails = populateImageDetails(pathTraverser->getImagePath(images.takeFirst().toStdString()), baseOptions); imageDetails = populateImageDetails(pathTraverser->getImagePath(images.takeFirst().toStdString()), baseOptions);
} }

View File

@@ -6,6 +6,7 @@
#include "appconfig.h" #include "appconfig.h"
#include <QApplication> #include <QApplication>
#include <QNetworkAccessManager>
#include <iostream> #include <iostream>
#include <sys/file.h> #include <sys/file.h>
#include <errno.h> #include <errno.h>
@@ -128,10 +129,14 @@ void ConfigureWindowFromSettings(MainWindow &w, const AppConfig &appConfig)
w.setBaseOptions(appConfig.baseDisplayOptions); w.setBaseOptions(appConfig.baseDisplayOptions);
} }
std::unique_ptr<ImageSelector> GetSelectorForConfig(const PathEntry& path, const bool debugMode) std::unique_ptr<ImageSelector> GetSelectorForConfig(const PathEntry& path, QNetworkAccessManager& networkManagerIn, const bool debugMode)
{ {
std::unique_ptr<PathTraverser> pathTraverser; std::unique_ptr<PathTraverser> pathTraverser;
if (!path.imageList.empty()) if (!path.rssFeedURL.empty())
{
pathTraverser = std::unique_ptr<PathTraverser>(new RedditRSSFeedPathTraverser(path.rssFeedURL, networkManagerIn, debugMode));
}
else if (!path.imageList.empty())
{ {
pathTraverser = std::unique_ptr<PathTraverser>(new ImageListPathTraverser(path.imageList, debugMode)); pathTraverser = std::unique_ptr<PathTraverser>(new ImageListPathTraverser(path.imageList, debugMode));
} }
@@ -161,18 +166,18 @@ std::unique_ptr<ImageSelector> GetSelectorForConfig(const PathEntry& path, const
return selector; return selector;
} }
std::unique_ptr<ImageSelector> GetSelectorForApp(const AppConfig& appConfig) std::unique_ptr<ImageSelector> GetSelectorForApp(const AppConfig& appConfig, QNetworkAccessManager& networkManagerIn)
{ {
if(appConfig.paths.count()==1) if(appConfig.paths.count()==1)
{ {
return GetSelectorForConfig(appConfig.paths[0], appConfig.debugMode); return GetSelectorForConfig(appConfig.paths[0], networkManagerIn, appConfig.debugMode);
} }
else else
{ {
std::unique_ptr<ListImageSelector> listSelector(new ListImageSelector()); std::unique_ptr<ListImageSelector> listSelector(new ListImageSelector());
for(const auto &path : appConfig.paths) for(const auto &path : appConfig.paths)
{ {
auto selector = GetSelectorForConfig(path, appConfig.debugMode); auto selector = GetSelectorForConfig(path, networkManagerIn, appConfig.debugMode);
listSelector->AddImageSelector(selector, path.exclusive, path.baseDisplayOptions); listSelector->AddImageSelector(selector, path.exclusive, path.baseDisplayOptions);
} }
// new things // new things
@@ -181,7 +186,7 @@ std::unique_ptr<ImageSelector> GetSelectorForApp(const AppConfig& appConfig)
} }
void ReloadConfigIfNeeded(AppConfig &appConfig, MainWindow &w, ImageSwitcher *switcher, ImageSelector *selector) void ReloadConfigIfNeeded(AppConfig &appConfig, MainWindow &w, ImageSwitcher *switcher, ImageSelector *selector, QNetworkAccessManager& networkManager)
{ {
QString jsonFile = getAppConfigFilePath(appConfig.configPath); QString jsonFile = getAppConfigFilePath(appConfig.configPath);
QDir directory; QDir directory;
@@ -198,7 +203,7 @@ void ReloadConfigIfNeeded(AppConfig &appConfig, MainWindow &w, ImageSwitcher *sw
ConfigureWindowFromSettings(w, appConfig); ConfigureWindowFromSettings(w, appConfig);
if(appConfig.PathOptionsChanged(oldConfig)) if(appConfig.PathOptionsChanged(oldConfig))
{ {
std::unique_ptr<ImageSelector> selector = GetSelectorForApp(appConfig); std::unique_ptr<ImageSelector> selector = GetSelectorForApp(appConfig, networkManager);
switcher->setImageSelector(selector); switcher->setImageSelector(selector);
} }
@@ -233,16 +238,19 @@ int main(int argc, char *argv[])
std::cout << "Overlay input: " << appConfig.overlay << std::endl; std::cout << "Overlay input: " << appConfig.overlay << std::endl;
} }
QNetworkAccessManager webCtrl;
MainWindow w; MainWindow w;
ConfigureWindowFromSettings(w, appConfig); ConfigureWindowFromSettings(w, appConfig);
w.setNetworkManager(&webCtrl);
w.show(); w.show();
std::unique_ptr<ImageSelector> selector = GetSelectorForApp(appConfig); std::unique_ptr<ImageSelector> selector = GetSelectorForApp(appConfig, webCtrl);
selector->setDebugMode(appConfig.debugMode); selector->setDebugMode(appConfig.debugMode);
ImageSwitcher switcher(w, appConfig.rotationSeconds * 1000, selector); ImageSwitcher switcher(w, appConfig.rotationSeconds * 1000, selector);
w.setImageSwitcher(&switcher); w.setImageSwitcher(&switcher);
std::function<void(MainWindow &w, ImageSwitcher *switcher, ImageSelector *selector)> reloader = [&appConfig](MainWindow &w, ImageSwitcher *switcher, ImageSelector *selector) { ReloadConfigIfNeeded(appConfig, w, switcher, selector); }; std::function<void(MainWindow &w, ImageSwitcher *switcher, ImageSelector *selector)> reloader = [&appConfig, &webCtrl](MainWindow &w, ImageSwitcher *switcher, ImageSelector *selector) { ReloadConfigIfNeeded(appConfig, w, switcher, selector, webCtrl); };
switcher.setConfigFileReloader(reloader); switcher.setConfigFileReloader(reloader);
switcher.start(); switcher.start();
return a.exec(); return a.exec();

View File

@@ -17,6 +17,7 @@
#include <QGraphicsPixmapItem> #include <QGraphicsPixmapItem>
#include <QApplication> #include <QApplication>
#include <QScreen> #include <QScreen>
#include <QNetworkReply>
#include <sstream> #include <sstream>
MainWindow::MainWindow(QWidget *parent) : MainWindow::MainWindow(QWidget *parent) :
@@ -165,15 +166,46 @@ void MainWindow::checkWindowSize()
void MainWindow::setImage(const ImageDetails &imageDetails) void MainWindow::setImage(const ImageDetails &imageDetails)
{ {
currentImage = imageDetails; currentImage = imageDetails;
downloadedData.clear();
if (pendingReply)
{
pendingReply->abort();
}
updateImage(false); updateImage(false);
} }
void MainWindow::fileDownloaded(QNetworkReply* netReply)
{
if (netReply == pendingReply)
{
pendingReply = nullptr;
QNetworkReply::NetworkError err = netReply->error();
if (err == QNetworkReply::NoError)
{
downloadedData = netReply->readAll();
netReply->deleteLater();
updateImage(false);
}
}
}
void MainWindow::updateImage(bool immediately) void MainWindow::updateImage(bool immediately)
{ {
checkWindowSize(); checkWindowSize();
if (currentImage.filename == "") if (currentImage.filename == "")
return; return;
if (currentImage.filename.find("https://") != std::string::npos && downloadedData.isNull())
{
if (pendingReply == nullptr)
{
QNetworkRequest request(QUrl(currentImage.filename.c_str()));
pendingReply = networkManager->get(request);
connect( networkManager, SIGNAL (finished(QNetworkReply*)), this, SLOT (fileDownloaded(QNetworkReply*)));
}
return;
}
QLabel *label = this->findChild<QLabel*>("image"); QLabel *label = this->findChild<QLabel*>("image");
const QPixmap* oldImage = label->pixmap(); const QPixmap* oldImage = label->pixmap();
if (oldImage != NULL && !immediately) if (oldImage != NULL && !immediately)
@@ -183,7 +215,28 @@ void MainWindow::updateImage(bool immediately)
this->setPalette(palette); this->setPalette(palette);
} }
QPixmap p( currentImage.filename.c_str() ); QPixmap p;
if (!downloadedData.isNull())
{
p.loadFromData(downloadedData);
// BUG BUG have the selector update this?
currentImage.width = p.width();
currentImage.height = p.height();
currentImage.rotation = 0;
if (currentImage.width > currentImage.height) {
currentImage.aspect = ImageAspect_Landscape;
} else if (currentImage.height > currentImage.width) {
currentImage.aspect = ImageAspect_Portrait;
} else {
currentImage.aspect = ImageAspect_Any;
}
}
else
{
p.load( currentImage.filename.c_str() );
}
if(debugMode) if(debugMode)
{ {
std::cout << "size:" << p.width() << "x" << p.height() << "(window:" << width() << "," << height() << ")" << std::endl; std::cout << "size:" << p.width() << "x" << p.height() << "(window:" << width() << "," << height() << ")" << std::endl;
@@ -382,3 +435,8 @@ const ImageDisplayOptions &MainWindow::getBaseOptions()
{ {
return baseImageOptions; return baseImageOptions;
} }
void MainWindow::setNetworkManager(QNetworkAccessManager *networkManagerIn)
{
networkManager = networkManagerIn;
}

View File

@@ -3,6 +3,7 @@
#include <QMainWindow> #include <QMainWindow>
#include <QPixmap> #include <QPixmap>
#include <QNetworkAccessManager>
#include "imagestructs.h" #include "imagestructs.h"
#include "imageselector.h" #include "imageselector.h"
@@ -33,8 +34,11 @@ public:
void setBaseOptions(const ImageDisplayOptions &baseOptionsIn); void setBaseOptions(const ImageDisplayOptions &baseOptionsIn);
const ImageDisplayOptions &getBaseOptions(); const ImageDisplayOptions &getBaseOptions();
void setImageSwitcher(ImageSwitcher *switcherIn); void setImageSwitcher(ImageSwitcher *switcherIn);
void setNetworkManager(QNetworkAccessManager *networkManagerIn);
public slots: public slots:
void checkWindowSize(); void checkWindowSize();
private slots:
void fileDownloaded(QNetworkReply* pReply);
private: private:
Ui::MainWindow *ui; Ui::MainWindow *ui;
@@ -43,6 +47,9 @@ private:
ImageDisplayOptions baseImageOptions; ImageDisplayOptions baseImageOptions;
bool imageAspectMatchesMonitor = false; bool imageAspectMatchesMonitor = false;
ImageDetails currentImage; ImageDetails currentImage;
QByteArray downloadedData;
QNetworkAccessManager *networkManager = nullptr;
QNetworkReply *pendingReply = nullptr;
bool debugMode = false; bool debugMode = false;
QSize lastScreenSize = {0,0}; QSize lastScreenSize = {0,0};

View File

@@ -5,6 +5,8 @@
#include <QDirIterator> #include <QDirIterator>
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QDomDocument>
#include <QDomAttr>
#include <iostream> #include <iostream>
#include <stdlib.h> /* srand, rand */ #include <stdlib.h> /* srand, rand */
@@ -108,5 +110,132 @@ ImageDisplayOptions ImageListPathTraverser::UpdateOptionsForImage(const std::str
// no per file options modification supported // no per file options modification supported
Q_UNUSED(filename); Q_UNUSED(filename);
Q_UNUSED(baseOptions); Q_UNUSED(baseOptions);
return ImageDisplayOptions(); return baseOptions;
}
RedditRSSFeedPathTraverser::RedditRSSFeedPathTraverser(const std::string& rssFeedURLIn, QNetworkAccessManager& networkManager, bool debugModeIn) :
PathTraverser("",debugModeIn), rssFeedURL(rssFeedURLIn), webCtrl(networkManager)
{
connect( &webCtrl, SIGNAL (finished(QNetworkReply*)), this, SLOT (fileDownloaded(QNetworkReply*)));
RequestRSSFeed();
}
RedditRSSFeedPathTraverser::~RedditRSSFeedPathTraverser()
{
}
void RedditRSSFeedPathTraverser::RequestRSSFeed()
{
if (pendingReply)
{
pendingReply->abort();
}
if (debugMode)
{
std::cout << "Requesting RSS feed:" << rssFeedURL << std::endl;
}
rssRequestedTime = QDateTime::currentDateTime();
QNetworkRequest request(QUrl(rssFeedURL.c_str()));
pendingReply = webCtrl.get(request);
}
void RedditRSSFeedPathTraverser::fileDownloaded(QNetworkReply* netReply)
{
if (netReply != pendingReply)
return;
pendingReply = nullptr;
QNetworkReply::NetworkError err = netReply->error();
if (err != QNetworkReply::NoError)
{
std::cout << "Failed to load Reddit RSS URL: " << err << std::endl;
return;
}
QString str (netReply->readAll());
QVariant vt = netReply->attribute(QNetworkRequest::RedirectionTargetAttribute);
netReply->deleteLater();
if (!vt.isNull())
{
if (debugMode)
{
std::cout << "Redirected to:" << vt.toUrl().toString().toStdString() << std::endl;
}
webCtrl.get(QNetworkRequest(vt.toUrl()));
}
else
{
QDomDocument doc;
QString error;
if (!doc.setContent(str, false, &error))
{
if (debugMode)
{
std::cout << "Failed to load page:" << error.toStdString() << std::endl;
}
}
else
{
QDomElement docElem = doc.documentElement();
QDomNodeList nodeList = docElem.elementsByTagName("entry");
for (int iEntry = 0; iEntry < nodeList.length(); ++iEntry)
{
QDomNode node = nodeList.item(iEntry);
QDomElement e = node.toElement();
QDomNode contentNode = e.elementsByTagName("content").item(0).firstChild();
QDomDocument docContent;
if (!docContent.setContent(contentNode.nodeValue(), false, &error))
{
continue;
}
QDomNodeList addressEntries = docContent.documentElement().elementsByTagName("a");
for (int iAddr = 0; iAddr < addressEntries.length(); ++iAddr)
{
QDomNode node = addressEntries.item(iAddr);
/*QString output;
QTextStream stream(&output);
node.save(stream, 0);
qDebug() << "nodeValue: " << output;*/
if (node.toElement().text() == "[link]" && node.hasAttributes() )
{
QDomAttr a = node.toElement().attributeNode("href");
// check if the URL matches one of our supported formats
for ( const QString& format : supportedFormats )
{
if (a.value().endsWith(format))
{
imageURLS.append(a.value());
}
}
}
}
}
}
}
}
QStringList RedditRSSFeedPathTraverser::getImages() const
{
// refresh the feed after 5 hours
if (rssRequestedTime.secsTo(QDateTime::currentDateTime()) > 60*60*5 )
{
const_cast<RedditRSSFeedPathTraverser *>(this)->RequestRSSFeed();
}
return imageURLS;
}
const std::string RedditRSSFeedPathTraverser::getImagePath(const std::string image) const
{
return image;
}
ImageDisplayOptions RedditRSSFeedPathTraverser::UpdateOptionsForImage(const std::string& filename, const ImageDisplayOptions& baseOptions) const
{
// no per file options modification supported
Q_UNUSED(filename);
return baseOptions;
} }

View File

@@ -4,6 +4,9 @@
#include <iostream> #include <iostream>
#include <QDir> #include <QDir>
#include <QStringList> #include <QStringList>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include "imageselector.h" #include "imageselector.h"
static const QStringList supportedFormats={"jpg","jpeg","png","tif","tiff"}; static const QStringList supportedFormats={"jpg","jpeg","png","tif","tiff"};
@@ -58,4 +61,27 @@ class ImageListPathTraverser : public PathTraverser
private: private:
QStringList imageList; QStringList imageList;
}; };
class RedditRSSFeedPathTraverser: public QObject, public PathTraverser
{
Q_OBJECT
public:
RedditRSSFeedPathTraverser(const std::string& rSSFeedURL,QNetworkAccessManager& networkManager, bool debugModeIn);
virtual ~RedditRSSFeedPathTraverser();
virtual QStringList getImages() const;
virtual const std::string getImagePath(const std::string image) const;
virtual ImageDisplayOptions UpdateOptionsForImage(const std::string& filename, const ImageDisplayOptions& baseOptions) const;
private slots:
void fileDownloaded(QNetworkReply* pReply);
private:
void RequestRSSFeed();
std::string rssFeedURL;
QStringList imageURLS;
QNetworkAccessManager& webCtrl;
QNetworkReply *pendingReply = nullptr;
QDateTime rssRequestedTime;
};
#endif // PATHTRAVERSER_H #endif // PATHTRAVERSER_H

View File

@@ -4,8 +4,8 @@
# #
#------------------------------------------------- #-------------------------------------------------
QT += core gui QT += core gui network xml
# CONFIG += qt debug CONFIG += qt debug
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets greaterThan(QT_MAJOR_VERSION, 4): QT += widgets