File Coverage

blib/lib/WWW/eiNetwork.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package WWW::eiNetwork;
2              
3 1     1   592 use strict;
  1         2  
  1         29  
4 1     1   5 use warnings;
  1         2  
  1         22  
5 1     1   5 use Carp;
  1         5  
  1         89  
6 1     1   1696 use HTML::TableContentParser;
  0            
  0            
7             use WWW::Mechanize;
8              
9             our $VERSION = '0.02';
10              
11             sub new
12             {
13             my ($class, %args) = @_;
14              
15             croak "You must specify your library card number" unless $args{card_number};
16             croak "You must specify your PIN number" unless $args{pin_number};
17              
18             # Strip trailing slash from URL prefix
19             my $prefix = $args{url_prefix} || '';
20             $prefix =~ s/\/$//;
21              
22             my $self =
23             {
24             card_number => $args{card_number},
25             pin_number => $args{pin_number},
26             url_prefix => $prefix || 'https://iiisy1.einetwork.net/patroninfo~S1',
27             };
28              
29             bless $self, $class;
30             return $self;
31             };
32              
33             sub _login
34             {
35             my ($self, $name, $card) = @_;
36              
37             my $mech = WWW::Mechanize->new;
38             $mech->get($self->{url_prefix});
39             $mech->form_with_fields(qw(code pin));
40             $mech->field('code', $self->{card_number});
41             $mech->field('pin', $self->{pin_number});
42             $mech->click('submit');
43              
44             my $uri = $mech->uri;
45             if ($uri =~ /patroninfo~S1\/(\d+)\//)
46             {
47             $self->{patron_id} = $1;
48             $self->{mech} = $mech;
49             return $self->{mech};
50             }
51             else
52             {
53             croak "Couldn't log in to eiNetwork!";
54             return;
55             }
56             }
57              
58             sub holds
59             {
60             my ($self, %args) = @_;
61              
62             my @classes = ('Title', 'Status', 'Pickup', 'Cancel');
63             my @items = $self->_get_content(
64             page => 'holds',
65             classes => \@classes,
66             html => $args{html},
67             );
68              
69             return wantarray ? @items : \@items;
70             }
71              
72             sub items
73             {
74             my ($self, %args) = @_;
75              
76             my @classes = ('Title', 'Barcode', 'Status', 'CallNo');
77             my @items = $self->_get_content(
78             page => 'items',
79             classes => \@classes,
80             html => $args{html},
81             );
82              
83             return wantarray ? @items : \@items;
84             }
85              
86             sub _get_content
87             {
88             my ($self, %args) = @_;
89              
90             my $page = $args{page};
91             my $classes = $args{classes};
92             my $html = $args{html};
93              
94             # Hack to facilitate unit tests
95             $html ||= $self->_get_html($page);
96              
97             my $tables = $self->_get_tables($html);
98              
99             my @items;
100             for my $table (@$tables)
101             {
102             next unless ($table->{class} and $table->{class} eq 'patFunc');
103             for my $row (@{$table->{rows}})
104             {
105             next unless ($row->{class} and $row->{class} eq 'patFuncEntry');
106             my %record;
107             for my $cell (@{$row->{cells}})
108             {
109             for my $class (@$classes)
110             {
111             if ($cell->{class} and $cell->{class} eq "patFunc$class")
112             {
113             my $data = $self->_cleanup_data($cell->{data});
114             $record{lc($class)} = $data;
115             next;
116             }
117             }
118             }
119            
120             push @items, \%record;
121             }
122             }
123            
124             return wantarray ? @items : \@items;
125             }
126              
127             sub _get_html
128             {
129             my ($self, $page) = @_;
130              
131             $self->_login or croak "Couldn't log in!";
132              
133             my $mech = $self->{mech};
134             return unless $mech;
135              
136             my $patron_id = $self->{patron_id};
137             return unless $patron_id;
138              
139             my $prefix = $self->{url_prefix};
140             return unless $prefix;
141              
142             $mech->get("$prefix/$patron_id/$page");
143             return $mech->content;
144             }
145              
146             sub _get_tables
147             {
148             my ($self, $html) = @_;
149              
150             my $tp = HTML::TableContentParser->new();
151             my $tables = $tp->parse($html);
152             return $tables;
153             }
154              
155             sub _cleanup_data
156             {
157             my ($self, $data) = @_;
158            
159             # If the result is a link, strip the link tags and use the title.
160             # Not the greatest regex, but works for these simple cases.
161             if ($data =~ /"\>\s*(.*)\s*<\/a>/m)
162             {
163             $data = $1;
164             }
165            
166             # If the data is a select and there's something selected, use the
167             # title of the selected option.
168             if ($data =~ /\
169             {
170             $data =~ /selected="selected">\s*(.*)\s*<\/option>/m;
171             $data = $1;
172             }
173              
174             # Remove leading and trailing whitespace.
175             $data =~ s/^\s*//;
176             $data =~ s/\s*$//;
177              
178             return $data;
179             }
180              
181             1;
182              
183              
184             =head1 NAME
185              
186             WWW::eiNetwork - Perl interface to Allegheny County, PA libraries
187              
188             =head1 SYNOPSIS
189              
190             use WWW::eiNetwork;
191              
192             my $ein = WWW::eiNetwork->new(
193             card_number => '23456000000000',
194             pin_number => '1234',
195             url_prefix => 'https://iiisy1.einetwork.net/patroninfo~S1', #optional
196             );
197              
198             my @holds = $ein->holds;
199             my @items = $ein->items;
200              
201             for my $hold (@holds)
202             {
203             print qq(
204             Title: $hold->{title}
205             Status: $hold->{status}
206             Pickup at: $hold->{pickup}
207             Cancel if not picked up by: $hold->{cancel}\n\n
208             );
209             }
210            
211             for my $item (@items)
212             {
213             print qq(
214             Title: $item->{title}
215             Barcode: $item->{barcode}
216             Status: $item->{status}
217             CallNo: $item->{callno}\n\n
218             );
219             }
220            
221             =head1 DESCRIPTION
222              
223             This module provides an object-oriented Perl interface to eiNetwork libraries in Allegheny County, Pennsylvania.
224              
225             =head1 DEPENDENCIES
226              
227             WWW::Mechanize, HTML::TableContentParser, Crypt::SSLeay or IO::Socket::SSL
228              
229             =head1 BUGS
230              
231             The eiNetwork doesn't provide a public API - this module uses screen scraping to pull data directly from the HTML on their site. While I made an effort to code this module in such a way that small changes to the site layout and table arrangement won't break the module, any number of changes to the EIN's site could break this module.
232              
233             =head1 DISCLAIMER
234              
235             The author of this module is not affiliated in any way with the EINetwork or any Allegheny County library.
236              
237             =head1 ACKNOWLEDGMENTS
238              
239             Thanks to:
240              
241             Adam Foxson (L) for the great newbie's tutorial to contributing to CPAN at the Pittsburgh Perl Workshop (L).
242              
243             Bob O'Neill (L) for sharing his CPAN know-how.
244              
245             =head1 COPYRIGHT AND LICENSE
246              
247             Copyright (C) 2008 Michael Aquilina. All rights reserved.
248              
249             This code is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
250              
251             =head1 AUTHOR
252              
253             Michael Aquilina, aquilina@cpan.org
254              
255             =cut