# - barcode: patron barcode
#
sub do_xact {
- my ($self, $conn, $auth, $session_id, $title_id, $barcode, $email) = @_;
+ my ($self, $conn, $auth, $session_id, $title_id, $barcode, $param) = @_;
my $action;
if ($self->api_name =~ /checkout/) {
# handler method constructs and submits request (and handles any external authentication)
my $res;
- # place_hold has email as optional additional param
- if ($action eq 'place_hold') {
- $res = $handler->place_hold($title_id, $user_token, $email);
+ if ($action eq 'checkout') {
+ # checkout has format as optional additional param
+ $res = $handler->checkout($title_id, $user_token, $param);
+ } elsif ($action eq 'place_hold') {
+ # place_hold has email as optional additional param
+ $res = $handler->place_hold($title_id, $user_token, $param);
} else {
$res = $handler->$action($title_id, $user_token);
}
}
);
+sub get_download_link {
+ my ($self, $conn, $auth, $session_id, $request_link) = @_;
+ my $handler = new_handler($session_id);
+ return $handler->do_get_download_link($request_link);
+}
+__PACKAGE__->register_method(
+ method => 'get_download_link',
+ api_name => 'open-ils.ebook_api.title.get_download_link',
+ api_level => 1,
+ argc => 3,
+ signature => {
+ desc => "Get download link for an OverDrive title that has been checked out",
+ params => [
+ {
+ name => 'authtoken',
+ desc => 'Authentication token',
+ type => 'string'
+ },
+ {
+ name => 'session_id',
+ desc => 'The session ID (provided by open-ils.ebook_api.start_session)',
+ type => 'string'
+ },
+ {
+ name => 'request_link',
+ desc => 'The URL used to request a download link',
+ type => 'string'
+ }
+ ],
+ return => {
+ desc => 'Success: { url => "http://example.com/download-link" } / Failure: { error_msg => "Download link request failed." }',
+ type => 'hashref'
+ }
+ }
+);
+
1;
};
if (my $res = $self->handle_http_request($req, $self->{session_id})) {
if ($res->{content}->{title}) {
- return {
+ my $info = {
title => $res->{content}->{title},
author => $res->{content}->{creators}[0]{name}
};
+ # Append format information (useful for checkouts).
+ $info->{formats} = $self->get_formats($title_id);
+ return $info;
} else {
$logger->error("EbookAPI: OverDrive metadata lookup failed for $title_id");
}
}
# request available formats
+ $holdings->{formats} = $self->get_formats($title_id);
+
+ return $holdings;
+}
+
+# Returns a list of available formats for a given title.
+sub get_formats {
+ my ($self, $title_id) = @_;
+ $self->do_client_auth() if (!$self->{bearer_token});
+ $self->get_library_info() if (!$self->{collection_token});
+ my $collection_token = $self->{collection_token};
+
+ my $formats = [];
+
my $format_req = {
method => 'GET',
uri => $self->{discovery_base_uri} . "/collections/$collection_token/products/$title_id/metadata"
if (my $format_res = $self->handle_http_request($format_req, $self->{session_id})) {
if ($format_res->{content}->{formats}) {
foreach my $f (@{$format_res->{content}->{formats}}) {
- push @{$holdings->{formats}}, $f->{name};
+ push @$formats, { id => $f->{id}, name => $f->{name} };
}
} else {
$logger->info("EbookAPI: OverDrive holdings format request for title $title_id contained no format information");
$logger->error("EbookAPI: failed to retrieve OverDrive holdings formats for title $title_id");
}
- return $holdings;
+ return $formats;
}
# POST https://patron.api.overdrive.com/v1/patrons/me/checkouts
# ],
# ...
# }
+#
+# Our return value looks like this:
+# {
+# due_date => "10/14/2013 10:56:00 AM",
+# formats => [
+# "ebook-overdrive" => "https://patron.api.overdrive.com/v1/patrons/me/checkouts/76C1B7D0-17F4-4C05-8397-C66C17411584/formats/ebook-overdrive/downloadlink?errorpageurl={errorpageurl}&odreadauthurl={odreadauthurl}",
+# ...
+# ]
+# }
sub checkout {
- my ($self, $title_id, $patron_token) = @_;
+ my ($self, $title_id, $patron_token, $format) = @_;
my $request_content = {
fields => [
{
}
]
};
+ if ($format) {
+ push @{$request_content->{fields}}, { name => 'formatType', value => $format };
+ }
my $req = {
method => 'POST',
uri => $self->{circulation_base_uri} . "/patrons/me/checkouts",
};
if (my $res = $self->handle_http_request($req, $self->{session_id})) {
if ($res->{content}->{expires}) {
- return { due_date => $res->{content}->{expires} };
+ my $checkout = { due_date => $res->{content}->{expires} };
+ if (defined $res->{content}->{formats}) {
+ my $formats = {};
+ foreach my $f (@{$res->{content}->{formats}}) {
+ my $ftype = $f->{formatType};
+ $formats->{$ftype} = $f->{linkTemplates}->{downloadLink}->{href};
+ }
+ $checkout->{formats} = $formats;
+ }
+ return $checkout;
}
$logger->error("EbookAPI: checkout failed for OverDrive title $title_id");
return { error_msg => ( (defined $res->{content}) ? $res->{content} : 'Unknown checkout error' ) };
foreach my $checkout (@{$res->{content}->{checkouts}}) {
my $title_id = $checkout->{reserveId};
my $title_info = $self->get_title_info($title_id);
- # TODO get download URL - need to "lock in" a format first, see OD Checkouts API docs
+ my $formats = {};
+ foreach my $f (@{$checkout->{formats}}) {
+ my $ftype = $f->{formatType};
+ $formats->{$ftype} = $f->{linkTemplates}->{downloadLink}->{href};
+ };
push @$checkouts, {
title_id => $title_id,
due_date => $checkout->{expires},
title => $title_info->{title},
- author => $title_info->{author}
+ author => $title_info->{author},
+ formats => $formats
}
};
$self->{checkouts} = $checkouts;
return $self->handle_http_request($req, $self->{session_id});
}
+# get download URL for checked-out title
+sub do_get_download_link {
+ my ($self, $request_link) = @_;
+ # Request links use the same domain as the circulation base URI, but they
+ # are apparently always plain HTTP. The request link still works if you
+ # use HTTPS instead. So, if our circulation base URI uses HTTPS, let's
+ # force the request link to HTTPS too, for two reasons:
+ # 1. A preference for HTTPS is implied by the library's circulation base
+ # URI setting.
+ # 2. The base URI of the request link has to match the circulation base URI
+ # (including the same protocol) in order for the handle_http_request()
+ # method above to automatically re-authenticate the patron, if required.
+ if ($self->{circulation_base_uri} =~ /^https:/) {
+ $request_link =~ s/^http:/https:/;
+ }
+ my $req = {
+ method => 'GET',
+ uri => $request_link
+ };
+ if (my $res = $self->handle_http_request($req, $self->{session_id})) {
+ if ($res->{content}->{links}->{contentlink}->{href}) {
+ return { url => $res->{content}->{links}->{contentlink}->{href} };
+ }
+ return { error_msg => ( (defined $res->{content}) ? $res->{content} : 'Could not get content link' ) };
+ }
+ $logger->error("EbookAPI: no response received from OverDrive server");
+ return;
+}
+
1;
# Patron ID or patron auth token, as returned by do_patron_auth().
my $user_token = shift;
+ # Ebook format to be checked out (optional, not used here).
+ my $format = shift;
+
# If checkout succeeds, the response is a hashref with the following fields:
# - due_date
# - xact_id (optional)
return $self->{holds};
}
+sub do_get_download_link {
+ my $self = shift;
+ my $request_link = shift;
+
+ # For some vendors (e.g. OverDrive), the workflow is as follows:
+ #
+ # 1. Perform a checkout.
+ # 2. Checkout response contains a URL which we use to request a
+ # format-specific download link for the checked-out title.
+ # 3. Submit a request to the request link.
+ # 4. Response contains a (temporary/dynamic) URL which the user
+ # clicks on to download the ebook in the desired format.
+ #
+ # For other vendors, the download link for a title is static and not
+ # format-dependent. In that case, we just return the original request link
+ # (but ideally the UI will skip the download link request altogether, since
+ # it's superfluous in that case).
+
+ return $request_link;
+}
#!perl
use strict; use warnings;
-use Test::More tests => 23; # XXX
+use Test::More tests => 24; # XXX
use OpenILS::Utils::TestUtils;
diag("Tests Ebook API");
my $checkout = $checkout_req->recv->content;
ok(exists $checkout->{due_date}, 'Ebook checked out');
+# open-ils.ebook_api.title.get_download_link
+my $request_link = 'http://example.com/ebookapi/t/003';
+my $download_link_req = $ebook_api->request(
+ 'open-ils.ebook_api.title.get_download_link', $authtoken, $session_id, $request_link);
+my $download_link = $download_link_req->recv->content;
+# Test module just returns the original request_link as the response.
+ok($download_link eq $request_link, 'Received download link for ebook');
+
# open-ils.ebook_api.renew
my $renew_req = $ebook_api->request(
'open-ils.ebook_api.renew', $authtoken, $session_id, '001', EBOOK_API_PATRON_USERNAME);
if (holdings.formats.length > 0) {
var formats_ul = dojo.create("ul", null, ebook.rec_id + '_formats');
dojo.forEach(holdings.formats, function(f) {
- dojo.create("li", { innerHTML: f }, formats_ul);
+ dojo.create("li", { innerHTML: f.name }, formats_ul);
});
var status_node = dojo.byId(ebook.rec_id + '_status');
var status_str = holdings.copies_available + ' of ' + holdings.copies_owned + ' available';
this.avail; // availability info for this title
this.holdings = {}; // holdings info
this.conns = {}; // references to Dojo event connection for performing actions with this ebook
-
}
Ebook.prototype.getDetails = function(callback) {
- var ses = dojo.cookie(this.vendor);
+ var ses = this.ses || dojo.cookie(this.vendor);
var ebook = this;
new OpenSRF.ClientSession('open-ils.ebook_api').request({
method: 'open-ils.ebook_api.title.details',
console.log('title details response: ' + resp.content());
ebook.title = resp.content().title;
ebook.author = resp.content().author;
+ if (typeof resp.content().formats !== 'undefined')
+ ebook.formats = resp.content().formats;
return callback(ebook);
}
}
}
Ebook.prototype.getAvailability = function(callback) {
- var ses = dojo.cookie(this.vendor);
+ var ses = this.ses || dojo.cookie(this.vendor);
new OpenSRF.ClientSession('open-ils.ebook_api').request({
method: 'open-ils.ebook_api.title.availability',
params: [ ses, this.id ],
}
Ebook.prototype.getHoldings = function(callback) {
- var ses = dojo.cookie(this.vendor);
+ var ses = this.ses || dojo.cookie(this.vendor);
new OpenSRF.ClientSession('open-ils.ebook_api').request({
method: 'open-ils.ebook_api.title.holdings',
params: [ ses, this.id ],
}
Ebook.prototype.checkout = function(authtoken, patron_id, callback) {
- var ses = dojo.cookie(this.vendor);
+ var ses = this.ses || dojo.cookie(this.vendor);
var ebook = this;
+ // get selected checkout format (optional, used by OverDrive)
+ var checkout_format;
+ var format_selector = dojo.byId('checkout-format');
+ if (format_selector) {
+ checkout_format = format_selector.value;
+ }
+ // perform checkout
new OpenSRF.ClientSession('open-ils.ebook_api').request({
method: 'open-ils.ebook_api.checkout',
- params: [ authtoken, ses, ebook.id, patron_id ],
+ params: [ authtoken, ses, ebook.id, patron_id, checkout_format ],
async: true,
oncomplete: function(r) {
var resp = r.recv();
}
Ebook.prototype.placeHold = function(authtoken, patron_id, callback) {
- var ses = dojo.cookie(this.vendor);
+ var ses = this.ses || dojo.cookie(this.vendor);
var ebook = this;
new OpenSRF.ClientSession('open-ils.ebook_api').request({
method: 'open-ils.ebook_api.place_hold',
}
Ebook.prototype.cancelHold = function(authtoken, patron_id, callback) {
- var ses = dojo.cookie(this.vendor);
+ var ses = this.ses || dojo.cookie(this.vendor);
var ebook = this;
new OpenSRF.ClientSession('open-ils.ebook_api').request({
method: 'open-ils.ebook_api.cancel_hold',
}).send();
}
+Ebook.prototype.download = function() {
+ var ses = this.ses || dojo.cookie(this.vendor);
+ var ebook = this;
+ var request_link;
+ var format_selector = dojo.byId('download-format');
+ if (!format_selector) {
+ console.log('could not find a specified format for download');
+ return;
+ } else {
+ request_link = format_selector.value;
+ }
+ // Request links include params like "errorpageurl={errorpageurl}"
+ // for redirecting the user if there's an error doing the download, etc.
+ // In these scenarios we always redirect the user to the current page.
+ // TODO: Add params to the current-page URL so that, if redirected, we
+ // can detect those params on page reload and show a useful message.
+ request_link = request_link.replace('{errorpageurl}', window.location.href);
+ request_link = request_link.replace('{odreadauthurl}', window.location.href);
+ // Now we're ready to request our download link.
+ new OpenSRF.ClientSession('open-ils.ebook_api').request({
+ method: 'open-ils.ebook_api.title.get_download_link',
+ params: [ authtoken, ses, request_link ],
+ async: true,
+ oncomplete: function(r) {
+ var resp = r.recv();
+ if (resp) {
+ if (resp.content().error_msg) {
+ console.log('download link request failed: ' + resp.content().error_msg);
+ } else if (resp.content().url) {
+ var url = resp.content().url;
+ console.log('download link received: ' + url);
+ window.location = url;
+ } else {
+ console.log('unknown error requesting download link');
+ }
+ }
+ }
+ }).send();
+}
+
holds_pending: [],
holds_ready: []
};
+var ebooks = [];
// Ebook to perform actions on.
var active_ebook;
// Update current page with detailed transaction info, where appropriate.
function addTransactionsToPage() {
+ // ensure active ebook has access to session ID to avoid scoping issues during transactions
+ if (active_ebook && typeof active_ebook.vendor !== 'undefined') {
+ active_ebook.ses = active_ebook.ses || dojo.cookie(active_ebook.vendor);
+ }
if (myopac_page) {
console.log('updating page with cached transaction details, if applicable');
if (myopac_page === 'ebook_circs')
} else {
dojo.empty('ebook_circs_main_table_body');
dojo.forEach(xacts.checkouts, function(x) {
- var dl_link = '<a href="' + x.download_url + '">' + l_strings.download + '</a>';
+ var ebook = new Ebook(x.vendor, x.title_id);
var tr = dojo.create("tr", null, dojo.byId('ebook_circs_main_table_body'));
dojo.create("td", { innerHTML: x.title }, tr);
dojo.create("td", { innerHTML: x.author }, tr);
dojo.create("td", { innerHTML: x.due_date }, tr);
- dojo.create("td", { innerHTML: dl_link}, tr);
+ var dl_td = dojo.create("td", null, tr);
+ if (x.download_url) {
+ dl_td.innerHTML = '<a href="' + x.download_url + '">' + l_strings.download + '</a>';
+ }
+ if (x.formats) {
+ var select = dojo.create("select", { id: "download-format" }, dl_td);
+ for (f in x.formats) {
+ dojo.create("option", { value: x.formats[f], innerHTML: f }, select);
+ }
+ var button = dojo.create("input", { id: "download-button", type: "button", value: l_strings.download }, dl_td);
+ ebook.conns.download = dojo.connect(button, 'onclick', ebook, "download");
+ }
// TODO: more actions (renew, checkin)
+ ebooks.push(ebook);
});
dojo.addClass('no_ebook_circs', "hidden");
dojo.removeClass('ebook_circs_main', "hidden");
// set up page for user to perform a checkout
function getReadyForCheckout() {
+ if (typeof ebook_action.type === 'undefined')
+ return;
if (typeof active_ebook === 'undefined') {
console.log('No active ebook specified, cannot prepare for checkout');
dojo.removeClass('ebook_checkout_failed', "hidden");
dojo.create("td", { innerHTML: ebook.author }, tr);
dojo.create("td", null, tr);
dojo.create("td", { id: "checkout-button-td" }, tr);
+ if (typeof active_ebook.formats !== 'undefined') {
+ var select = dojo.create("select", { id: "checkout-format" }, dojo.byId('checkout-button-td'));
+ dojo.forEach(active_ebook.formats, function(f) {
+ dojo.create("option", { value: f.id, innerHTML: f.name }, select);
+ });
+ }
var button = dojo.create("input", { id: "checkout-button", type: "button", value: l_strings.checkout }, dojo.byId('checkout-button-td'));
ebook.conns.checkout = dojo.connect(button, 'onclick', "doCheckout");
dojo.removeClass('ebook_circs_main', "hidden");
// set up page for user to place a hold
function getReadyForHold() {
+ if (typeof ebook_action.type === 'undefined')
+ return;
if (typeof active_ebook === 'undefined') {
console.log('No active ebook specified, cannot prepare for hold');
dojo.removeClass('ebook_hold_failed', "hidden");
// check out our active ebook
function doCheckout() {
+ var ses = dojo.cookie(active_ebook.vendor); // required when inspecting checkouts for download_url
active_ebook.checkout(authtoken, patron_id, function(resp) {
- if (resp.due_date) {
- console.log('Checkout succeeded!');
- dojo.destroy('checkout-button');
- dojo.removeClass('ebook_checkout_succeeded', "hidden");
- // add our successful checkout to top of transaction cache
- var new_xact = {
- title_id: active_ebook.id,
- title: active_ebook.title,
- author: active_ebook.author,
- due_date: resp.due_date,
- download_url: '' // TODO - for OverDrive, user must "lock in" a format first!
- };
- xacts.checkouts.unshift(new_xact);
- cleanupAfterAction();
- } else {
+ if (resp.error_msg) {
console.log('Checkout failed: ' + resp.error_msg);
dojo.removeClass('ebook_checkout_failed', "hidden");
+ return;
+ }
+ console.log('Checkout succeeded!');
+ dojo.destroy('checkout-button');
+ dojo.destroy('checkout-format'); // remove optional format selector
+ dojo.removeClass('ebook_checkout_succeeded', "hidden");
+ // add our successful checkout to top of transaction cache
+ var new_xact = {
+ title_id: active_ebook.id,
+ title: active_ebook.title,
+ author: active_ebook.author,
+ due_date: resp.due_date,
+ finish: function() {
+ console.log('new_xact.finish()');
+ xacts.checkouts.unshift(this);
+ cleanupAfterAction();
+ // When we switch to jQuery, we can use .one() instead of .on(),
+ // obviating the need for an explicit disconnect here.
+ dojo.disconnect(active_ebook.conns.checkout);
+ }
+ };
+ if (resp.download_url) {
+ // Use download URL from checkout response, if available.
+ new_xact.download_url = resp.download_url;
+ dojo.create("a", { href: new_xact.download_url, innerHTML: l_strings.download }, dojo.byId('checkout-button-td'));
+ new_xact.finish();
+ } else if (typeof resp.formats !== 'undefined') {
+ // User must select download format from list of options.
+ var select = dojo.create("select", { id: "download-format" }, dojo.byId('checkout-button-td'));
+ for (f in resp.formats) {
+ dojo.create("option", { value: resp.formats[f], innerHTML: f }, select);
+ }
+ var button = dojo.create("input", { id: "download-button", type: "button", value: l_strings.download }, dojo.byId('checkout-button-td'));
+ active_ebook.conns.download = dojo.connect(button, 'onclick', active_ebook, "download");
+ new_xact.finish();
+ } else if (typeof resp.xact_id !== 'undefined') {
+ // No download URL provided by API checkout response. Grab fresh
+ // list of user checkouts from API, find the just-completed
+ // checkout by transaction ID, and get the download URL from that.
+ // We call the OpenSRF method directly because Relation.getCheckouts()
+ // results in scoping issues when retrieving the vendor session cookie.
+ new_xact.xact_id = resp.xact_id;
+ new OpenSRF.ClientSession('open-ils.ebook_api').request({
+ method: 'open-ils.ebook_api.patron.get_checkouts',
+ params: [ authtoken, ses, patron_id ],
+ async: false,
+ oncomplete: function(r) {
+ var resp = r.recv();
+ if (resp) {
+ dojo.forEach(resp.content(), function(x) {
+ if (x.xact_id === new_xact.xact_id) {
+ new_xact.download_url = x.download_url;
+ dojo.create("a", { href: new_xact.download_url, innerHTML: l_strings.download }, dojo.byId('checkout-button-td'));
+ return;
+ }
+ });
+ new_xact.finish();
+ }
+ }
+ }).send();
}
- // When we switch to jQuery, we can use .one() instead of .on(),
- // obviating the need for an explicit disconnect here.
- dojo.disconnect(active_ebook.conns.checkout);
});
}