]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OneClickdigital.pm
LP#1541559: ebook API handler for OneClickdigital
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / EbookAPI / OneClickdigital.pm
1 #!/usr/bin/perl
2
3 # Copyright (C) 2015 BC Libraries Cooperative
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18
19 package OpenILS::Application::EbookAPI::OneClickdigital;
20
21 use strict;
22 use warnings;
23
24 use OpenILS::Application;
25 use OpenILS::Application::EbookAPI;
26 use base qw/OpenILS::Application::EbookAPI/;
27 use OpenSRF::AppSession;
28 use OpenSRF::EX qw(:try);
29 use OpenSRF::Utils::SettingsClient;
30 use OpenSRF::Utils::Logger qw($logger);
31 use OpenSRF::Utils::Cache;
32 use OpenILS::Application::AppUtils;
33 use Data::Dumper;
34
35 sub new {
36     my( $class, $args ) = @_;
37     $class = ref $class || $class;
38     return bless $args, $class;
39 }
40
41 sub ou {
42     my $self = shift;
43     return $self->{ou};
44 }
45
46 sub vendor {
47     my $self = shift;
48     return $self->{vendor};
49 }
50
51 sub session_id {
52     my $self = shift;
53     return $self->{session_id};
54 }
55
56 sub base_uri {
57     my $self = shift;
58     return $self->{base_uri};
59 }
60
61 sub library_id {
62     my $self = shift;
63     return $self->{library_id};
64 }
65
66 sub basic_token {
67     my $self = shift;
68     return $self->{basic_token};
69 }
70
71 sub patron_id {
72     my $self = shift;
73     return $self->{patron_id};
74 }
75
76 sub initialize {
77     my $self = shift;
78     my $ou = $self->{ou};
79
80     $self->{base_uri} = 'https://api.oneclickdigital.us/v1'; # TODO pull from org setting?
81
82     my $library_id = OpenILS::Application::AppUtils->ou_ancestor_setting_value($ou, 'ebook_api.oneclickdigital.library_id');
83     if ($library_id) {
84         $self->{library_id} = $library_id;
85     } else {
86         $logger->error("EbookAPI: no OneClickdigital library ID found for org unit $ou");
87         return;
88     }
89
90     my $basic_token = OpenILS::Application::AppUtils->ou_ancestor_setting_value($ou, 'ebook_api.oneclickdigital.basic_token');
91     if ($basic_token) {
92         $self->{basic_token} = $basic_token;
93     } else {
94         $logger->error("EbookAPI: no OneClickdigital basic token found for org unit $ou");
95         return;
96     }
97
98     return $self;
99
100 }
101
102 # OneClickdigital API does not require separate client auth;
103 # we just need to include our basic auth token in requests
104 sub do_client_auth {
105     my $self = shift;
106     return;
107 }
108
109 # retrieve OneClickdigital patron ID (if any) based on patron barcode
110 # GET http://api.oneclickdigital.us/v1/rpc/libraries/{libraryID}/patrons/{barcode}
111 sub do_patron_auth {
112     my ($self, $barcode) = @_;
113     my $base_uri = $self->{base_uri};
114     my $library_id = $self->{library_id};
115     my $session_id = $self->{session_id};
116     my $req = {
117         method => 'GET',
118         uri    => "$base_uri/rpc/libraries/$library_id/patrons/$barcode"
119     };
120     my $res = $self->request($req, $session_id);
121     # TODO distinguish between unregistered patrons and patron auth failure
122     if (defined ($res) && $res->{content}->{patronId}) {
123         return $res->{content}->{patronId};
124     }
125     $logger->error("EbookAPI: no OneClickdigital patron ID found for barcode $barcode");
126     return;
127 }
128
129 # does this title have available "copies"? y/n
130 # GET http://api.oneclickdigital.us/v1/libraries/{libraryID}/media/{isbn}/availability
131 sub do_availability_lookup {
132     my ($self, $isbn) = @_;
133     my $base_uri = $self->{base_uri};
134     my $library_id = $self->{library_id};
135     my $session_id = $self->{session_id};
136     my $req = {
137         method => 'GET',
138         uri    => "$base_uri/libraries/$library_id/media/$isbn/availability"
139     };
140     my $res = $self->request($req, $session_id);
141     if (defined ($res)) {
142         $logger->info("EbookAPI: received availability response for ISBN $isbn: " . Dumper $res);
143         return $res->{content}->{availability};
144     } else {
145         $logger->error("EbookAPI: could not retrieve OneClickdigital availability for ISBN $isbn");
146         return;
147     }
148 }
149
150 # OneClickdigital API does not support detailed holdings lookup,
151 # so we return basic availability information.
152 sub do_holdings_lookup {
153     my ($self, $isbn) = @_;
154     my $avail = $self->do_availability_lookup($isbn);
155     return { available => $avail };
156 }
157
158 # checkout an item to a patron
159 # item is identified by ISBN, patron ID is their barcode
160 # POST //api.{domain}/v1/libraries/{libraryId}/patrons/{patronId}/checkouts/{isbn}
161 sub checkout {
162     my ($self, $isbn, $patron_id) = @_;
163     my $base_uri = $self->{base_uri};
164     my $library_id = $self->{library_id};
165     my $session_id = $self->{session_id};
166     my $req = {
167         method => 'POST',
168         uri    => "$base_uri/libraries/$library_id/patrons/$patron_id/checkouts/$isbn"
169     };
170     my $res = $self->request($req, $session_id);
171
172     # TODO: more sophisticated response handling
173     # HTTP 200 response indicates success, HTTP 409 indicates checkout limit reached
174     if (defined ($res)) {
175         if ($res->{is_success}) {
176             return {
177                 xact_id => $res->{content}->{transactionId},
178                 due_date => $res->{content}->{expiration}
179             };
180         } else {
181             $logger->error("EbookAPI: checkout failed for OneClickdigital title $isbn");
182             return { error_msg => $res->{content} };
183         }
184     } else {
185         $logger->error("EbookAPI: no response received from OneClickdigital server");
186         return;
187     }
188 }
189
190 # renew a checked-out item
191 # item id = ISBN, patron id = barcode
192 # PUT //api.{domain}/v1/libraries/{libraryId}/patrons/{patronId}/checkouts/{isbn}
193 sub renew {
194     my ($self, $isbn, $patron_id) = @_;
195     my $base_uri = $self->{base_uri};
196     my $library_id = $self->{library_id};
197     my $session_id = $self->{session_id};
198     my $req = {
199         method => 'PUT',
200         uri    => "$base_uri/libraries/$library_id/patrons/$patron_id/checkouts/$isbn"
201     };
202     my $res = $self->request($req, $session_id);
203
204     # TODO: more sophisticated response handling
205     # HTTP 200 response indicates success
206     if (defined ($res)) {
207         if ($res->{is_success}) {
208             return {
209                 xact_id => $res->{content}->{transactionId},
210                 due_date => $res->{content}->{expiration}
211             };
212         } else {
213             $logger->error("EbookAPI: renewal failed for OneClickdigital title $isbn");
214             return { error_msg => $res->{content} };
215         }
216     } else {
217         $logger->error("EbookAPI: no response received from OneClickdigital server");
218         return;
219     }
220 }
221
222 # checkin a checked-out item
223 # item id = ISBN, patron id = barcode
224 # XXX API docs indicate that a bearer token is required!
225 # DELETE //api.{domain}/v1/libraries/{libraryId}/patrons/{patronId}/checkouts/{isbn}
226 sub checkin {
227 }
228
229 sub place_hold {
230 }
231
232 sub cancel_hold {
233 }
234
235 # GET //api.{domain}/v1/libraries/{libraryId}/patrons/{patronId}/checkouts/all
236 sub get_patron_checkouts {
237     my ($self, $patron_id) = @_;
238     my $base_uri = $self->{base_uri};
239     my $library_id = $self->{library_id};
240     my $session_id = $self->{session_id};
241     my $req = {
242         method => 'GET',
243         uri    => "$base_uri/libraries/$library_id/patrons/$patron_id/checkouts/all"
244     };
245     my $res = $self->request($req, $session_id);
246
247     my $checkouts = [];
248     if (defined ($res)) {
249         $logger->info("EbookAPI: received response for OneClickdigital checkouts: " . Dumper $res);
250         foreach my $checkout (@{$res->{content}}) {
251             push @$checkouts, {
252                 xact_id => $checkout->{transactionID},
253                 title_id => $checkout->{isbn},
254                 due_date => $checkout->{expiration},
255                 download_url => $checkout->{downloadURL},
256                 title => $checkout->{title},
257                 author => $checkout->{authors}
258             };
259         };
260         $logger->info("EbookAPI: retrieved " . scalar(@$checkouts) . " OneClickdigital checkouts for patron $patron_id");
261         $self->{checkouts} = $checkouts;
262         return $self->{checkouts};
263     } else {
264         $logger->error("EbookAPI: failed to retrieve OneClickdigital checkouts for patron $patron_id");
265         return;
266     }
267 }
268
269 # GET //api.{domain}/v1/libraries/{libraryId}/patrons/{patronId}/holds/all
270 sub get_patron_holds {
271     my ($self, $patron_id) = @_;
272     my $base_uri = $self->{base_uri};
273     my $library_id = $self->{library_id};
274     my $session_id = $self->{session_id};
275     my $req = {
276         method => 'GET',
277         uri    => "$base_uri/libraries/$library_id/patrons/$patron_id/holds/all"
278     };
279     my $res = $self->request($req, $session_id);
280
281     my $holds = [];
282     if (defined ($res)) {
283         $logger->info("EbookAPI: received response for OneClickdigital holds: " . Dumper $res);
284         foreach my $hold (@{$res->{content}}) {
285             push @$holds, {
286                 xact_id => $hold->{transactionID},
287                 title_id => $hold->{isbn},
288                 expire_date => $hold->{expiration},
289                 title => $hold->{title},
290                 author => $hold->{authors},
291                 # XXX queue position/size and pending vs ready info not available via API
292                 queue_position => '-',
293                 queue_size => '-',
294                 is_ready => 0
295             };
296         };
297         $logger->info("EbookAPI: retrieved " . scalar(@$holds) . " OneClickdigital holds for patron $patron_id");
298         $self->{holds} = $holds;
299         return $self->{holds};
300     } else {
301         $logger->error("EbookAPI: failed to retrieve OneClickdigital holds for patron $patron_id");
302         return;
303     }
304 }
305