diff --git a/README.adoc b/README.adoc index c2e419b..0240251 100644 --- a/README.adoc +++ b/README.adoc @@ -5,8 +5,8 @@ an URI to the archived version, the current date and time, title, description, 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 -AsciiDoc. +The database can be filtered by time, tags and full text and exported to CSV, +AsciiDoc, JSON or RSS. Archiving is done using the Wayback machine from the https://archive.org/[Internet Archive]. diff --git a/cmake/remwhareadConfig.cmake.in b/cmake/remwhareadConfig.cmake.in index 14c485a..5c1586e 100644 --- a/cmake/remwhareadConfig.cmake.in +++ b/cmake/remwhareadConfig.cmake.in @@ -2,7 +2,7 @@ include(CMakeFindDependencyMacro) include(GNUInstallDirs) find_depencency(Poco - COMPONENTS Foundation Net NetSSL Data DataSQLite JSON + COMPONENTS Foundation Net NetSSL Data DataSQLite JSON XML CONFIG REQUIRED) find_dependency(PkgConfig REQUIRED) pkg_check_modules(libxdg-basedir REQUIRED IMPORTED_TARGET libxdg-basedir) diff --git a/include/export/rss.hpp b/include/export/rss.hpp new file mode 100644 index 0000000..8fe9495 --- /dev/null +++ b/include/export/rss.hpp @@ -0,0 +1,46 @@ +/* This file is part of remwharead. + * Copyright © 2019 tastytea + * + * 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 . + */ + +#ifndef REMWHAREAD_RSS_HPP +#define REMWHAREAD_RSS_HPP + +#include +#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 diff --git a/include/remwharead.hpp b/include/remwharead.hpp index 6dd0be2..232147f 100644 --- a/include/remwharead.hpp +++ b/include/remwharead.hpp @@ -45,6 +45,7 @@ #include "export/export.hpp" #include "export/simple.hpp" #include "export/json.hpp" +#include "export/rss.hpp" #include "search.hpp" #include "sqlite.hpp" #include "time.hpp" diff --git a/include/types.hpp b/include/types.hpp index 2b38c50..a03a1c1 100644 --- a/include/types.hpp +++ b/include/types.hpp @@ -35,7 +35,8 @@ namespace remwharead asciidoc, bookmarks, simple, - json + json, + rss }; } diff --git a/man/remwharead.1.adoc b/man/remwharead.1.adoc index 27c5300..8aa89dd 100644 --- a/man/remwharead.1.adoc +++ b/man/remwharead.1.adoc @@ -2,7 +2,7 @@ :doctype: manpage :Author: tastytea :Email: tastytea@tastytea.de -:Date: 2019-09-03 +:Date: 2019-09-06 :Revision: 0.0.0 :man source: remwharead :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 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 https://archive.org/[Internet Archive]. @@ -36,7 +36,7 @@ Add tags to _URI_, delimited by commas. *-e* _format_, *--export* _format_:: Export to _format_. Possible values are _csv_, _asciidoc_, _bookmarks_, -_simple_ or _json_. See _FORMATS_. +_simple_, _json_ or _rss_. See _FORMATS_. *-f* _file_, *--file* _file_:: 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), _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 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_ -finds all things that have either Mountain and Big, or Vegetable in them. You can -use _||_ instead of _OR_ and _&&_ instead of _AND_. Note that *--search-tags* -only matches whole tags, Pill does not match Pillow. +finds all things that have either Mountain and Big, or Vegetable in them. You +can use _||_ instead of _OR_ and _&&_ instead of _AND_. Note that +*--search-tags* only matches whole tags, Pill does not match Pillow. == PROTOCOL SUPPORT diff --git a/pkg-config/remwharead.pc.in b/pkg-config/remwharead.pc.in index f572fcb..001d6b6 100644 --- a/pkg-config/remwharead.pc.in +++ b/pkg-config/remwharead.pc.in @@ -10,4 +10,4 @@ Version: @PROJECT_VERSION@ Cflags: -I${includedir} Libs: -L${libdir} -l${name} -lPocoData -lstdc++fs Requires.private: libxdg-basedir, icu-uc, icu-i18n -Libs.private: -lPocoFoundation -lPocoNet -lPocoNetSSL -lPocoDataSQLite -lPocoJSON +Libs.private: -lPocoFoundation -lPocoNet -lPocoNetSSL -lPocoDataSQLite -lPocoJSON -lPocoXML diff --git a/src/cli/main.cpp b/src/cli/main.cpp index 01f9c95..1d155e6 100644 --- a/src/cli/main.cpp +++ b/src/cli/main.cpp @@ -29,6 +29,7 @@ #include "export/bookmarks.hpp" #include "export/simple.hpp" #include "export/json.hpp" +#include "export/rss.hpp" #include "search.hpp" using namespace remwharead; @@ -191,6 +192,19 @@ int App::main(const std::vector &args) } break; } + case export_format::rss: + { + if (file.is_open()) + { + Export::RSS(entries, file).print(); + file.close(); + } + else + { + Export::RSS(entries).print(); + } + break; + } default: { break; diff --git a/src/cli/parse_options.cpp b/src/cli/parse_options.cpp index 3a16507..fcab7a6 100644 --- a/src/cli/parse_options.cpp +++ b/src/cli/parse_options.cpp @@ -143,6 +143,10 @@ void App::handle_options(const std::string &name, const std::string &value) { _format = export_format::json; } + else if (value == "rss") + { + _format = export_format::rss; + } else { cerr << "Error: Unknown format.\n"; diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index 329ca44..94af7f6 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -3,7 +3,9 @@ include(GNUInstallDirs) find_package(PkgConfig REQUIRED) pkg_check_modules(libxdg-basedir REQUIRED IMPORTED_TARGET libxdg-basedir) # 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 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(PocoNetSSL_FOUND) 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) else() find_file(Poco_h NAMES "Poco/Poco.h" @@ -42,7 +45,7 @@ else() "but the files seem to be in the standard directories. " "Let's hope this works.") target_link_libraries(${PROJECT_NAME} - PRIVATE PocoFoundation PocoNet PocoNetSSL PocoDataSQLite PocoJSON + PRIVATE PocoFoundation PocoNet PocoNetSSL PocoDataSQLite PocoJSON PocoXML PUBLIC PocoData) endif() endif() diff --git a/src/lib/export/rss.cpp b/src/lib/export/rss.cpp new file mode 100644 index 0000000..758b8a5 --- /dev/null +++ b/src/lib/export/rss.cpp @@ -0,0 +1,139 @@ +/* This file is part of remwharead. + * Copyright © 2019 tastytea + * + * 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 . + */ + +#include +#include +#include +#include +#include +#include +#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; + } + } +}