Compare commits

..

5 Commits

5 changed files with 112 additions and 30 deletions

View File

@ -1,15 +1,20 @@
PREFIX = ~/.local PREFIX = ~/.local
VERSION = 0.2 VERSION = 0.2
# CC = cc PKG_CONFIG = pkg-config
# Comment out if JSON output support isn't needed
JSONLIBS = `$(PKG_CONFIG) --libs json-c`
JSONINCS = `$(PKG_CONFIG) --cflags json-c`
JSONFLAG = -DJSON
CURL_CONFIG = curl-config
SRC = minrss.c util.c net.c handlers.c SRC = minrss.c util.c net.c handlers.c
OBJ = $(SRC:.c=.o) OBJ = $(SRC:.c=.o)
PKG_CONFIG = pkg-config INCS = `$(PKG_CONFIG) --cflags libxml-2.0` `$(CURL_CONFIG) --cflags` $(JSONINC)
CURL_CONFIG = curl-config LIBS = `$(PKG_CONFIG) --libs libxml-2.0` `$(CURL_CONFIG) --libs` $(JSONLIBS)
INCS = `$(PKG_CONFIG) --cflags libxml-2.0` `$(CURL_CONFIG) --cflags`
LIBS = `$(PKG_CONFIG) --libs libxml-2.0` `$(CURL_CONFIG) --libs`
WARN = -Wall -Wpedantic -Wextra WARN = -Wall -Wpedantic -Wextra
CFLAGS = $(INCS) $(LIBS) $(WARN) -DVERSION=\"$(VERSION)\" CFLAGS = $(INCS) $(LIBS) $(WARN) -DVERSION=\"$(VERSION)\" $(JSONFLAG)
all: config.h minrss all: config.h minrss

11
README
View File

@ -1,8 +1,10 @@
MinRSS MinRSS
====== ======
MinRSS is an RSS/Atom feed reader for Linux inspired by suckless.org's MinRSS is an RSS/Atom feed reader for Linux inspired by suckless.org's IRC
IRC clients ii and sic. Instead of presenting articles as entries clients ii and sic. Instead of presenting articles as entries in a menu, it
in a menu, it saves them as files in folders. saves them as files in folders.
These files can either be formatted as HTML, or as JSON to help with scripting.
rss rss
|--news |--news
@ -16,6 +18,9 @@ Requirements
------------ ------------
You need libcurl and libxml2 to compile MinRSS. You need libcurl and libxml2 to compile MinRSS.
json-c is required for JSON output. To disable this feature, comment out the
relevant lines in Makefile.
Installation Installation
------------ ------------
Run this command to build MinRSS: Run this command to build MinRSS:

View File

@ -21,7 +21,6 @@ typedef struct {
.url = "https://example.com/rss/feed.rss", .url = "https://example.com/rss/feed.rss",
// This will be used as the folder name for the feed. // This will be used as the folder name for the feed.
.feedName = "examplefeed", .feedName = "examplefeed",
.tags = "test example sample"
}, },
*/ */
@ -49,3 +48,13 @@ static const int maxRedirs = 10;
// Restrict allowed protocols for curl using a bitmask. // Restrict allowed protocols for curl using a bitmask.
// For more information: https://curl.se/libcurl/c/CURLOPT_PROTOCOLS.html // For more information: https://curl.se/libcurl/c/CURLOPT_PROTOCOLS.html
static const int curlProtocols = CURLPROTO_HTTPS | CURLPROTO_HTTP; static const int curlProtocols = CURLPROTO_HTTPS | CURLPROTO_HTTP;
enum outputFormats {
OUTPUT_HTML,
#ifdef JSON
OUTPUT_JSON,
#endif // JSON
};
// When saving, sets the format of the saved file.
static const enum outputFormats outputFormat = OUTPUT_HTML;

View File

@ -4,6 +4,9 @@
#include <libxml/parser.h> #include <libxml/parser.h>
#include <libxml/tree.h> #include <libxml/tree.h>
#include <libxml/xmlreader.h> #include <libxml/xmlreader.h>
#ifdef JSON
#include <json-c/json.h>
#endif // JSON
#include "config.h" #include "config.h"
#include "util.h" #include "util.h"
@ -53,7 +56,7 @@ atomLink(itemStruct *item, xmlNodePtr node)
return 1; return 1;
} }
if (!rel) { if (!rel || propIs(rel, "alternate")) {
copyField(item, FIELD_LINK, (char *)href); copyField(item, FIELD_LINK, (char *)href);
} else if (propIs(rel, "enclosure")) { } else if (propIs(rel, "enclosure")) {
copyField(item, FIELD_ENCLOSURE_URL, (char *)href); copyField(item, FIELD_ENCLOSURE_URL, (char *)href);
@ -132,11 +135,46 @@ openFile(const char *folder, char *fileName, char *fileExt)
static void static void
outputHtml(itemStruct *item, FILE *f) outputHtml(itemStruct *item, FILE *f)
{ {
fprintf(f, "<h1>%s</h1><br>\n", item->fields[FIELD_TITLE]); if (item->fields[FIELD_TITLE])
fprintf(f, "<a href=\"%s\">Link</a><br>\n", item->fields[FIELD_LINK]); fprintf(f, "<h1>%s</h1><br>\n", item->fields[FIELD_TITLE]);
fprintf(f, "%s", item->fields[FIELD_DESCRIPTION]); if (item->fields[FIELD_LINK])
fprintf(f, "<a href=\"%s\">Link</a><br>\n", item->fields[FIELD_LINK]);
if (item->fields[FIELD_ENCLOSURE_URL])
fprintf(f, "<a href=\"%s\">Enclosure</a><br>\n", item->fields[FIELD_ENCLOSURE_URL]);
if (item->fields[FIELD_DESCRIPTION])
fprintf(f, "%s", item->fields[FIELD_DESCRIPTION]);
} }
#ifdef JSON
static void
outputJson(itemStruct *item, FILE *f)
{
json_object *root = json_object_new_object();
if (item->fields[FIELD_TITLE])
json_object_object_add(root, "title",
json_object_new_string(item->fields[FIELD_TITLE]));
if (item->fields[FIELD_LINK])
json_object_object_add(root, "link",
json_object_new_string(item->fields[FIELD_LINK]));
if (item->fields[FIELD_ENCLOSURE_URL]) {
json_object *enclosure = json_object_new_object();
json_object_object_add(enclosure, "link",
json_object_new_string(item->fields[FIELD_ENCLOSURE_URL]));
json_object_object_add(root, "enclosure", enclosure);
}
if (item->fields[FIELD_DESCRIPTION])
json_object_object_add(root, "description",
json_object_new_string(item->fields[FIELD_DESCRIPTION]));
fprintf(f, "%s", json_object_to_json_string_ext(root, 0));
json_object_put(root);
}
#endif // JSON
void void
itemAction(itemStruct *item, const char *folder) itemAction(itemStruct *item, const char *folder)
{ {
@ -149,11 +187,32 @@ itemAction(itemStruct *item, const char *folder)
while (cur) { while (cur) {
prev = cur; prev = cur;
FILE *itemFile = openFile(folder, san(cur->fields[FIELD_TITLE]), ".html");
char fileExt[10];
void (*outputFunction)(itemStruct *, FILE *);
switch (outputFormat) {
case OUTPUT_HTML:
memcpy(fileExt, ".html", 6);
outputFunction = &outputHtml;
break;
#ifdef JSON
case OUTPUT_JSON:
memcpy(fileExt, ".json", 6);
outputFunction = &outputJson;
break;
#endif //JSON
default:
logMsg(0, "Output format is invalid.");
break;
}
FILE *itemFile = openFile(folder, san(cur->fields[FIELD_TITLE]), fileExt);
// Do not overwrite files // Do not overwrite files
if (!ftell(itemFile)) { if (!ftell(itemFile)) {
outputHtml(cur, itemFile); outputFunction(cur, itemFile);
newItems++; newItems++;
} }

View File

@ -25,7 +25,11 @@ You should have received a copy of the GNU General Public License along with thi
#include "handlers.h" #include "handlers.h"
#include "config.h" #include "config.h"
#define TAGIS(X, Y) (!xmlStrcmp(X->name, (const xmlChar *) Y)) static inline int
tagIs(xmlNodePtr node, char *str)
{
return !xmlStrcmp(node->name, (const xmlChar *) str);
}
static int static int
parseXml(xmlDocPtr doc, parseXml(xmlDocPtr doc,
@ -50,9 +54,9 @@ parseXml(xmlDocPtr doc,
enum feedFormat format = NONE; enum feedFormat format = NONE;
if (TAGIS(rootNode, "rss")) { if (tagIs(rootNode, "rss")) {
format = RSS; format = RSS;
} else if (TAGIS(rootNode, "feed")) { } else if (tagIs(rootNode, "feed")) {
if (!xmlStrcmp(rootNode->ns->href, (const xmlChar *) "http://www.w3.org/2005/Atom")) if (!xmlStrcmp(rootNode->ns->href, (const xmlChar *) "http://www.w3.org/2005/Atom"))
format = ATOM; format = ATOM;
} }
@ -69,10 +73,10 @@ parseXml(xmlDocPtr doc,
switch (format) { switch (format) {
case RSS: case RSS:
// Get channel XML tag // Get channel XML tag
while(cur && !TAGIS(cur, "channel")) while(cur && !tagIs(cur, "channel"))
cur = cur->next; cur = cur->next;
if (!cur || !TAGIS(cur, "channel")) { if (!cur || !tagIs(cur, "channel")) {
logMsg(1, "Invalid RSS syntax.\n"); logMsg(1, "Invalid RSS syntax.\n");
return 1; return 1;
} }
@ -101,10 +105,10 @@ parseXml(xmlDocPtr doc,
switch (format) { switch (format) {
case RSS: case RSS:
isArticle = TAGIS(cur, "item"); isArticle = tagIs(cur, "item");
break; break;
case ATOM: case ATOM:
isArticle = TAGIS(cur, "entry"); isArticle = tagIs(cur, "entry");
break; break;
default: default:
logMsg(1, "Missing article tag name for format\n"); logMsg(1, "Missing article tag name for format\n");
@ -128,21 +132,21 @@ parseXml(xmlDocPtr doc,
switch (format) { switch (format) {
case RSS: case RSS:
if TAGIS(itemNode, "link") if (tagIs(itemNode, "link"))
copyField(item, FIELD_LINK, itemKey); copyField(item, FIELD_LINK, itemKey);
else if TAGIS(itemNode, "description") else if (tagIs(itemNode, "description"))
copyField(item, FIELD_DESCRIPTION, itemKey); copyField(item, FIELD_DESCRIPTION, itemKey);
else if TAGIS(itemNode, "title") else if (tagIs(itemNode, "title"))
copyField(item, FIELD_TITLE, itemKey); copyField(item, FIELD_TITLE, itemKey);
else if TAGIS(itemNode, "enclosure") else if (tagIs(itemNode, "enclosure"))
rssEnclosure(item, itemNode); rssEnclosure(item, itemNode);
break; break;
case ATOM: case ATOM:
if TAGIS(itemNode, "link") if (tagIs(itemNode, "link"))
atomLink(item, itemNode); atomLink(item, itemNode);
else if TAGIS(itemNode, "content") else if (tagIs(itemNode, "content"))
copyField(item, FIELD_DESCRIPTION, itemKey); copyField(item, FIELD_DESCRIPTION, itemKey);
else if TAGIS(itemNode, "title") else if (tagIs(itemNode, "title"))
copyField(item, FIELD_TITLE, itemKey); copyField(item, FIELD_TITLE, itemKey);
break; break;
default: default: