Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
282 lines (238 sloc) 7.76 KB
#!/usr/bin/env perl
#
# CLI for the Lawrence Public Library bibliocommons site.
# Current features:
# * list all checked out books for a user
#
# Create a ~/.lfk-library.yml file with entries
# for each user credential set you want to query.
# Example:
# --
# patron_name:
# username: foo
# password: bar
#
# Released to public domain by author Peter Karman, 2016.
#
# Installation requires:
#
# % cpan WWW::Mechanize YAML::Tiny HTML::TreeBuilder::XPath
# % cpan Text::Table::Any Text::Table::Tiny JSON IO::Prompt::Simple
# % cpan LWP::Protocol::https Text::Table::HTML Email::Stuffer
use strict;
use warnings;
use v5.10;
use Carp;
use Data::Dump qw( dump );
use Getopt::Long;
use WWW::Mechanize;
use YAML::Tiny;
use HTML::TreeBuilder::XPath;
use Text::Table::Any;
use JSON;
use IO::Prompt::Simple;
use Email::Stuffer;
my $usage = "$0 [--all | patron_name] [--renew]\n";
my $CHECK_ALL;
my $RENEW;
my $VERBOSE;
my $PROMPT = 1;
my $HTML;
my $NOTIFY;
GetOptions(
'html' => \$HTML,
"all" => \$CHECK_ALL,
"renew" => \$RENEW,
"prompt!" => \$PROMPT,
"verbose" => \$VERBOSE,
"notify=s" => \$NOTIFY,
) or die $usage;
die $usage unless $CHECK_ALL or @ARGV;
my $conf_file = "$ENV{HOME}/.lfk-library.yml";
my $config = YAML::Tiny->read($conf_file)->[0];
my @patrons = $CHECK_ALL ? ( sort keys %$config ) : @ARGV;
my $base_url = 'https://lawrence.bibliocommons.com';
my $login_url = "$base_url/user/login?destination=https://lplks.org";
my $checked_out_url = "$base_url/checkedout";
my $TABLE_HEADER = [ '#', 'Title', 'Due Date', 'Holds', 'eBook' ];
# global var for queuing overdue or due soon items
my @NOTIFY_QUEUE = ($TABLE_HEADER);
###########################################################################
# functions
sub notify {
my $body = Text::Table::Any::table(
rows => \@NOTIFY_QUEUE,
header_row => 1,
backend => table_backend(),
);
Email::Stuffer->to($NOTIFY)->from( $ENV{EMAIL} )
->subject( $ENV{SUBJECT} || 'LFK Library Due' )->html_body($body)
->send_or_die;
}
sub table_backend {
return 'Text::Table::HTML' if $HTML;
return 'Text::Table::Tiny';
}
sub login {
my ( $www, $who ) = @_;
$www->get($login_url);
$www->submit_form(
form_number => 4,
fields => {
name => $config->{$who}->{username},
user_pin => $config->{$who}->{password}
}
);
unless ( $www->success ) {
die "Failed to authenticate: " . $www->status;
}
}
sub checked_out_items {
my ($www) = @_;
$www->get($checked_out_url);
my $html_tree
= HTML::TreeBuilder::XPath->new_from_content( $www->content );
return $html_tree->findnodes('//div[contains(@class, "listItem")]');
}
sub build_report_table {
my ( $www, $checked_out ) = @_;
my $count = 0;
my @rows = ( [@$TABLE_HEADER] );
push @{ $rows[0] }, 'Renewed' if $RENEW;
for my $item ( $checked_out->get_nodelist ) {
# digital checkouts are skipped
next if $item->as_HTML =~ m/EPUB|MP3/;
#say $item->as_HTML;
my $biblio = $item->find_by_attribute( 'class', 'primary_bib_info' );
#say $biblio->as_HTML;
my $item_link = $biblio->look_down( '_tag' => 'a' );
if ( !$item_link ) {
say "No item link for " . $item->as_HTML;
next;
}
my $title = $item_link->as_trimmed_text;
my $format_div = $biblio->find_by_attribute( 'class', 'format' );
my $is_ebook = $format_div->as_HTML =~ m/eBook/;
#say $title;
my $pending_holds
= $item->find_by_attribute( 'class', 'checkedout_num_waiting' );
if ($pending_holds) {
$pending_holds = $pending_holds->as_trimmed_text;
}
else {
# some caching bug on site prevents holds from always showing up
# on the summary list page. So check the item subscription stats page.
my $href = $item_link->attr('href');
my ($item_id) = ( $href =~ m,/item/show/(\d+)_, );
#say $href;
#say $item_id;
my $url = sprintf( "%s/item/show_circulation_widget/%s.json",
$base_url, $item_id );
#say $url;
my $resp = $www->get($url);
my $resp_meta = decode_json $resp->decoded_content;
my $tree = HTML::TreeBuilder::XPath->new_from_content(
$resp_meta->{'html'} );
my $holds
= $tree->find_by_attribute( 'testid', 'text_holdcopies' );
$holds = $holds->as_trimmed_text if $holds;
#say "holds: $holds";
$pending_holds = $holds if $holds;
}
my $due_div
= $item->find_by_attribute( 'class', 'checkedout_due_date' );
#say $due_div->as_HTML;
my $duedate
= $due_div->find_by_attribute( 'class', 'checkedout_status out' );
my $duedate_soon = $due_div->find_by_attribute( 'class',
'checkedout_status coming_due' );
my $duedate_overdue = $due_div->find_by_attribute( 'class',
'checkedout_status overdue' );
my $due = $duedate || $duedate_soon || $duedate_overdue;
my $due_when = $due->as_trimmed_text( extra_chars => '\xA0' );
#say $due_when;
my $row = [
++$count, $title,
$due_when, $pending_holds,
( $is_ebook ? 'x' : '' )
];
if ( $NOTIFY
&& $pending_holds
&& ( $duedate_soon || $duedate_overdue ) )
{
push @NOTIFY_QUEUE, $row;
}
if ( !$pending_holds
&& $RENEW
&& ( $duedate_soon || $duedate_overdue ) )
{
my $confirmation;
if ($PROMPT) {
$confirmation = prompt "renew $title (due $due_when)",
{ anyone => [qw/y n/] };
}
else {
$confirmation = 'y';
}
if ( $confirmation eq 'n' ) {
push @$row, 'skipped renewal';
}
elsif ( renew_item( $www, $item ) ) {
push @$row, 'renewed';
}
else {
push @$row, 'renewal failed';
push @NOTIFY_QUEUE, $row;
}
}
push @rows, $row;
}
return \@rows;
}
sub renew_item {
my ( $www, $item ) = @_;
my $renewal_link = $item->find_by_attribute( 'class',
'btn btn-link single_circ_action' );
#say $renewal_link->as_HTML;
if (!$renewal_link) {
warn "No Renewal Link for $item";
return;
}
my $renewal_url = $base_url . $renewal_link->attr('href');
#say $renewal_url;
my $resp = $www->get($renewal_url);
my $response = $resp->decoded_content;
#say $response;
return if $response =~ /overdrive\.com/;
my $renewal_meta = decode_json $response;
$www->update_html( $renewal_meta->{html} );
$response = $www->submit_form();
my $renewed_resp = $response->decoded_content;
if ( $renewed_resp
=~ m/This item has been renewed the maximum number of times allowed/ )
{
return 0;
}
#say $www->success;
#say $response->decoded_content;
return $www->success;
}
#########################################################################
# main loop
say "<h1>LFK Library Report</h1>" if $HTML;
for my $who (@patrons) {
my $www = WWW::Mechanize->new;
login( $www, $who );
my $checked_out = checked_out_items($www);
my $rows = build_report_table( $www, $checked_out );
say $HTML ? "<h2>$who</h2>" : $who;
next unless @$rows > 1;
print Text::Table::Any::table(
rows => $rows,
header_row => 1,
backend => table_backend(),
);
}
if ( $NOTIFY && scalar(@NOTIFY_QUEUE) > 1 ) {
notify();
}