package App::Zapzi;
# ABSTRACT: store articles and publish them to read later
use utf8;
use strict;
use warnings;
our $VERSION = '0.008'; # VERSION
binmode(STDOUT, ":encoding(UTF-8)");
use Browser::Open;
use Getopt::Lucid 1.05 qw( :all );
use File::HomeDir;
use File::Temp;
use App::Zapzi::Database;
use App::Zapzi::Folders;
use App::Zapzi::Articles;
use App::Zapzi::FetchArticle;
use App::Zapzi::Transform;
use App::Zapzi::Publish;
use Moo 1.003000;
use Carp;
has run => (is => 'rw', default => -1);
has force => (is => 'rw', default => 0);
has noarchive => (is => 'rw', default => 0);
has folder => (is => 'rw', default => 'Inbox');
has transformer => (is => 'rw', default => '');
our $_the_app;
sub BUILD { $_the_app = shift; }
sub get_app
{
croak 'Must create an instance of App::Zapzi first' unless $_the_app;
return $_the_app;
}
has zapzi_dir =>
(
is => 'ro',
default => sub
{
return $ENV{ZAPZI_DIR} // File::HomeDir->my_home . "/.zapzi";
}
);
has zapzi_ebook_dir =>
(
is => 'ro',
default => sub
{
my $self = shift;
return $self->zapzi_dir . '/ebooks';
}
);
has database =>
(
is => 'ro',
default => sub
{
my $self = shift;
return App::Zapzi::Database->new(app => $self);
}
);
has test_database =>
(
is => 'ro',
default => 0
);
sub process_args
{
my $self = shift;
my @args = @_;
my @specs =
(
Switch("help|h"),
Switch("version|v"),
Switch("init"),
Switch("add"),
Switch("list|ls"),
Switch("list-folders|lsf"),
Switch("make-folder|mkf|md"),
Switch("delete-folder|rmf|rd"),
Switch("delete-article|delete|rm"),
Switch("show|view"),
Switch("export|cat"),
Switch("publish|pub"),
Param("folder|f"),
Param("transformer|t"),
Switch("force"),
Switch("noarchive"),
);
my $options = Getopt::Lucid->getopt(\@specs, \@args)->validate;
$self->force($options->get_force);
$self->noarchive($options->get_noarchive);
$self->folder($options->get_folder // $self->folder);
$self->transformer($options->get_transformer // $self->transformer);
$self->help if $options->get_help;
$self->version if $options->get_version;
$self->init if $options->get_init;
# For any further operations we need a database
if (! -r $self->database->database_file && ! $self->test_database)
{
print "Zapzi database does not exist; did you run 'zapzi init'?\n";
$self->run(1);
return;
}
# Upgrade the DB, if needed
$self->database->upgrade
unless $self->database->check_version || $self->run == 0;
unless ($options->get_make_folder)
{
if (! $self->validate_folder($self->folder))
{
$self->run(1);
return;
}
}
$self->list if $options->get_list;
$self->list_folders if $options->get_list_folders;
$self->make_folder(@args) if $options->get_make_folder;
$self->delete_folder(@args) if $options->get_delete_folder;
$self->delete_article(@args) if $options->get_delete_article;
$self->add(@args) if $options->get_add;
$self->show('browser', @args) if $options->get_show;
$self->show('stdout', @args) if $options->get_export;
$self->publish if $options->get_publish;
# Fallthrough if no valid commands given
$self->help if $self->run == -1;
}
sub init
{
my $self = shift;
my $dir = $self->zapzi_dir;
if (! $dir || $dir eq '')
{
print "Zapzi directory not supplied\n";
$self->run(1);
}
elsif (-d $dir && ! $self->force)
{
print "Zapzi directory $dir already exists\n";
print "To force recreation, run with the --force option\n";
$self->run(1);
}
else
{
$self->database->init;
print "Created Zapzi directory $dir\n";
$self->run(0);
}
}
sub validate_folder
{
my $self = shift;
if (! App::Zapzi::Articles::get_folder($self->folder))
{
printf("Folder '%s' does not exist\n", $self->folder);
$self->run(1);
return;
}
else
{
return 1;
}
}
sub list
{
my $self = shift;
my $summary = App::Zapzi::Articles::articles_summary($self->folder);
foreach (@$summary)
{
my $article = $_;
printf("%s %4d %s %-45s\n", $self->folder,
$article->{id}, $article->{created}->strftime('%d-%b-%Y'),
$article->{title});
}
$self->run(0);
}
sub list_folders
{
my $self = shift;
my $summary = App::Zapzi::Folders::folders_summary();
foreach (sort keys %$summary)
{
printf("%-10s %3d\n", $_, $summary->{$_});
}
$self->run(0);
}
sub make_folder
{
my $self = shift;
my @args = @_;
if (! @args)
{
print "Need to provide folder names to create\n";
$self->run(1);
return;
}
$self->run(0);
for (@args)
{
my $folder = $_;
if (App::Zapzi::Folders::get_folder($folder))
{
print "Folder '$folder' already exists\n";
}
else
{
App::Zapzi::Folders::add_folder($folder);
print "Created folder '$folder'\n";
}
}
}
sub delete_folder
{
my $self = shift;
my @args = @_;
if (! @args)
{
print "Need to provide folder names to delete\n";
$self->run(1);
return;
}
$self->run(0);
for (@args)
{
my $folder = $_;
if (App::Zapzi::Folders::is_system_folder($folder))
{
print "Can't remove '$folder' as it is needed by the system\n";
}
elsif (! App::Zapzi::Folders::get_folder($folder))
{
print "Folder '$folder' does not exist\n";
}
else
{
App::Zapzi::Folders::delete_folder($folder);
print "Deleted folder '$folder'\n";
}
}
}
sub delete_article
{
my $self = shift;
my @args = @_;
if (! @args)
{
print "Need to provide article IDs\n";
$self->run(1);
return;
}
$self->run(0);
for (@args)
{
my $id = $_;
my $art_rs = App::Zapzi::Articles::get_article($id);
if ($art_rs)
{
if (App::Zapzi::Articles::delete_article($id))
{
print "Deleted article $id\n";
}
else
{
print "Could not delete article $id\n";
}
}
else
{
print "Could not get article $id\n";
$self->run(1);
}
}
}
sub add
{
my $self = shift;
my @args = @_;
if (! @args)
{
print "Need to provide articles names to add\n";
$self->run(1);
return;
}
$self->run(0);
for (@args)
{
my $source = $_;
print "Working on $source\n";
my $f = App::Zapzi::FetchArticle->new(source => $source);
if (! $f->fetch)
{
print "Could not get article: ", $f->error, "\n\n";
$self->run(1);
next;
}
my $tx = App::Zapzi::Transform->new(raw_article => $f,
transformer => $self->transformer);
if (! $tx->to_readable)
{
print "Could not transform article\n\n";
$self->run(1);
next;
}
printf("Got '%s' (%.1fkb)\n", $tx->title,
length($tx->readable_text) / 1024);
my $rs = App::Zapzi::Articles::add_article(title => $tx->title,
text => $tx->readable_text,
folder => $self->folder);
printf("Added article %d to folder '%s'\n\n", $rs->id, $self->folder);
}
}
sub show
{
my $self = shift;
my $output = shift;
my @args = @_;
if (! @args)
{
print "Need to provide article IDs\n";
$self->run(1);
return;
}
$self->run(0);
my $tempdir;
$tempdir = File::Temp->newdir("zapzi-article-XXXXX", TMPDIR => 1)
if $output eq 'browser';
for (@args)
{
my $article_text = App::Zapzi::Articles::export_article($_);
if (! $article_text)
{
print "Could not get article $_\n\n";
$self->run(1);
next;
}
if ($output ne 'browser')
{
print $article_text, "\n\n";
next;
}
# Send the article to a temp file and view in a browser
my $tempfile = "$tempdir/$_.html";
open my $fh, '>:encoding(UTF-8)', $tempfile
or die "Can't open temporary file: $!\n";
print {$fh} $article_text;
close $fh;
my $rc = Browser::Open::open_browser($tempfile);
if (!defined($rc))
{
print "Could not open browser";
$self->run(1);
next;
}
}
}
sub publish
{
my $self = shift;
$self->run(0);
my $articles = App::Zapzi::Articles::get_articles($self->folder);
my $count = $articles->count;
if ($count == 0)
{
print "No articles in '", $self->folder, "' to publish\n";
$self->run(1);
return;
}
printf("Publishing '%s' - %d articles\n", $self->folder, $count);
my $pub = App::Zapzi::Publish->
new(folder => $self->folder,
archive_folder => $self->noarchive ? undef : 'Archive');
if (! $pub->publish())
{
print "Failed to publish ebook\n";
$self->run(1);
return;
}
print "Published ", $pub->filename, "\n";
}
sub help
{
my $self = shift;
print << 'EOF';
$ zapzi help|h
Shows this help text
$ zapzi version|v
Show version information
$ zapzi init [--force]
Initialises new zapzi database. Will not create a new database
if one exists already unless you set --force.
$ zapzi add [-t TRANSFORMER] FILE | URL | POD
Adds article to database. Accepts multiple file names or URLs.
TRANSFORMER determines how to extract the text from the article
and can be HTML, HTMLExtractMain, POD or TextMarkdown
If not specified, Zapzi will choose the best option based on the
content type of the article.
$ zapzi list | ls [-f FOLDER]
Lists articles in FOLDER.
$ zapzi list-folders | lsf
Lists a summary of all folders.
$ zapzi make-folder | mkf | md FOLDER
Make a new folder.
$ zapzi delete-folder | rmf | rd FOLDER
Remove a folder and all articles in it.
$ zapzi delete-article | delete | rm ID
Removes article ID.
$ zapzi export | cat ID
Prints content of readable article to STDOUT
$ zapzi show | view ID
Opens a browser to view the readable text of article ID
$ zapzi publish | pub [-f FOLDER] [--noarchive]
Publishes articles in FOLDER to an eBook. Will archive articles unless
--noarchive is set.
EOF
$self->run(0);
}
sub version
{
my $self = shift;
my $v = "dev";
no strict 'vars'; ## no critic - $VERSION does not exist in dev
$v = "$VERSION" if defined $VERSION;
print "App::Zapzi $v and Perl $]\n";
print "Database schema version ", $self->database->get_version, "\n";
$self->run(0);
}
1;
__END__
=pod
=head1 NAME
App::Zapzi - store articles and publish them to read later
=head1 VERSION
version 0.008
=head1 DESCRIPTION
This class implements the application functions for Zapzi. See the
page for the L<zapzi> command for details on how to run it.
=head1 ATTRIBUTES
=head2 run
The current state of the application, -1 means nothing has been done,
0 OK, otherwise an error code. Used for exit code when the process
terminates.
=head2 force
Option to force processing of the init command. Default is unset.
=head2 noarchive
Option to not archive articles on publication
=head2 folder
Folder to work on. Default is 'Inbox'
=head2 transformer
Transformer to extract text from the article. Default is '', which
means Zapzi will automatically the best option based on the content
type of the text.
=head2 zapzi_dir
The folder where Zapzi files are stored.
=head2 zapzi_ebook_dir
The folder where Zapzi published eBook files are stored.
=head2 database
The instance of App:Zapzi::Database used by the application.
=head2 test_database
If set, use an in-memory database. Used to speed up testing only.
=head1 METHODS
=head2 get_app
=method BUILD
At construction time, a copy of the application object is stored and
can be retrieved later via C<get_app>.
=head2 process_args(@args)
Read the arguments C<@args> (normally you'd pass in C<@ARGV> and
process them according to the command line specification for the
application.
=head2 init
Creates the database. Will only do so if the database does not exist
already or if the L<force> attribute is set.
=head2 validate_folder
Determines if the folder specified exists.
=head2 list
Lists out the articles in L<folder>.
=head2 list_folders
List folder names and article counts.
=head2 make_folder
Create one or more new folders. Will ignore any folders that already
exist.
=head2 delete_folder
Remove one or more new folders. Will not allow removal of system
folders ie Inbox and Archive, but will ignore removal of folders that
do not exist.
=head2 delete_article
Remove an article from the database
=head2 add
Add an article to the database for later publication.
=head2 show(output, articles)
Exports article text. If C<output> is 'browser' then will start a
browser to view the article, otherwise it will print to STDOUT.
=head2 publish
Publish a folder of articles to an eBook
=head2 help
Displays help text.
=head2 version
Displays version information.
=head1 AUTHOR
Rupert Lane <rupert@rupert-lane.org>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2013 by Rupert Lane.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut