File Coverage

blib/lib/App/Zapzi.pm
Criterion Covered Total %
statement 28 30 93.3
branch n/a
condition n/a
subroutine 10 10 100.0
pod n/a
total 38 40 95.0


line stmt bran cond sub pod time code
1             package App::Zapzi;
2             # ABSTRACT: store articles and publish them to read later
3              
4 8     8   1000694 use utf8;
  8         80  
  8         44  
5 8     8   300 use strict;
  8         22  
  8         398  
6 8     8   41 use warnings;
  8         10  
  8         569  
7              
8             our $VERSION = '0.017'; # VERSION
9              
10             binmode(STDOUT, ":encoding(UTF-8)");
11              
12 8     8   4527 use IO::Interactive;
  8         63403  
  8         41  
13 8     8   5406 use Term::Prompt 1.04;
  8         121347  
  8         646  
14 8     8   5278 use Browser::Open;
  8         21322  
  8         458  
15 8     8   4906 use Getopt::Lucid 1.05 qw( :all );
  8         99825  
  8         1659  
16 8     8   5465 use File::HomeDir;
  8         45235  
  8         665  
17 8     8   67 use Path::Tiny;
  8         14  
  8         434  
18 8     8   4141 use App::Zapzi::Database;
  0            
  0            
19             use App::Zapzi::Folders;
20             use App::Zapzi::Articles;
21             use App::Zapzi::FetchArticle;
22             use App::Zapzi::Transform;
23             use App::Zapzi::Publish;
24             use App::Zapzi::UserConfig;
25             use App::Zapzi::Distribute;
26             use Moo 1.003000;
27             use Carp;
28              
29              
30             has run => (is => 'rw', default => -1);
31              
32              
33             has force => (is => 'rw', default => 0);
34              
35              
36             has noarchive => (is => 'rw', default => 0);
37              
38              
39             has long => (is => 'rw', default => 0);
40              
41              
42             has folder => (is => 'rw', default => 'Inbox');
43              
44              
45             has transformer => (is => 'rw', default => '');
46              
47              
48             has format => (is => 'rw');
49              
50              
51             has encoding => (is => 'rw');
52              
53              
54             has distribute => (is => 'rw');
55              
56              
57             our $_the_app;
58             sub BUILD { $_the_app = shift; }
59             sub get_app
60             {
61             croak 'Must create an instance of App::Zapzi first' unless $_the_app;
62             return $_the_app;
63             }
64              
65              
66             has zapzi_dir =>
67             (
68             is => 'ro',
69             default => sub
70             {
71             return $ENV{ZAPZI_DIR} // File::HomeDir->my_home . "/.zapzi";
72             }
73             );
74              
75              
76             has zapzi_ebook_dir =>
77             (
78             is => 'ro',
79             default => sub
80             {
81             my $self = shift;
82             return $self->zapzi_dir . '/ebooks';
83             }
84             );
85              
86              
87             has database =>
88             (
89             is => 'ro',
90             default => sub
91             {
92             my $self = shift;
93             return App::Zapzi::Database->new(app => $self);
94             }
95             );
96              
97              
98             has test_database =>
99             (
100             is => 'ro',
101             default => 0
102             );
103              
104              
105             has interactive =>
106             (
107             is => 'ro',
108             default => sub { IO::Interactive::is_interactive(); }
109             );
110              
111              
112             sub process_args
113             {
114             my $self = shift;
115             my @args = @_;
116              
117             my @specs =
118             (
119             Switch("help|h"),
120             Switch("version|v"),
121             Switch("init"),
122             Switch("config"),
123             Switch("add"),
124             Switch("list|ls"),
125             Switch("list-folders|lsf"),
126             Switch("make-folder|mkf|md"),
127             Switch("delete-folder|rmf|rd"),
128             Switch("delete-article|delete|rm"),
129             Switch("show|view"),
130             Switch("export|cat"),
131             Switch("move|mv"),
132             Switch("publish|pub"),
133              
134             Param("folder|f"),
135             Param("transformer|t"),
136             Param("format|fmt"),
137             Param("encoding|enc"),
138             Param("distribute|d"),
139             Switch("force"),
140             Switch("noarchive"),
141             Switch("long|l"),
142             );
143              
144             my $options = Getopt::Lucid->getopt(\@specs, \@args)->validate;
145              
146             $self->force($options->get_force);
147             $self->noarchive($options->get_noarchive);
148             $self->long($options->get_long);
149             $self->folder($options->get_folder // $self->folder);
150             $self->transformer($options->get_transformer // $self->transformer);
151              
152             $self->help if $options->get_help;
153             $self->version if $options->get_version;
154             $self->init if $options->get_init;
155              
156             # For any further operations we need a database
157             if (! -r $self->database->database_file && ! $self->test_database)
158             {
159             print "Zapzi database does not exist; did you run 'zapzi init'?\n";
160             $self->run(1);
161             return;
162             }
163              
164             # Upgrade the DB, if needed
165             $self->database->upgrade
166             unless $self->database->check_version || $self->run == 0;
167              
168             unless ($options->get_make_folder)
169             {
170             if (! $self->validate_folder($self->folder))
171             {
172             $self->run(1);
173             return;
174             }
175             }
176              
177             $self->format($options->get_format //
178             $self->format //
179             App::Zapzi::UserConfig::get('publish_format'));
180             $self->encoding($options->get_encoding //
181             $self->encoding //
182             App::Zapzi::UserConfig::get('publish_encoding'));
183             $self->distribute($options->get_distribute //
184             $self->distribute //
185             App::Zapzi::UserConfig::get('distribution_method'));
186              
187             $self->config(@args) if $options->get_config;
188             $self->list if $options->get_list;
189             $self->list_folders if $options->get_list_folders;
190             $self->make_folder(@args) if $options->get_make_folder;
191             $self->delete_folder(@args) if $options->get_delete_folder;
192             $self->delete_article(@args) if $options->get_delete_article;
193             @args = $self->add(@args) if $options->get_add;
194             $self->show('browser', @args) if $options->get_show;
195             $self->show('stdout', @args) if $options->get_export;
196             $self->move(@args) if $options->get_move;
197             $self->publish(@args) if $options->get_publish;
198              
199             # Fallthrough if no valid commands given
200             $self->help if $self->run == -1;
201             }
202              
203              
204             sub init
205             {
206             my $self = shift;
207             my $dir = $self->zapzi_dir;
208              
209             $self->run(1);
210              
211             if (! $dir || $dir eq '')
212             {
213             print "Zapzi directory not supplied\n";
214             }
215             elsif (-d $dir && ! $self->force)
216             {
217             print "Zapzi directory $dir already exists\n";
218             print "To force recreation, run with the --force option\n";
219             }
220             else
221             {
222             $self->database->init;
223             print "Created Zapzi directory $dir\n\n";
224             if ($self->init_config())
225             {
226             print "\nInitialisation completed. Type 'zapzi help' to view " .
227             "command line options\n";
228             $self->run(0);
229             }
230             }
231             }
232              
233              
234             sub init_config
235             {
236             my $self = shift;
237              
238             if ($self->interactive)
239             {
240             print "Select configuration options. " .
241             "Press enter to accept defaults.\n\n";
242             }
243             else
244             {
245             print "Not running interactively, so will use defaults for " .
246             "configuration variables.\n";
247             print "Use the 'zapzi config' command to view/set these later\n";
248             }
249              
250             my @keys = App::Zapzi::UserConfig::get_user_init_configurable_keys();
251             while (@keys)
252             {
253             my $key = shift @keys;
254             my $value = App::Zapzi::UserConfig::get_default($key);
255              
256             if ($self->interactive)
257             {
258             $value = prompt('s', # validate by sub
259             App::Zapzi::UserConfig::get_description($key),
260             App::Zapzi::UserConfig::get_options($key),
261             $value, # the default value
262             App::Zapzi::UserConfig::get_validater($key));
263             }
264              
265             # Only ask distribution_destination if distribution_method is set
266             if ($key eq 'distribution_method' && lc($value) ne 'nothing')
267             {
268             unshift @keys, 'distribution_destination';
269             }
270              
271             return unless App::Zapzi::UserConfig::set($key, $value);
272             }
273              
274             return 1;
275             }
276              
277              
278              
279             sub config
280             {
281             my $self = shift;
282             my @args = @_;
283             my $command = shift @args;
284              
285             $self->run(0);
286             if (! $command || $command eq 'get')
287             {
288             # Get all unless keys were specified
289             @args = App::Zapzi::UserConfig::get_user_configurable_keys()
290             unless @args;
291              
292             for (@args)
293             {
294             my $key = $_;
295             my $doc = App::Zapzi::UserConfig::get_doc($key);
296             if ($doc)
297             {
298             print $doc;
299             my $value = App::Zapzi::UserConfig::get($key);
300             printf("%s = %s\n\n", $key, $value ? $value : '<not set>');
301             }
302             else
303             {
304             print "Config variable '$key' does not exist\n";
305             $self->run(1);
306             }
307             }
308             }
309             elsif ($command eq 'set')
310             {
311             if (scalar(@args) != 2)
312             {
313             print "Invalid config set command - try 'set key value'\n";
314             $self->run(1);
315             }
316             else
317             {
318             # set key value
319             my ($key, $input) = @args;
320             if (my $value = App::Zapzi::UserConfig::set($key, $input))
321             {
322             print "Set '$key' = '$value'\n";
323             }
324             else
325             {
326             print "Invalid config set command '$key $input'\n";
327             $self->run(1);
328             }
329             }
330             }
331             else
332             {
333             print "Invalid config command - try 'get' or 'set'\n";
334             $self->run(1);
335             }
336             }
337              
338              
339             sub validate_folder
340             {
341             my $self = shift;
342              
343             if (! App::Zapzi::Articles::get_folder($self->folder))
344             {
345             printf("Folder '%s' does not exist\n", $self->folder);
346             $self->run(1);
347             return;
348             }
349             else
350             {
351             return 1;
352             }
353             }
354              
355              
356             sub validate_article_ids
357             {
358             my ($self, @args) = @_;
359              
360             if (scalar(@args) < 1 || grep { /[^0-9]/ } @args)
361             {
362             print "Need to supply one or more article IDs\n";
363             $self->run(1);
364             return;
365             }
366              
367             return 1;
368             }
369              
370              
371             sub list
372             {
373             my $self = shift;
374             my $summary = App::Zapzi::Articles::articles_summary($self->folder);
375             foreach (@$summary)
376             {
377             my $article = $_;
378             if ($self->long)
379             {
380             print "Folder: ", $self->folder, "\n";
381             print "ID: ", $article->{id}, "\n";
382             print "Title: ", $article->{title}, "\n";
383             print "Source: ", $article->{source}, "\n";
384             print "Created: ",
385             $article->{created}->strftime('%d-%b-%Y %H:%M:%S'), "\n";
386             printf("Size: %.1fkb\n", length($article->{text}) / 1024);
387             print "\n";
388             }
389             else
390             {
391             printf("%s %4d %s %-45s\n", $self->folder,
392             $article->{id}, $article->{created}->strftime('%d-%b-%Y'),
393             $article->{title});
394             }
395             }
396             $self->run(0);
397             }
398              
399              
400             sub list_folders
401             {
402             my $self = shift;
403             my $summary = App::Zapzi::Folders::folders_summary();
404             foreach (sort keys %$summary)
405             {
406             printf("%-10s %3d\n", $_, $summary->{$_});
407             }
408              
409             $self->run(0);
410             }
411              
412              
413             sub make_folder
414             {
415             my $self = shift;
416             my @args = @_;
417              
418             if (! @args)
419             {
420             print "Need to provide folder names to create\n";
421             $self->run(1);
422             return;
423             }
424              
425             $self->run(0);
426             for (@args)
427             {
428             my $folder = $_;
429             if (App::Zapzi::Folders::get_folder($folder))
430             {
431             print "Folder '$folder' already exists\n";
432             }
433             else
434             {
435             App::Zapzi::Folders::add_folder($folder);
436             print "Created folder '$folder'\n";
437             }
438             }
439             }
440              
441              
442             sub delete_folder
443             {
444             my $self = shift;
445             my @args = @_;
446              
447             if (! @args)
448             {
449             print "Need to provide folder names to delete\n";
450             $self->run(1);
451             return;
452             }
453              
454             $self->run(0);
455             for (@args)
456             {
457             my $folder = $_;
458             if (App::Zapzi::Folders::is_system_folder($folder))
459             {
460             print "Can't remove '$folder' as it is needed by the system\n";
461             }
462             elsif (! App::Zapzi::Folders::get_folder($folder))
463             {
464             print "Folder '$folder' does not exist\n";
465             }
466             else
467             {
468             App::Zapzi::Folders::delete_folder($folder);
469             print "Deleted folder '$folder'\n";
470             }
471             }
472             }
473              
474              
475             sub delete_article
476             {
477             my $self = shift;
478             my @args = @_;
479              
480             return unless $self->validate_article_ids(@args);
481              
482             $self->run(0);
483             for (@args)
484             {
485             my $id = $_;
486             my $art_rs = App::Zapzi::Articles::get_article($id);
487             if ($art_rs)
488             {
489             if (App::Zapzi::Articles::delete_article($id))
490             {
491             print "Deleted article $id\n";
492             }
493             else
494             {
495             print "Could not delete article $id\n";
496             }
497             }
498             else
499             {
500             print "Could not get article $id\n";
501             $self->run(1);
502             }
503             }
504             }
505              
506              
507             sub add
508             {
509             my $self = shift;
510             my @args = @_;
511              
512             if (scalar @args == 1 && $args[0] eq '-')
513             {
514             @args = _read_lines_from_stdin();
515             }
516              
517             if (! @args)
518             {
519             print "Need to provide articles names to add\n";
520             $self->run(1);
521             return;
522             }
523              
524             $self->run(0);
525             my @article_ids;
526             for (@args)
527             {
528             my $source = $_;
529             print "Working on $source\n";
530             my $f = App::Zapzi::FetchArticle->new(source => $source);
531             if (! $f->fetch)
532             {
533             print "Could not get article: ", $f->error, "\n\n";
534             $self->run(1);
535             next;
536             }
537              
538             my $tx = App::Zapzi::Transform->new(raw_article => $f,
539             transformer => $self->transformer);
540             if (! $tx->to_readable)
541             {
542             print "Could not transform article: ", $tx->error, "\n\n";
543             $self->run(1);
544             next;
545             }
546              
547             printf("Got '%s' (%.1fkb)\n", $tx->title,
548             length($tx->readable_text) / 1024);
549              
550             my $rs = App::Zapzi::Articles::add_article(title => $tx->title,
551             source =>
552             $f->validated_source,
553             text => $tx->readable_text,
554             folder => $self->folder);
555             printf("Added article %d to folder '%s'\n\n", $rs->id, $self->folder);
556             push @article_ids, $rs->id;
557             }
558              
559             # Allow other commands in the command line to operate on the list of
560             # articles added.
561             return @article_ids;
562             }
563              
564             sub _read_lines_from_stdin
565             {
566             # Return an list of arguments by reading lines from STDIN
567              
568             my @args;
569             while (<STDIN>)
570             {
571             $_ =~ s/^\s+|\s+$//g ; # remove leading and trailing whitespace
572             next if ($_ eq ''); # ignore blank lines
573             push(@args, $_);
574             }
575             return @args;
576             }
577              
578              
579             sub show
580             {
581             my $self = shift;
582             my $output = shift;
583             my @args = @_;
584              
585             return unless $self->validate_article_ids(@args);
586              
587             $self->run(0);
588             my $tempdir;
589              
590             $tempdir = Path::Tiny->tempdir("zapzi-article-XXXXX", TMPDIR => 1)
591             if $output eq 'browser';
592              
593             for (@args)
594             {
595             my $article_text = App::Zapzi::Articles::export_article($_);
596             if (! $article_text)
597             {
598             print "Could not get article $_\n\n";
599             $self->run(1);
600             next;
601             }
602              
603             if ($output ne 'browser')
604             {
605             print $article_text, "\n\n";
606             next;
607             }
608              
609             # Send the article to a temp file and view in a browser
610             my $tempfile = "$tempdir/$_.html";
611             open my $fh, '>:encoding(UTF-8)', $tempfile
612             or die "Can't open temporary file: $!\n";
613             print {$fh} $article_text;
614             close $fh;
615              
616             my $rc = Browser::Open::open_browser($tempfile);
617             if (!defined($rc))
618             {
619             print "Could not open browser";
620             $self->run(1);
621             next;
622             }
623             }
624             }
625              
626              
627             sub move
628             {
629             my $self = shift;
630             my @args = @_;
631              
632             $self->run(0);
633              
634             my $folder = pop @args;
635             if (! $folder || ! App::Zapzi::Folders::get_folder($folder))
636             {
637             print "Need to supply a valid folder name as last argument\n";
638             $self->run(1);
639             return;
640             }
641              
642             return unless $self->validate_article_ids(@args);
643              
644             my @moved;
645             for (@args)
646             {
647             my $id = $_;
648             my $article = App::Zapzi::Articles::get_article($id);
649             if (! $article)
650             {
651             print "Could not get article $id\n";
652             $self->run(1);
653             }
654             else
655             {
656             App::Zapzi::Articles::move_article($id, $folder);
657             push @moved, $_;
658             }
659             }
660              
661             if (@moved)
662             {
663             print "Moved articles @moved to '$folder'\n";
664             }
665             }
666              
667              
668             sub publish
669             {
670             my $self = shift;
671             my @args = @_;
672              
673             if ($self->distribute)
674             {
675             push @args, App::Zapzi::UserConfig::get('distribution_destination');
676             if (scalar @args == 0)
677             {
678             print "You need to provide an argument to the distribute command\n";
679             $self->run(1);
680             return;
681             }
682             }
683             elsif (@args)
684             {
685             print "Invalid publish command arguments\n";
686             $self->run(1);
687             return;
688             }
689              
690             $self->run(0);
691             my $articles = App::Zapzi::Articles::get_articles($self->folder);
692             my $count = $articles->count;
693              
694             if ($count == 0)
695             {
696             print "No articles in '", $self->folder, "' to publish\n";
697             $self->run(1);
698             return;
699             }
700              
701             printf("Publishing '%s' - %d articles\n", $self->folder, $count);
702              
703             my $pub = App::Zapzi::Publish->
704             new(folder => $self->folder,
705             format => $self->format,
706             encoding => $self->encoding,
707             archive_folder => $self->noarchive ? undef : 'Archive');
708              
709             if (! $pub->publish())
710             {
711             print "Failed to publish ebook\n";
712             $self->run(1);
713             return;
714             }
715              
716             if ($self->distribute && lc($self->distribute) ne 'nothing')
717             {
718             print "Published ", path($pub->filename)->basename, "\n";
719             my $dist = App::Zapzi::Distribute->
720             new(file => $pub->filename,
721             method => $self->distribute,
722             destination => $args[0]);
723              
724             if ($dist->distribute())
725             {
726             print "Distributed OK: " . $dist->completion_message . "\n";
727             }
728             else
729             {
730             print "Distribution error: " . $dist->completion_message . "\n";
731             print "Original file can still be found in " .
732             $pub->filename . "\n";
733             $self->run(1);
734             }
735              
736             }
737             else
738             {
739             print "Published ", $pub->filename, "\n";
740             }
741             }
742              
743              
744             sub help
745             {
746             my $self = shift;
747              
748             print << 'EOF';
749             $ zapzi help | h
750             Shows this help text.
751              
752             $ zapzi version | v
753             Shows version information.
754              
755             $ zapzi init [--force]
756             Initialises new zapzi database. Will not create a new database
757             if one exists already unless you set --force.
758              
759             $ zapzi config get [KEYS]
760             Prints configuration variables specified by KEYS, or all config
761             variables if KEYS not provided.
762              
763             $ zapzi config set KEY VALUE
764             Set configuration variable KEY to VALUE.
765              
766             $ zapzi add [-t TRANSFORMER] FILE | URL | POD | -
767             Adds article to database. Accepts multiple file names or URLs.
768             or read articles names from standard input with -
769             TRANSFORMER determines how to extract the text from the article
770             and can be HTML, HTMLExtractMain, POD or TextMarkdown
771             If not specified, Zapzi will choose the best option based on the
772             content type of the article.
773              
774             $ zapzi list | ls [-f FOLDER] [-l | --long]
775             Lists articles in FOLDER, one line per article. The -l option shows
776             a more detailed listing.
777              
778             $ zapzi list-folders | lsf
779             Lists a summary of all folders.
780              
781             $ zapzi make-folder | mkf | md FOLDER
782             Makes a new folder.
783              
784             $ zapzi delete-folder | rmf | rd FOLDER
785             Remove a folder and all articles in it.
786              
787             $ zapzi delete-article | delete | rm ID
788             Removes article ID.
789              
790             $ zapzi move | mv ARTICLES FOLDER
791             Move one or more articles to the given folder.
792              
793             $ zapzi export | cat ID
794             Prints content of readable article to STDOUT.
795              
796             $ zapzi show | view ID
797             Opens a browser to view the readable text of article ID.
798              
799             $ zapzi publish | pub [-f FOLDER] [--format FORMAT]
800             [--encoding ENC] [--noarchive]
801             [--distribute METHOD DESTINATION]
802             Publishes articles in FOLDER to an eBook.
803             Format can be specified as MOBI, EPUB or HTML.
804             Will archive articles unless --noarchive is set.
805             Optionally distribute using METHOD to DESTINATION.
806             EOF
807              
808             $self->run(0);
809             }
810              
811              
812             sub version
813             {
814             my $self = shift;
815              
816             my $v = "dev";
817             no strict 'vars'; ## no critic - $VERSION does not exist in dev
818             $v = "$VERSION" if defined $VERSION;
819              
820             print "App::Zapzi $v and Perl $]\n";
821             print "Database schema version ", $self->database->get_version, "\n";
822             $self->run(0);
823             }
824              
825             1;
826              
827             __END__
828              
829             =pod
830              
831             =encoding UTF-8
832              
833             =head1 NAME
834              
835             App::Zapzi - store articles and publish them to read later
836              
837             =head1 VERSION
838              
839             version 0.017
840              
841             =head1 DESCRIPTION
842              
843             This class implements the application functions for Zapzi. See the
844             page for the L<zapzi> command for details on how to run it.
845              
846             =head1 ATTRIBUTES
847              
848             =head2 run
849              
850             The current state of the application, -1 means nothing has been done,
851             0 OK, otherwise an error code. Used for exit code when the process
852             terminates.
853              
854             =head2 force
855              
856             Option to force processing of the init command. Default is unset.
857              
858             =head2 noarchive
859              
860             Option to not archive articles on publication
861              
862             =head2 long
863              
864             Option to present a detailed listing
865              
866             =head2 folder
867              
868             Folder to work on. Default is 'Inbox'
869              
870             =head2 transformer
871              
872             Transformer to extract text from the article. Default is '', which
873             means Zapzi will automatically the best option based on the content
874             type of the text.
875              
876             =head2 format
877              
878             Format to publish a collection of folder articles in.
879              
880             =head2 encoding
881              
882             Encoding to publish a collection of folder articles in. Zapzi will
883             select the best encoding for the content and publication format if not
884             specified.
885              
886             =head2 distribute
887              
888             Method to distribute a published eBook - eg copy, script, email.
889              
890             =head2 zapzi_dir
891              
892             The folder where Zapzi files are stored.
893              
894             =head2 zapzi_ebook_dir
895              
896             The folder where Zapzi published eBook files are stored.
897              
898             =head2 database
899              
900             The instance of App:Zapzi::Database used by the application.
901              
902             =head2 test_database
903              
904             If set, use an in-memory database. Used to speed up testing only.
905              
906             =head2 interactive
907              
908             If set, this is an interactive session where Zapzi can prompt the user
909             for input.
910              
911             =head1 METHODS
912              
913             =head2 get_app
914             =method BUILD
915              
916             At construction time, a copy of the application object is stored and
917             can be retrieved later via C<get_app>.
918              
919             =head2 process_args(@args)
920              
921             Read the arguments C<@args> (normally you'd pass in C<@ARGV> and
922             process them according to the command line specification for the
923             application.
924              
925             =head2 init
926              
927             Creates the database. Will only do so if the database does not exist
928             already or if the L<force> attribute is set.
929              
930             =head2 init_config
931              
932             On initialiseation, ask the user for settings for configuration
933             variables. Will not ask if this is being run non-interactively.
934              
935             =head2 config(@args)
936              
937             Get or set configuration variables.
938              
939             If args is 'get' will list out all variables and their values. If args
940             is 'get x' will list the value of variable x. If args is 'set x y'
941             will set the value of x to be y.
942              
943             =head2 validate_folder
944              
945             Determines if the folder specified exists.
946              
947             =head2 validate_article_ids(@args)
948              
949             Determines if @args could be article IDs.
950              
951             =head2 list
952              
953             Lists out the articles in L<folder>.
954              
955             =head2 list_folders
956              
957             List folder names and article counts.
958              
959             =head2 make_folder
960              
961             Create one or more new folders. Will ignore any folders that already
962             exist.
963              
964             =head2 delete_folder
965              
966             Remove one or more new folders. Will not allow removal of system
967             folders ie Inbox and Archive, but will ignore removal of folders that
968             do not exist.
969              
970             =head2 delete_article
971              
972             Remove an article from the database
973              
974             =head2 add
975              
976             Add an article to the database for later publication.
977              
978             =head2 show(output, articles)
979              
980             Exports article text. If C<output> is 'browser' then will start a
981             browser to view the article, otherwise it will print to STDOUT.
982              
983             =head2 move
984              
985             Move one or more articles to a folder.
986              
987             =head2 publish
988              
989             Publish a folder of articles to an eBook
990              
991             =head2 help
992              
993             Displays help text.
994              
995             =head2 version
996              
997             Displays version information.
998              
999             =head1 AUTHOR
1000              
1001             Rupert Lane <rupert@rupert-lane.org>
1002              
1003             =head1 COPYRIGHT AND LICENSE
1004              
1005             This software is copyright (c) 2015 by Rupert Lane.
1006              
1007             This is free software; you can redistribute it and/or modify it under
1008             the same terms as the Perl 5 programming language system itself.
1009              
1010             =cut