Bug 10320 - Integrate OverDrive search into OPAC

Show any relevant results from the OverDrive ebook/audiobook service
on the OPAC search. This is done by showing a link with "Found xx
results in the library's OverDrive collection" at the top of search
results and linking to a page that shows the full results.

This requires an OverDrive developer account, and is enabled by
setting the OverDriveClientKey and OverDriveClientSecret
system preferences.  In addition, this patch adds the
OverDriveLibraryID system preference.

Signed-off-by: Srdjan <srdjan@catalyst.net.nz>
Signed-off-by: Henry Bankhead <hbankhead@losgatosca.gov>
Signed-off-by: Chris Cormack <chris@bigballofwax.co.nz>
Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Katrin Fischer <Katrin.Fischer.83@web.de>
Signed-off-by: Galen Charlton <gmc@esilibrary.com>
This commit is contained in:
Jesse Weaver 2013-05-16 17:11:15 -06:00 committed by Galen Charlton
parent 794f0f28c9
commit 2d0bdc80fd
17 changed files with 620 additions and 2 deletions

141
C4/External/OverDrive.pm vendored Normal file
View File

@ -0,0 +1,141 @@
package C4::External::OverDrive;
# Copyright (c) 2013 ByWater
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or ( at your option ) any later
# version.
#
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use strict;
use warnings;
use JSON;
use Koha::Cache;
use HTTP::Request;
use HTTP::Request::Common;
use LWP::Authen::Basic;
use LWP::UserAgent;
BEGIN {
require Exporter;
our $VERSION = 3.07.00.049;
our @ISA = qw( Exporter ) ;
our @EXPORT = qw(
IsOverDriveEnabled
GetOverDriveToken
);
}
sub _request {
my ( $request ) = @_;
my $ua = LWP::UserAgent->new( "Koha " . C4::Context->KOHAVERSION );
my $response;
eval {
$response = $ua->request( $request ) ;
};
if ( $@ ) {
warn "OverDrive request failed: $@";
return;
}
return $response;
}
=head1 NAME
C4::External::OverDrive - Retrieve OverDrive content availability information
=head2 FUNCTIONS
This module provides content search for OverDrive,
=over
=item IsOverDriveEnabled
Returns 1 if all of the necessary system preferences for OverDrive are set.
=back
=cut
sub IsOverDriveEnabled {
return (
C4::Context->preference( 'OverDriveClientKey' ) &&
C4::Context->preference( 'OverDriveClientSecret' )
);
}
=item GetOverDriveToken
Fetches an OAuth2 auth token for the OverDrive API, reusing an existing token in
Memcache if possible.
Returns the token ( as "bearer ..." ) or undef on failure.
=back
=cut
sub GetOverDriveToken {
my $key = C4::Context->preference( 'OverDriveClientKey' );
my $secret = C4::Context->preference( 'OverDriveClientSecret' );
return unless ( $key && $secret ) ;
my $cache;
eval { $cache = Koha::Cache->new() };
my $token;
$cache and $token = $cache->get_from_cache( "overdrive_token" ) and return $token;
my $request = HTTP::Request::Common::POST( 'https://oauth.overdrive.com/token', [
grant_type => 'client_credentials'
] ) ;
$request->header( Authorization => LWP::Authen::Basic->auth_header( $key, $secret ) );
my $response = _request( $request ) or return;
if ( $response->header('Content-Type') !~ m!application/json! ) {
warn "Could not connect to OverDrive: " . $response->message;
return;
}
my $contents = from_json( $response->decoded_content );
if ( !$response->is_success ) {
warn "Could not log into OverDrive: " . ( $contents ? $contents->{'error_description'} : $response->decoded_content );
return;
}
$token = $contents->{'token_type'} . ' ' . $contents->{'access_token'};
# Fudge factor to prevent spurious failures
$cache->set_in_cache( 'overdrive_token', $token, $contents->{'expires_in'} - 5 );
return $token;
}
1;
__END__
=head1 NOTES
=cut
=head1 AUTHOR
Jesse Weaver <pianohacker@gmail.com>
=cut

View File

@ -7089,6 +7089,21 @@ if ( CheckVersion($DBversion) ) {
SetVersion($DBversion);
}
$DBversion = "3.13.00.XXX";
if(CheckVersion($DBversion)) {
$dbh->do(
"INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES('OverDriveClientKey','','Client key for OverDrive integration','30','Free')"
);
$dbh->do(
"INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES('OverDriveClientSecret','','Client key for OverDrive integration','30','YesNo')"
);
$dbh->do(
"INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES('OpacDriveLibraryID','','Library ID for OverDrive integration','','Integer')"
);
print "Upgrade to $DBversion done (Bug 10320 - Show results from library's OverDrive collection in OPAC search)\n";
SetVersion($DBversion);
}
=head1 FUNCTIONS
=head2 TableExists($table)

View File

@ -326,3 +326,14 @@ Enhanced Content:
yes: Enable
no: "Don't enable"
- the ability to use Koha Plugins. Note, the plugin system must also be enabled in the Koha configuration file to be fully enabled.
OverDrive:
-
- Include OverDrive availability information with the client key
- pref: OverDriveClientKey
- and client secret
- pref: OverDriveClientSecret
- .
-
- "Show items from the OverDrive catalog of library #"
- pref: OverDriveLibraryID
- .

View File

@ -0,0 +1,61 @@
if ( typeof KOHA == "undefined" || !KOHA ) {
var KOHA = {};
}
KOHA.OverDrive = ( function() {
var proxy_base_url = '/cgi-bin/koha/svc/overdrive_proxy';
var library_base_url = 'http://api.overdrive.com/v1/libraries/';
return {
Get: function( url, params, callback ) {
$.ajax( {
type: 'GET',
url: url.replace( /https?:\/\/api.overdrive.com\/v1/, proxy_base_url ),
dataType: 'json',
data: params,
error: function( xhr, error ) {
try {
callback( JSON.parse( xhr.responseText ));
} catch ( e ) {
callback( {error: xhr.responseText || true} );
}
},
success: callback
} );
},
GetCollectionURL: function( library_id, callback ) {
if ( KOHA.OverDrive.collection_url ) {
callback( KOHA.OverDrive.collection_url );
return;
}
KOHA.OverDrive.Get(
library_base_url + library_id,
{},
function ( data ) {
if ( data.error ) {
callback( data );
return;
}
KOHA.OverDrive.collection_url = data.links.products.href;
callback( data.links.products.href );
}
);
},
Search: function( library_id, q, limit, offset, callback ) {
KOHA.OverDrive.GetCollectionURL( library_id, function( data ) {
if ( data.error ) {
callback( data );
return;
}
KOHA.OverDrive.Get(
data,
{q: q, limit: limit, offset: offset},
callback
);
} );
}
};
} )();

View File

@ -3029,6 +3029,7 @@ padding: 0.1em 0;
.thumbnail-shelfbrowser span {
margin: 0px auto;
}
<<<<<<< HEAD
.sorting_asc {
padding-right: 19px;
background: url("../../images/asc.gif") no-repeat scroll right center #EEEEEE;
@ -3048,3 +3049,12 @@ padding: 0.1em 0;
padding-right: 19px;
background: #EEEEEE none;
}
#overdrive-results {
font-weight: bold;
padding-left: 1em;
}
.throbber {
vertical-align: middle;
}

View File

@ -0,0 +1,61 @@
if ( typeof KOHA == "undefined" || !KOHA ) {
var KOHA = {};
}
KOHA.OverDrive = ( function() {
var proxy_base_url = '/cgi-bin/koha/svc/overdrive_proxy';
var library_base_url = 'http://api.overdrive.com/v1/libraries/';
return {
Get: function( url, params, callback ) {
$.ajax( {
type: 'GET',
url: url.replace( /https?:\/\/api.overdrive.com\/v1/, proxy_base_url ),
dataType: 'json',
data: params,
error: function( xhr, error ) {
try {
callback( JSON.parse( xhr.responseText ));
} catch ( e ) {
callback( {error: xhr.responseText || true} );
}
},
success: callback
} );
},
GetCollectionURL: function( library_id, callback ) {
if ( KOHA.OverDrive.collection_url ) {
callback( KOHA.OverDrive.collection_url );
return;
}
KOHA.OverDrive.Get(
library_base_url + library_id,
{},
function ( data ) {
if ( data.error ) {
callback( data );
return;
}
KOHA.OverDrive.collection_url = data.links.products.href;
callback( data.links.products.href );
}
);
},
Search: function( library_id, q, limit, offset, callback ) {
KOHA.OverDrive.GetCollectionURL( library_id, function( data ) {
if ( data.error ) {
callback( data );
return;
}
KOHA.OverDrive.Get(
data,
{q: q, limit: limit, offset: offset},
callback
);
} );
}
};
} )();

View File

@ -0,0 +1,161 @@
[% INCLUDE 'doc-head-open.inc' %]
[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog &rsaquo; OverDrive search for '[% q | html %]'
[% INCLUDE 'doc-head-close.inc' %]
<script type="text/javascript" src="[% themelang %]/js/overdrive.js"></script>
<script type="text/javascript">
var querystring = "[% q |replace( "'", "\'" ) |replace( '\n', '\\n' ) |replace( '\r', '\\r' ) |html %]";
var results_per_page = [% OPACnumSearchResults %];
function fetch_availability( prod, $tr ) {
var $availability_summary = $( '<span class="results_summary"></span>' );
$tr.find( '.info' ).append( $availability_summary );
$availability_summary.html( '<span class="label">Availability: </span> Loading...' );
KOHA.OverDrive.Get(
prod.links.availability.href,
{},
function ( data ) {
if ( data.error ) return;
$availability_summary.html( '<span class="label">Copies available: </span><span class="available"><strong>' + data.copiesAvailable + '</strong> out of ' + data.copiesOwned + '</span>' );
if ( data.numberOfHolds ) {
$availability_summary.find( '.available' ).append( ', waiting holds: <strong>' + data.numberOfHolds + '</strong>' );
}
$tr.find( '.info' ).append( '<span class="results_summary actions"><span class="label">Actions: </span><a href="http://' + prod.contentDetails[0].href + '" ' + ( data.copiesAvailable ? ' class="addtocart">Check out' : ' class="hold">Place hold' ) + '</a></span>' );
}
);
}
function search( offset ) {
$( '#overdrive-status' ).html( 'Searching OverDrive... <img class="throbber" src="/opac-tmpl/lib/jquery/plugins/themes/classic/throbber.gif" /></span>' );
KOHA.OverDrive.Search( "[% OverDriveLibraryID %]", querystring, results_per_page, offset, function( data ) {
if ( data.error ) {
$( '#overdrive-status' ).html( '<strong class="unavailable">Error searching OverDrive collection.</strong>' );
return;
}
if ( !data.totalItems ) {
$( '#overdrive-status' ).html( '<strong>No results found in the library\'s OverDrive collection.</strong>' );
return;
}
$( '#results tbody' ).empty();
$( '#overdrive-status' ).html( '<strong>Found ' + data.totalItems + ' results in the library\'s OverDrive collection.</strong>' );
for ( var i = 0; data.products[i]; i++ ) {
var prod = data.products[i];
var results = [];
results.push( '<tr>' );
results.push( '<td class="info"><a class="title" href="http://', prod.contentDetails[0].href, '">' );
results.push( prod.title );
if ( prod.subtitle ) results.push( ', ', prod.subtitle );
results.push( '</a>' );
results.push( '<p>by ', prod.primaryCreator.name, '</p>' );
results.push( '<span class="results_summary"><span class="label">Type: </span>', prod.mediaType, '</span>' );
if ( prod.starRating ) results.push( '<span class="results_summary"><span class="label">Average rating: <img src="[% themelang %]/../images/Star', Math.round( parseInt( prod.starRating )), '.gif" title="" style="max-height: 15px; vertical-align: bottom"/></span>' );
results.push( '</td>' );
results.push( '<td>' );
if ( prod.images.thumbnail ) {
results.push( '<a href="http://', prod.contentDetails[0].href, '">' );
results.push( '<img class="thumbnail" src="', prod.images.thumbnail.href, '" />' );
results.push( '</a>' );
}
results.push( '</td>' );
results.push( '</tr>' );
var $tr = $( results.join( '' ));
$( '#results tbody' ).append( $tr );
fetch_availability( prod, $tr );
}
$( '#results tr:odd' ).addClass( 'highlight' );
var pages = [];
var cur_page = offset / results_per_page;
var max_page = Math.floor( data.totalItems / results_per_page );
if ( cur_page != 0 ) {
pages.push( '<a class="nav" href="#" data-offset="' + (offset - results_per_page) + '">&lt;&lt; Previous</a>' );
}
for ( var page = Math.max( 0, cur_page - 9 ); page <= Math.min( max_page, cur_page + 9 ); page++ ) {
if ( page == cur_page ) {
pages.push( ' <span class="current">' + ( page + 1 ) + '</span>' );
} else {
pages.push( ' <a class="nav" href="#" data-offset="' + ( page * results_per_page ) + '">' + ( page + 1 ) + '</a>' );
}
}
if ( cur_page < max_page ) {
pages.push( ' <a class="nav" href="#" data-offset="' + (offset + results_per_page) + '">Next >></a>' );
}
if ( pages.length > 1 ) $( '#top-pages, #bottom-pages' ).find( '.pages' ).html( pages.join( '' ) );
} );
}
$( document ).ready( function() {
$( '#breadcrumbs p' )
.append( ' ' )
.append( '<span id="overdrive-status"></span>' );
$( document ).on( 'click', 'a.nav', function() {
search( $( this ).data( 'offset' ) );
return false;
});
search( 0 );
} );
</script>
<style>
.actions a.addtocart {
display: inline;
}
</style>
</head>
<body>
[% IF ( OpacNav ) %]
<div id="doc3" class="yui-t1">
[% ELSE %]
<div id="doc3" class="yui-t7">
[% END %]
<div id="bd">
[% INCLUDE 'masthead.inc' %]
<h1>OverDrive search for '[% q | html %]'</h1>
<div id="breadcrumbs">
<p></p>
</div>
<div id="yui-main"><div class="yui-b searchresults">
<div id="top-pages">
<div class="pages">
</div>
</div>
<table id="results">
<tbody>
</tbody>
</table>
<div id="bottom-pages">
<div class="pages">
</div>
</div>
</div></div>
[% IF ( OpacNav ) %]
<div class="yui-b"><div id="opacnav" class="container">
[% INCLUDE 'navigation.inc' %]
</div></div>
[% END %]
</div>
[% INCLUDE 'opac-bottom.inc' %]

View File

@ -16,6 +16,7 @@
<link rel="stylesheet" type="text/css" href="[% themelang %]/css/jquery.rating.css" />
[% END %]
<script type="text/javascript" src="[% themelang %]/js/overdrive.js"></script>
<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.checkboxes.min.js"></script>
[% IF ( OpacHighlightedWords ) %]<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.highlight-3.js"></script>
[% END %]<script type="text/javascript">
@ -251,7 +252,10 @@ $(document).ready(function(){
[% END %]
$("#holdDetails").hide();
[% IF ( query_desc ) %][% IF ( OpacHighlightedWords ) %]var query_desc = "[% query_desc |replace("'", "\'") |replace('\n', '\\n') |replace('\r', '\\r') |html %]";
[% IF ( query_desc ) %]
var query_desc = "[% query_desc |replace("'", "\'") |replace('\n', '\\n') |replace('\r', '\\r') |html %]";
var querystring = "[% querystring |replace("'", "\'") |replace('\n', '\\n') |replace('\r', '\\r') |html %]";
[% IF ( OpacHighlightedWords ) %]
q_array = query_desc.split(" ");
// ensure that we don't have "" at the end of the array, which can
// break the highlighter
@ -260,7 +264,27 @@ $(document).ready(function(){
}
highlightOn();
$("#highlight_toggle_on" ).hide().click(function() {highlightOn() ;});
$("#highlight_toggle_off").show().click(function() {highlightOff();});[% END %][% END %]
$("#highlight_toggle_off").show().click(function() {highlightOff();});
[% END %]
[% IF ( OverDriveEnabled ) %]
var $overdrive_results = $( '<span id="overdrive-results">Searching OverDrive... <img class="throbber" src="/opac-tmpl/lib/jquery/plugins/themes/classic/throbber.gif" /></span>' );
$( '#breadcrumbs p' ).eq(0)
.append( ' ' )
.append( $overdrive_results );
KOHA.OverDrive.Search( "[% OverDriveLibraryID %]", querystring, 1, 0, function( data ) {
if ( data.error ) {
$overdrive_results.html( 'Error searching OverDrive collection' );
return;
}
if ( data.totalItems ) {
$overdrive_results.html( 'Found <a href="/cgi-bin/koha/opac-overdrive-search.pl?q=' + escape( querystring ) + '">' + data.totalItems + ' results</a> in OverDrive collection' );
} else {
$overdrive_results.remove();
}
} );
[% END %]
[% END %]
[% IF ( TagsInputEnabled && loggedinusername ) %]
$("#tagsel_tag").click(function(){

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

45
opac/opac-overdrive-search.pl Executable file
View File

@ -0,0 +1,45 @@
#!/usr/bin/perl
# Copyright 2009 BibLibre SARL
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use strict;
use warnings;
use CGI;
use C4::Auth qw(:DEFAULT get_session);
use C4::Output;
my $cgi = new CGI;
# Getting the template and auth
my ($template, $loggedinuser, $cookie)
= get_template_and_user({template_name => "opac-overdrive-search.tmpl",
query => $cgi,
type => "opac",
authnotrequired => 1,
flagsrequired => {borrowers => 1},
debug => 1,
});
$template->{'VARS'}->{'q'} = $cgi->param('q');
$template->{'VARS'}->{'limit'} = C4::Context->preference('OPACnumSearchResults') || 20;
$template->{'VARS'}->{'OPACnumSearchResults'} = C4::Context->preference('OPACnumSearchResults') || 20;
$template->{'VARS'}->{'OverDriveLibraryID'} = C4::Context->preference('OverDriveLibraryID');
output_html_with_http_headers $cgi, $cookie, $template->output;

View File

@ -51,6 +51,7 @@ use C4::Tags qw(get_tags);
use C4::Branch; # GetBranches
use C4::SocialData;
use C4::Ratings;
use C4::External::OverDrive;
use POSIX qw(ceil floor strftime);
use URI::Escape;
@ -886,5 +887,10 @@ $template->{VARS}->{IDreamBooksReviews} = C4::Context->preference('IDreamBooksRe
$template->{VARS}->{IDreamBooksReadometer} = C4::Context->preference('IDreamBooksReadometer');
$template->{VARS}->{IDreamBooksResults} = C4::Context->preference('IDreamBooksResults');
if ($offset == 0 && IsOverDriveEnabled()) {
$template->param(OverDriveEnabled => 1);
$template->param(OverDriveLibraryID => C4::Context->preference('OverDriveLibraryID'));
}
$template->param( borrowernumber => $borrowernumber);
output_with_http_headers $cgi, $cookie, $template->output, $content_type;

83
opac/svc/overdrive_proxy Executable file
View File

@ -0,0 +1,83 @@
#!/usr/bin/perl
# Copyright 2013 ByWater
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with Koha; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
=head1 NAME
svc/overdrive_proxy: Proxy OAuth'd requests to OverDrive
=head1 SYNOPSIS
svc/overdrive_proxy/libraries/9001 -> https://api.overdrive.com/v1/libraries/9001
=head1 DESCRIPTION
This service proxies incoming requests to the OverDrive OAuth API, to keep the
JS side from having to deal with cross-origin/authentication issues.
=cut
use strict;
use warnings;
use CGI qw(-oldstyle_urls);
use JSON;
use C4::Context;
use C4::External::OverDrive;
use C4::Output;
my $query = new CGI;
my $token;
if ( !IsOverDriveEnabled() || !( $token = GetOverDriveToken() ) ) {
print $query->header(
-status => '400 Bad Request',
-content
);
print to_json({
error => 'invalid_client',
error_description => 'OverDrive login failed'
});
exit;
}
my $request = HTTP::Request::Common::GET( "https://api.overdrive.com/v1" . $query->path_info . '?' . $query->query_string );
$request->header( Authorization => $token );
my $ua = LWP::UserAgent->new( "Koha " . C4::Context->KOHAVERSION );
my $response = $ua->request( $request ) ;
if ( $response->code eq '500' ) {
print $query->header(
-status => '500 Internal Server Error'
);
warn "OverDrive request failed: " . $response->message;
print to_json({
error => 'invalid_client',
error_description => 'OverDrive request failed'
});
exit;
}
output_with_http_headers $query, undef, $response->content, 'json', $response->status_line;