From: Jeff Davis Date: Tue, 4 Jul 2017 23:20:11 +0000 (-0700) Subject: LP#1673870: Handle OverDrive ebook checkout and download X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=856fcdae8cd367c0b5cdd2fe9c92e03a81423a12;p=working%2FEvergreen.git LP#1673870: Handle OverDrive ebook checkout and download The workflow for checking out and downloading a title via the OverDrive API is relatively complex: 1. Check out a title. 2. Lock in a specific format for the checked-out title. Once you lock in a format, you can only download the title in that format -- except that the browser-based OverDrive Read and OverDrive Listen formats are always available (if supported for that title), even if you've locked in another format. 3. Request a link for downloading the title in the specified format. Download links are dynamically generated and only work for 60 seconds from the time of your request. To simplify the process, we require the user to lock in a format during checkout. Then, when the user clicks the Download button, we request a download link; OverDrive responds with a URL, and we immediately redirect the current browser tab/window to that URL. A new API call, open-ils.ebook_api.title.get_download_link, has been added for requesting the download link. Since API calls are not vendor-specific, we also add support for the new method in the test module, complete with unit test. Signed-off-by: Jeff Davis --- diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm index d6961a8a01..bc968b5586 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI.pm @@ -481,7 +481,7 @@ __PACKAGE__->register_method( # - 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/) { @@ -509,9 +509,12 @@ sub do_xact { # 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); } @@ -845,4 +848,40 @@ __PACKAGE__->register_method( } ); +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; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm index 9549fa49d9..b3d0cab7be 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/OverDrive.pm @@ -376,10 +376,13 @@ sub get_title_info { }; 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(); + return $info; } else { $logger->error("EbookAPI: OverDrive metadata lookup failed for $title_id"); } @@ -446,6 +449,20 @@ sub do_holdings_lookup { } # request available formats + $holdings->{formats} = $self->get_formats(); + + 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" @@ -453,7 +470,7 @@ sub do_holdings_lookup { 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, $f->{name}; } } else { $logger->info("EbookAPI: OverDrive holdings format request for title $title_id contained no format information"); @@ -462,7 +479,7 @@ sub do_holdings_lookup { $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 @@ -500,8 +517,17 @@ sub do_holdings_lookup { # ], # ... # } +# +# 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 => [ { @@ -510,6 +536,9 @@ sub checkout { } ] }; + if ($format) { + push @{$request_content->{fields}}, { name => 'formatType', value => $format }; + } my $req = { method => 'POST', uri => $self->{circulation_base_uri} . "/patrons/me/checkouts", @@ -517,7 +546,16 @@ sub checkout { }; 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' ) }; @@ -637,12 +675,17 @@ sub get_patron_checkouts { 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; @@ -703,4 +746,21 @@ sub do_get_patron_xacts { 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) = @_; + 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; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm index adab52c094..31d7bf9ac0 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm @@ -272,6 +272,9 @@ sub checkout { # 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) @@ -503,3 +506,23 @@ sub get_patron_holds { 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; +} diff --git a/Open-ILS/src/perlmods/live_t/20-lp1541559-ebook-api.t b/Open-ILS/src/perlmods/live_t/20-lp1541559-ebook-api.t index 6c0aedb888..0b81a4ca68 100644 --- a/Open-ILS/src/perlmods/live_t/20-lp1541559-ebook-api.t +++ b/Open-ILS/src/perlmods/live_t/20-lp1541559-ebook-api.t @@ -1,6 +1,6 @@ #!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"); @@ -154,6 +154,14 @@ my $checkout_req = $ebook_api->request( 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); diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js b/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js index 5119507da9..881f8c3301 100644 --- a/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js +++ b/Open-ILS/web/js/ui/default/opac/ebook_api/ebook.js @@ -13,7 +13,6 @@ function Ebook(vendor, id) { 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) { @@ -29,6 +28,8 @@ Ebook.prototype.getDetails = function(callback) { 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); } } @@ -72,9 +73,16 @@ Ebook.prototype.getHoldings = function(callback) { Ebook.prototype.checkout = function(authtoken, patron_id, callback) { var 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(); @@ -120,3 +128,34 @@ Ebook.prototype.cancelHold = function(authtoken, patron_id, callback) { }).send(); } +Ebook.prototype.download = function(authtoken) { + var 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; + } + 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) { + console.log('download link received: ' + resp.content().url); + //window.location = resp.url; + } else { + console.log('unknown error requesting download link'); + } + } + } + }).send(); +} + diff --git a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js index be520d55e5..a371d7f8bd 100644 --- a/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js +++ b/Open-ILS/web/js/ui/default/opac/ebook_api/loggedin.js @@ -113,12 +113,23 @@ function updateCheckoutView() { } else { dojo.empty('ebook_circs_main_table_body'); dojo.forEach(xacts.checkouts, function(x) { - var dl_link = '' + l_strings.download + ''; + x.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 = '' + l_strings.download + ''; + } + 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); + x.ebook.conns.download = dojo.connect(button, 'onclick', x.ebook, "download"); + } // TODO: more actions (renew, checkin) }); dojo.addClass('no_ebook_circs', "hidden"); @@ -195,6 +206,12 @@ function getReadyForCheckout() { 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, innerHTML: f }, 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"); @@ -242,15 +259,27 @@ function doCheckout() { if (resp.due_date) { 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, - download_url: '' // TODO - for OverDrive, user must "lock in" a format first! + due_date: resp.due_date }; + if (resp.download_url) { + new_xact.download_url = resp.download_url; + } + if (typeof resp.formats !== 'undefined') { + new_xact.ebook = new Ebook(active_ebook.vendor, active_ebook.title_id); + 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')); + new_xact.ebook.conns.download = dojo.connect(button, 'onclick', new_xact.ebook, "download"); + } xacts.checkouts.unshift(new_xact); cleanupAfterAction(); } else {