Added RSS export.
continuous-integration/drone/push Build is passing Details

This commit is contained in:
tastytea 2019-09-06 00:53:58 +02:00
parent 7557064215
commit c9b82fd774
Signed by: tastytea
GPG Key ID: CFC39497F1B26E07
11 changed files with 228 additions and 14 deletions

View File

@ -5,8 +5,8 @@
an URI to the archived version, the current date and time, title, description, an URI to the archived version, the current date and time, title, description,
the full text of the page and optional tags. the full text of the page and optional tags.
The database can be filtered by time, tags and full text and exported to CSV or The database can be filtered by time, tags and full text and exported to CSV,
AsciiDoc. AsciiDoc, JSON or RSS.
Archiving is done using the Wayback machine from the Archiving is done using the Wayback machine from the
https://archive.org/[Internet Archive]. https://archive.org/[Internet Archive].

View File

@ -2,7 +2,7 @@ include(CMakeFindDependencyMacro)
include(GNUInstallDirs) include(GNUInstallDirs)
find_depencency(Poco find_depencency(Poco
COMPONENTS Foundation Net NetSSL Data DataSQLite JSON COMPONENTS Foundation Net NetSSL Data DataSQLite JSON XML
CONFIG REQUIRED) CONFIG REQUIRED)
find_dependency(PkgConfig REQUIRED) find_dependency(PkgConfig REQUIRED)
pkg_check_modules(libxdg-basedir REQUIRED IMPORTED_TARGET libxdg-basedir) pkg_check_modules(libxdg-basedir REQUIRED IMPORTED_TARGET libxdg-basedir)

46
include/export/rss.hpp Normal file
View File

@ -0,0 +1,46 @@
/* This file is part of remwharead.
* Copyright © 2019 tastytea <tastytea@tastytea.de>
*
* 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, version 3.
*
* 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. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef REMWHAREAD_RSS_HPP
#define REMWHAREAD_RSS_HPP
#include <string>
#include "export.hpp"
namespace remwharead
{
namespace Export
{
using std::string;
/*!
* @brief Export as RSS feed.
*
* @since 0.8.0
*
* @headerfile rss.hpp remwharead/export/rss.hpp
*/
class RSS : protected ExportBase
{
public:
using ExportBase::ExportBase;
virtual void print() const override;
};
}
}
#endif // REMWHAREAD_RSS_HPP

View File

@ -45,6 +45,7 @@
#include "export/export.hpp" #include "export/export.hpp"
#include "export/simple.hpp" #include "export/simple.hpp"
#include "export/json.hpp" #include "export/json.hpp"
#include "export/rss.hpp"
#include "search.hpp" #include "search.hpp"
#include "sqlite.hpp" #include "sqlite.hpp"
#include "time.hpp" #include "time.hpp"

View File

@ -35,7 +35,8 @@ namespace remwharead
asciidoc, asciidoc,
bookmarks, bookmarks,
simple, simple,
json json,
rss
}; };
} }

View File

@ -2,7 +2,7 @@
:doctype: manpage :doctype: manpage
:Author: tastytea :Author: tastytea
:Email: tastytea@tastytea.de :Email: tastytea@tastytea.de
:Date: 2019-09-03 :Date: 2019-09-06
:Revision: 0.0.0 :Revision: 0.0.0
:man source: remwharead :man source: remwharead
:man manual: General Commands Manual :man manual: General Commands Manual
@ -24,7 +24,7 @@ remwharead - Saves URIs of things you want to remember in a database
the full text of the page and optional tags. the full text of the page and optional tags.
The database can be filtered by time, tags and full text and exported to CSV, The database can be filtered by time, tags and full text and exported to CSV,
AsciiDoc, a bookmarks file or JSON. AsciiDoc, a bookmarks file, JSON or RSS.
Archiving is done using the Wayback machine from the Archiving is done using the Wayback machine from the
https://archive.org/[Internet Archive]. https://archive.org/[Internet Archive].
@ -36,7 +36,7 @@ Add tags to _URI_, delimited by commas.
*-e* _format_, *--export* _format_:: *-e* _format_, *--export* _format_::
Export to _format_. Possible values are _csv_, _asciidoc_, _bookmarks_, Export to _format_. Possible values are _csv_, _asciidoc_, _bookmarks_,
_simple_ or _json_. See _FORMATS_. _simple_, _json_ or _rss_. See _FORMATS_.
*-f* _file_, *--file* _file_:: *-f* _file_, *--file* _file_::
Save output to _file_. Default is stdout. Save output to _file_. Default is stdout.
@ -139,13 +139,19 @@ Export as JSON array. See https://tools.ietf.org/html/rfc8259[RFC 8259]. Each
object contains the members _uri_, _archive_uri_, _datetime_, _tags_ (array), object contains the members _uri_, _archive_uri_, _datetime_, _tags_ (array),
_title_, _description_ and _fulltext_. _title_, _description_ and _fulltext_.
=== rss
Export as http://www.rssboard.org/rss-specification[RSS] feed. Because the URL
of the feed is unknown to *remwharead*, the generated feed is slightly out of
specification (the element _link_ in _channel_ is empty).
== SEARCH EXPRESSIONS == SEARCH EXPRESSIONS
A search expression is either a single term, or several terms separated by _AND_ A search expression is either a single term, or several terms separated by _AND_
or _OR_. _AND_ takes precedence. The expression _Mountain AND Big OR Vegetable_ or _OR_. _AND_ takes precedence. The expression _Mountain AND Big OR Vegetable_
finds all things that have either Mountain and Big, or Vegetable in them. You can finds all things that have either Mountain and Big, or Vegetable in them. You
use _||_ instead of _OR_ and _&&_ instead of _AND_. Note that *--search-tags* can use _||_ instead of _OR_ and _&&_ instead of _AND_. Note that
only matches whole tags, Pill does not match Pillow. *--search-tags* only matches whole tags, Pill does not match Pillow.
== PROTOCOL SUPPORT == PROTOCOL SUPPORT

View File

@ -10,4 +10,4 @@ Version: @PROJECT_VERSION@
Cflags: -I${includedir} Cflags: -I${includedir}
Libs: -L${libdir} -l${name} -lPocoData -lstdc++fs Libs: -L${libdir} -l${name} -lPocoData -lstdc++fs
Requires.private: libxdg-basedir, icu-uc, icu-i18n Requires.private: libxdg-basedir, icu-uc, icu-i18n
Libs.private: -lPocoFoundation -lPocoNet -lPocoNetSSL -lPocoDataSQLite -lPocoJSON Libs.private: -lPocoFoundation -lPocoNet -lPocoNetSSL -lPocoDataSQLite -lPocoJSON -lPocoXML

View File

@ -29,6 +29,7 @@
#include "export/bookmarks.hpp" #include "export/bookmarks.hpp"
#include "export/simple.hpp" #include "export/simple.hpp"
#include "export/json.hpp" #include "export/json.hpp"
#include "export/rss.hpp"
#include "search.hpp" #include "search.hpp"
using namespace remwharead; using namespace remwharead;
@ -191,6 +192,19 @@ int App::main(const std::vector<std::string> &args)
} }
break; break;
} }
case export_format::rss:
{
if (file.is_open())
{
Export::RSS(entries, file).print();
file.close();
}
else
{
Export::RSS(entries).print();
}
break;
}
default: default:
{ {
break; break;

View File

@ -143,6 +143,10 @@ void App::handle_options(const std::string &name, const std::string &value)
{ {
_format = export_format::json; _format = export_format::json;
} }
else if (value == "rss")
{
_format = export_format::rss;
}
else else
{ {
cerr << "Error: Unknown format.\n"; cerr << "Error: Unknown format.\n";

View File

@ -3,7 +3,9 @@ include(GNUInstallDirs)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
pkg_check_modules(libxdg-basedir REQUIRED IMPORTED_TARGET libxdg-basedir) pkg_check_modules(libxdg-basedir REQUIRED IMPORTED_TARGET libxdg-basedir)
# Some distributions do not contain Poco*Config.cmake recipes. # Some distributions do not contain Poco*Config.cmake recipes.
find_package(Poco COMPONENTS Foundation Net NetSSL Data DataSQLite JSON CONFIG) find_package(Poco
COMPONENTS Foundation Net NetSSL Data DataSQLite JSON XML
CONFIG)
file(GLOB_RECURSE sources_lib *.cpp) file(GLOB_RECURSE sources_lib *.cpp)
file(GLOB_RECURSE headers_lib ../../include/*.hpp) file(GLOB_RECURSE headers_lib ../../include/*.hpp)
@ -28,7 +30,8 @@ target_link_libraries(${PROJECT_NAME}
# If no Poco*Config.cmake recipes are found, look for headers in standard dirs. # If no Poco*Config.cmake recipes are found, look for headers in standard dirs.
if(PocoNetSSL_FOUND) if(PocoNetSSL_FOUND)
target_link_libraries(${PROJECT_NAME} target_link_libraries(${PROJECT_NAME}
PRIVATE Poco::Foundation Poco::Net Poco::NetSSL Poco::DataSQLite Poco::JSON PRIVATE Poco::Foundation Poco::Net Poco::NetSSL Poco::DataSQLite
Poco::JSON Poco::XML
PUBLIC Poco::Data) PUBLIC Poco::Data)
else() else()
find_file(Poco_h NAMES "Poco/Poco.h" find_file(Poco_h NAMES "Poco/Poco.h"
@ -42,7 +45,7 @@ else()
"but the files seem to be in the standard directories. " "but the files seem to be in the standard directories. "
"Let's hope this works.") "Let's hope this works.")
target_link_libraries(${PROJECT_NAME} target_link_libraries(${PROJECT_NAME}
PRIVATE PocoFoundation PocoNet PocoNetSSL PocoDataSQLite PocoJSON PRIVATE PocoFoundation PocoNet PocoNetSSL PocoDataSQLite PocoJSON PocoXML
PUBLIC PocoData) PUBLIC PocoData)
endif() endif()
endif() endif()

139
src/lib/export/rss.cpp Normal file
View File

@ -0,0 +1,139 @@
/* This file is part of remwharead.
* Copyright © 2019 tastytea <tastytea@tastytea.de>
*
* 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, version 3.
*
* 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. If not, see <http://www.gnu.org/licenses/>.
*/
#include <ctime>
#include <Poco/XML/XMLWriter.h>
#include <Poco/SAX/AttributesImpl.h>
#include <Poco/DateTime.h>
#include <Poco/DateTimeFormatter.h>
#include <Poco/Timestamp.h>
#include "time.hpp"
#include "version.hpp"
#include "export/rss.hpp"
namespace remwharead
{
using std::cerr;
using std::endl;
using std::time_t;
using Poco::XML::XMLWriter;
using Poco::XML::AttributesImpl;
using Poco::DateTime;
using Poco::DateTimeFormatter;
using Poco::Timestamp;
void Export::RSS::print() const
{
try
{
XMLWriter writer(_out, XMLWriter::CANONICAL);
AttributesImpl attrs_rss, attrs_guid;
constexpr char timefmt_rfc822[] = "%w, %d %b %Y %H:%M:%S %Z";
attrs_rss.addAttribute("", "", "version", "", "2.0");
attrs_rss.addAttribute("", "", "xmlns:atom", "",
"http://www.w3.org/2005/Atom");
attrs_guid.addAttribute("", "", "isPermaLink", "", "false");
writer.startDocument();
writer.startElement("", "", "rss", attrs_rss);
writer.startElement("", "", "channel");
writer.startElement("", "", "title");
writer.characters("Visited things");
writer.endElement("", "", "title");
writer.startElement("", "", "link");
// FIXME: There has to be an URL here.
writer.endElement("", "", "link");
writer.startElement("", "", "description");
writer.characters("Export from remwharead.");
writer.endElement("", "", "description");
writer.startElement("", "", "generator");
writer.characters(string("remwharead ") + global::version);
writer.endElement("", "", "generator");
const string now = DateTimeFormatter::format(DateTime(),
timefmt_rfc822);
writer.startElement("", "", "lastBuildDate");
writer.characters(now);
writer.endElement("", "", "lastBuildDate");
for (const Database::entry &entry : _entries)
{
writer.startElement("", "", "item");
writer.startElement("", "", "title");
writer.characters(entry.title);
writer.endElement("", "", "title");
writer.startElement("", "", "link");
writer.characters(entry.uri);
writer.endElement("", "", "link");
writer.startElement("", "", "guid", attrs_guid);
writer.characters(entry.uri + " at " +
timepoint_to_string(entry.datetime));
writer.endElement("", "", "guid");
const time_t time = system_clock::to_time_t(entry.datetime);
const string time_visited = DateTimeFormatter::format(
Timestamp::fromEpochTime(time), timefmt_rfc822);
writer.startElement("", "", "pubDate");
writer.characters(time_visited);
writer.endElement("", "", "pubDate");
string description = entry.description;
if (!description.empty())
{
description += "\n\n";
}
if (!entry.tags.empty())
{
description += "Tags: ";
for (const string &tag : entry.tags)
{
description += tag;
if (tag != *(entry.tags.rbegin()))
{
description += ", ";
}
}
}
if (!entry.archive_uri.empty())
{
description += "\n\nArchived version: " + entry.archive_uri;
}
writer.startElement("", "", "description");
writer.characters(description);
writer.endElement("", "", "description");
writer.endElement("", "", "item");
}
writer.endElement("", "", "channel");
writer.endElement("", "", "rss");
writer.endDocument();
_out << endl;
}
catch (std::exception &e)
{
cerr << "Error in " << __func__ << ": " << e.what() << endl;
}
}
}