Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
23.95% |
40 / 167 |
|
37.04% |
10 / 27 |
CRAP | |
0.00% |
0 / 1 |
SolrOverdrive | |
23.95% |
40 / 167 |
|
37.04% |
10 / 27 |
2616.32 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
supportsOpenUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
supportsCoinsOpenUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAvailableDigitalFormats | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
getDigitalFormats | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
getFormattedDigitalFormats | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
110 | |||
getPreviewLinks | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
56 | |||
supportsAjaxStatus | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getOverdriveAccess | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isLoggedIn | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
getOverdriveID | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
getOverdriveAvailability | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
supportsPatronActions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isCheckedOut | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
90 | |||
isHeld | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
42 | |||
getBreadcrumb | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getMarcReader | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getTitleSection | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getGeneralNotes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getThumbnail | |
62.50% |
5 / 8 |
|
0.00% |
0 / 1 |
2.21 | |||
getSummary | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getIsMarc | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAllSubjectHeadings | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
getFormattedRawData | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
getRealTimeTitleHold | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
getURLs | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getPermanentLink | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | /** |
4 | * VuFind Record Driver for SolrOverdrive Records |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2019. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 |
22 | * USA |
23 | * |
24 | * @category VuFind |
25 | * @package RecordDrivers |
26 | * @author Demian Katz <demian.katz@villanova.edu> |
27 | * @author Brent Palmer <brent-palmer@icpl.org> |
28 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public |
29 | * License |
30 | * @link https://vufind.org/wiki/development:plugins:record_drivers Wiki |
31 | */ |
32 | |
33 | namespace VuFind\RecordDriver; |
34 | |
35 | use Laminas\Config\Config; |
36 | use Laminas\Log\LoggerAwareInterface; |
37 | use VuFind\DigitalContent\OverdriveConnector; |
38 | |
39 | use function in_array; |
40 | |
41 | /** |
42 | * VuFind Record Driver for SolrOverdrive Records |
43 | * |
44 | * @category VuFind |
45 | * @package RecordDrivers |
46 | * @author Demian Katz <demian.katz@villanova.edu> |
47 | * @author Brent Palmer <brent-palmer@icpl.org> |
48 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public |
49 | * License |
50 | * @link https://vufind.org/wiki/development:plugins:record_drivers Wiki |
51 | */ |
52 | class SolrOverdrive extends SolrMarc implements LoggerAwareInterface |
53 | { |
54 | use \VuFind\Log\LoggerAwareTrait { |
55 | logError as error; |
56 | } |
57 | |
58 | /** |
59 | * Overdrive Connector |
60 | * |
61 | * @var OverdriveConnector $connector Overdrive Connector |
62 | */ |
63 | protected $connector; |
64 | |
65 | /** |
66 | * Overdrive Configuration Object |
67 | * |
68 | * @var object |
69 | */ |
70 | protected $config; |
71 | |
72 | /** |
73 | * Constructor |
74 | * |
75 | * @param Config $mainConfig VuFind main configuration |
76 | * @param Config $recordConfig Record-specific configuration |
77 | * @param OverdriveConnector $connector Overdrive Connector |
78 | */ |
79 | public function __construct( |
80 | Config $mainConfig = null, |
81 | $recordConfig = null, |
82 | OverdriveConnector $connector = null |
83 | ) { |
84 | $this->connector = $connector; |
85 | $this->config = $connector->getConfig(); |
86 | parent::__construct($mainConfig, $recordConfig, null); |
87 | } |
88 | |
89 | /** |
90 | * Supports OpenURL |
91 | * |
92 | * @return bool |
93 | */ |
94 | public function supportsOpenUrl() |
95 | { |
96 | return false; |
97 | } |
98 | |
99 | /** |
100 | * Supports coins OpenURL |
101 | * |
102 | * @return bool |
103 | */ |
104 | public function supportsCoinsOpenUrl() |
105 | { |
106 | return false; |
107 | } |
108 | |
109 | /** |
110 | * Get Available Digital Formats |
111 | * |
112 | * Return the digital download formats that are available for linking to. |
113 | * |
114 | * @return array |
115 | * @throws \Exception |
116 | */ |
117 | public function getAvailableDigitalFormats() |
118 | { |
119 | $formats = []; |
120 | $formatNames = $this->connector->getFormatNames(); |
121 | $od_id = $this->getOverdriveID(); |
122 | |
123 | if ($checkout = $this->connector->getCheckout($od_id, false)) { |
124 | // If we're already locked in, then we need free ones and locked in ones. |
125 | if ($checkout->isFormatLockedIn) { |
126 | foreach ($checkout->formats as $format) { |
127 | $formatType = $format->formatType; |
128 | $formats[$formatType] = $formatNames[$formatType]; |
129 | } |
130 | } else { |
131 | // Not locked in, we can show all formats |
132 | foreach ($this->getDigitalFormats() as $format) { |
133 | $formats[$format->id] = $formatNames[$format->id]; |
134 | } |
135 | } |
136 | } |
137 | return $formats; |
138 | } |
139 | |
140 | /** |
141 | * Get Formats |
142 | * |
143 | * Returns an array of digital formats for this resource. |
144 | * |
145 | * @return array Array of formats. |
146 | * @throws \Exception |
147 | */ |
148 | public function getDigitalFormats() |
149 | { |
150 | $formats = []; |
151 | $formatNames = $this->connector->getFormatNames(); |
152 | if ($this->getIsMarc()) { |
153 | $od_id = $this->getOverdriveID(); |
154 | $fulldata = $this->connector->getMetadata([$od_id]); |
155 | $data = $fulldata[strtolower($od_id)]; |
156 | } else { |
157 | $jsonData = $this->fields['fullrecord']; |
158 | $data = json_decode($jsonData, false); |
159 | } |
160 | |
161 | foreach ($data->formats as $format) { |
162 | $format->name = $formatNames[$format->id]; |
163 | $formats[$format->id] = $format; |
164 | } |
165 | |
166 | return $formats; |
167 | } |
168 | |
169 | /** |
170 | * Get an array of all the formats associated with the record with metadata |
171 | * associated with it. This array is designed to be used in a template. |
172 | * The key for each entry is the translatable token for the format name |
173 | * |
174 | * @return array |
175 | * @throws \Exception |
176 | */ |
177 | public function getFormattedDigitalFormats() |
178 | { |
179 | $results = []; |
180 | foreach ($this->getDigitalFormats() as $key => $format) { |
181 | $tmpresults = []; |
182 | if ($format->fileSize > 0) { |
183 | if ($format->fileSize > 1000000) { |
184 | $size = round($format->fileSize / 1000000); |
185 | $size .= ' GB'; |
186 | } elseif ($format->fileSize > 1000) { |
187 | $size = round($format->fileSize / 1000); |
188 | $size .= ' MB'; |
189 | } else { |
190 | $size = $format->fileSize; |
191 | $size .= ' KB'; |
192 | } |
193 | $tmpresults['File Size'] = $size; |
194 | } |
195 | if ($format->partCount) { |
196 | $tmpresults['Parts'] = $format->partCount; |
197 | } |
198 | if ($format->identifiers) { |
199 | foreach ($format->identifiers as $id) { |
200 | if (in_array($id->type, ['ISBN', 'ASIN'])) { |
201 | $tmpresults[$id->type] = $id->value; |
202 | } |
203 | } |
204 | } |
205 | if ($format->onSaleDate) { |
206 | $tmpresults['Release Date'] = $format->onSaleDate; |
207 | } |
208 | $results[$format->name] = $tmpresults; |
209 | } |
210 | |
211 | return $results; |
212 | } |
213 | |
214 | /** |
215 | * Returns links for showing previews |
216 | * |
217 | * @return array an array of links |
218 | * @throws \Exception |
219 | */ |
220 | public function getPreviewLinks() |
221 | { |
222 | $results = []; |
223 | if ($this->getIsMarc()) { |
224 | $od_id = $this->getOverdriveID(); |
225 | $fulldata = $this->connector->getMetadata([$od_id]); |
226 | $data = $fulldata[strtolower($od_id)] ?? null; |
227 | } else { |
228 | $jsonData = $this->fields['fullrecord']; |
229 | $data = json_decode($jsonData, false); |
230 | } |
231 | |
232 | if (isset($data->formats[0]->samples[0])) { |
233 | foreach ($data->formats[0]->samples as $format) { |
234 | if ( |
235 | $format->formatType == 'audiobook-overdrive' |
236 | || $format->formatType == 'ebook-overdrive' |
237 | || $format->formatType == 'magazine-overdrive' |
238 | ) { |
239 | $results = $format; |
240 | } |
241 | } |
242 | } |
243 | return $results; |
244 | } |
245 | |
246 | /** |
247 | * Returns true if the record supports real-time AJAX status lookups. |
248 | * |
249 | * @return bool |
250 | */ |
251 | public function supportsAjaxStatus() |
252 | { |
253 | return $this->config->enableAjaxStatus ?? true; |
254 | } |
255 | |
256 | /** |
257 | * Get Overdrive Access |
258 | * |
259 | * Pass-through to the connector to determine whether logged-in user |
260 | * has access to Overdrive actions |
261 | * |
262 | * @return bool Whether the logged-in user has access to Overdrive. |
263 | */ |
264 | public function getOverdriveAccess() |
265 | { |
266 | return $this->connector->getAccess(); |
267 | } |
268 | |
269 | /** |
270 | * Is Logged in |
271 | * |
272 | * Returns whether the current user is logged in |
273 | * |
274 | * @return bool |
275 | */ |
276 | public function isLoggedIn() |
277 | { |
278 | return $this->connector->getUser() ? true : false; |
279 | } |
280 | |
281 | /** |
282 | * Get Overdrive ID |
283 | * |
284 | * Returns the Overdrive ID (or resource ID) for the current item. Note: for |
285 | * records in marc format, this may be different than the Solr Record ID |
286 | * |
287 | * @return string OverdriveID |
288 | * @throws \Exception |
289 | */ |
290 | public function getOverdriveID() |
291 | { |
292 | $result = 0; |
293 | |
294 | if ($this->config) { |
295 | if ($this->getIsMarc()) { |
296 | $field = $this->config->idField; |
297 | $subfield = $this->config->idSubfield; |
298 | $result = strtolower( |
299 | $this->getFieldArray($field, $subfield)[0] ?? '' |
300 | ); |
301 | } else { |
302 | $result = strtolower($this->getUniqueID()); |
303 | } |
304 | } |
305 | return $result; |
306 | } |
307 | |
308 | /** |
309 | * Returns the availability for the current record |
310 | * |
311 | * @return object|bool returns an object with the info in it (see URL above) |
312 | * or false if there was a problem. |
313 | * @throws \Exception |
314 | */ |
315 | public function getOverdriveAvailability() |
316 | { |
317 | $overDriveId = $this->getOverdriveID(); |
318 | return $this->connector->getAvailability($overDriveId); |
319 | } |
320 | |
321 | /** |
322 | * Returns a boolean indicating if patron actions are supported |
323 | * |
324 | * @return bool |
325 | */ |
326 | public function supportsPatronActions() |
327 | { |
328 | return $this->config->usePatronAPI; |
329 | } |
330 | |
331 | /** |
332 | * Is Checked Out |
333 | * |
334 | * Is this resource already checked out to the user? |
335 | * |
336 | * @return object Returns the checkout information if currently checked out |
337 | * by this user or false in the data property if not. |
338 | * @throws \Exception |
339 | */ |
340 | public function isCheckedOut() |
341 | { |
342 | $result = $this->connector->getResultObject(); |
343 | if ($this->isLoggedIn() && $this->supportsPatronActions()) { |
344 | $overdriveID = $this->getOverdriveID(); |
345 | $result = $this->connector->getCheckouts(true); |
346 | if ($result->status) { |
347 | $checkedout = false; |
348 | $checkouts = $result->data; |
349 | // In case of a magazine issue, we have to get all the checkouts to see if the |
350 | // current title is the parentID of one of the user's checkouts. Return data as |
351 | // array in case there are multiple issues checked out to the user. |
352 | $result->data = []; |
353 | $result->isMagazine = false; |
354 | foreach ($checkouts as $checkout) { |
355 | if ($checkout->metadata->mediaType == 'Magazine') { |
356 | $idToCheck = strtolower($checkout->metadata->parentMagazineReferenceId); |
357 | } else { |
358 | $idToCheck = $checkout->reserveId; |
359 | } |
360 | if (strtolower($idToCheck) == $overdriveID) { |
361 | $checkedout = true; |
362 | $result->status = true; |
363 | $result->data[] = $checkout; |
364 | //this checkout is a magazine issue of the current title |
365 | if ($checkout->metadata->mediaType == 'Magazine') { |
366 | $result->isMagazine = true; |
367 | } |
368 | } |
369 | } |
370 | if (!$checkedout) { |
371 | $result->data = false; |
372 | } |
373 | } |
374 | } |
375 | return $result; |
376 | } |
377 | |
378 | /** |
379 | * Is Held |
380 | * Checks to see if the current record is on hold through Overcdrive. |
381 | * |
382 | * @return object|bool Returns the hold info if on hold or false if not. |
383 | * @throws \Exception |
384 | */ |
385 | public function isHeld() |
386 | { |
387 | if ($this->isLoggedIn() && $this->supportsPatronActions()) { |
388 | $overDriveId = $this->getOverdriveID(); |
389 | $result = $this->connector->getHolds(true); |
390 | if ($result->status) { |
391 | $holds = $result->data; |
392 | foreach ($holds as $hold) { |
393 | if (strtolower($hold->reserveId) == $overDriveId) { |
394 | return $hold; |
395 | } |
396 | } |
397 | } |
398 | } |
399 | // If it didn't work, an error should be logged from the connector |
400 | return false; |
401 | } |
402 | |
403 | /** |
404 | * Get Bread Crumb |
405 | * |
406 | * @return string |
407 | */ |
408 | public function getBreadcrumb() |
409 | { |
410 | $short = $this->getShortTitle(); |
411 | return $short ? $short : $this->getTitle(); |
412 | } |
413 | |
414 | /** |
415 | * Get Marc Reader |
416 | * |
417 | * Override the base marc trait to return an empty marc reader object if no MARC |
418 | * is available. |
419 | * |
420 | * @return \VuFind\Marc\MarcReader |
421 | */ |
422 | public function getMarcReader() |
423 | { |
424 | return $this->getIsMarc() |
425 | ? parent::getMarcReader() |
426 | : new $this->marcReaderClass('<record></record>'); |
427 | } |
428 | |
429 | /** |
430 | * Get Title Section |
431 | * |
432 | * @return string |
433 | */ |
434 | public function getTitleSection() |
435 | { |
436 | return $this->getIsMarc() |
437 | ? parent::getTitleSection() |
438 | : ''; // I don't think Overdrive has this metadata |
439 | } |
440 | |
441 | /** |
442 | * Get general notes on the record. |
443 | * |
444 | * @return array |
445 | */ |
446 | public function getGeneralNotes() |
447 | { |
448 | return $this->getIsMarc() ? parent::getGeneralNotes() : []; |
449 | } |
450 | |
451 | /** |
452 | * Returns one of three things: a full URL to a thumbnail preview of the |
453 | * record if an image is available in an external system; an array of |
454 | * parameters to send to VuFind's internal cover generator if no fixed URL |
455 | * exists; or false if no thumbnail can be generated. |
456 | * |
457 | * @param string $size Size of thumbnail (small, medium or large -- small |
458 | * is |
459 | * default). |
460 | * |
461 | * @return string|array|bool |
462 | * @throws \Exception |
463 | */ |
464 | public function getThumbnail($size = 'small') |
465 | { |
466 | $coverMap = [ |
467 | 'large' => 'cover300Wide', |
468 | 'medium' => 'cover150Wide', |
469 | 'small' => 'thumbnail', |
470 | ]; |
471 | $cover = $coverMap[$size] ?? 'cover'; |
472 | |
473 | // If the record is marc then the cover links probably aren't there. |
474 | if ($this->getIsMarc()) { |
475 | $od_id = $this->getOverdriveID(); |
476 | $fulldata = $this->connector->getMetadata([$od_id]); |
477 | $data = $fulldata[strtolower($od_id)] ?? null; |
478 | } else { |
479 | $jsonData = $this->fields['fullrecord']; |
480 | $data = json_decode($jsonData, false); |
481 | } |
482 | return $data->images->{$cover}->href ?? false; |
483 | } |
484 | |
485 | /** |
486 | * Get an array of summary strings for the record. |
487 | * |
488 | * @return array |
489 | */ |
490 | public function getSummary() |
491 | { |
492 | if ($this->getIsMarc()) { |
493 | return parent::getSummary(); |
494 | } |
495 | // Non-MARC case: |
496 | $desc = $this->fields['description'] ?? ''; |
497 | |
498 | $newDesc = preg_replace('/’/i', '', $desc); |
499 | $newDesc = strip_tags($newDesc); |
500 | return [$newDesc]; |
501 | } |
502 | |
503 | /** |
504 | * Is Marc Based Record |
505 | * |
506 | * Return whether this is a marc-based record. |
507 | * |
508 | * @return bool |
509 | */ |
510 | public function getIsMarc() |
511 | { |
512 | return $this->config->isMarc; |
513 | } |
514 | |
515 | /** |
516 | * Get all subject headings associated with this record. Each heading is |
517 | * returned as an array of chunks, increasing from least specific to most |
518 | * specific. |
519 | * |
520 | * @param bool $extended Whether to return a keyed array with the following |
521 | * keys: |
522 | * - heading: the actual subject heading chunks |
523 | * - type: heading type |
524 | * - source: source vocabulary |
525 | * |
526 | * @return array |
527 | */ |
528 | public function getAllSubjectHeadings($extended = false) |
529 | { |
530 | if (!$this->config) { |
531 | return []; |
532 | } |
533 | return $this->getIsMarc() |
534 | ? parent::getAllSubjectHeadings($extended) |
535 | : DefaultRecord::getAllSubjectHeadings($extended); |
536 | } |
537 | |
538 | /** |
539 | * Get Formatted Raw Data |
540 | * |
541 | * Returns the raw data formatted for staff display tab |
542 | * |
543 | * @return array Multidimensional array with data |
544 | */ |
545 | public function getFormattedRawData() |
546 | { |
547 | $jsonData = $this->fields['fullrecord']; |
548 | $data = json_decode($jsonData, true); |
549 | $c_arr = []; |
550 | foreach ($data['creators'] as $creator) { |
551 | $c_arr[] = "<strong>{$creator['role']}<strong>: " |
552 | . $creator['name']; |
553 | } |
554 | $data['creators'] = implode('<br>', $c_arr); |
555 | return $data; |
556 | } |
557 | |
558 | /** |
559 | * Get a link for placing a title level hold. |
560 | * |
561 | * @return mixed A url if a hold is possible, boolean false if not |
562 | */ |
563 | public function getRealTimeTitleHold() |
564 | { |
565 | $od_id = $this->getOverdriveID(); |
566 | $rec_id = $this->getUniqueID(); |
567 | $urlDetails = [ |
568 | 'action' => 'Hold', |
569 | 'record' => $rec_id, |
570 | 'query' => "od_id=$od_id&rec_id=$rec_id", |
571 | 'anchor' => '', |
572 | ]; |
573 | return $urlDetails; |
574 | } |
575 | |
576 | /** |
577 | * Return an array of associative URL arrays with one or more of the following |
578 | * keys: |
579 | * |
580 | * <li> |
581 | * <ul>desc: URL description text to display (optional)</ul> |
582 | * <ul>url: fully-formed URL (required if 'route' is absent)</ul> |
583 | * <ul>route: VuFind route to build URL with (required if 'url' is absent)</ul> |
584 | * <ul>routeParams: Parameters for route (optional)</ul> |
585 | * <ul>queryString: Query params to append after building route (optional)</ul> |
586 | * </li> |
587 | * |
588 | * @return array |
589 | */ |
590 | public function getURLs() |
591 | { |
592 | return $this->getIsMarc() |
593 | ? parent::getURLs() |
594 | : $this->getPermanentLink(); |
595 | } |
596 | |
597 | /** |
598 | * Get Permanent Link to the resource on your institution's OverDrive site |
599 | * |
600 | * @return array the permanent link to the resource |
601 | */ |
602 | public function getPermanentLink() |
603 | { |
604 | if (!empty($libraryURL = $this->config->libraryURL)) { |
605 | $data = json_decode($this->fields['fullrecord'], false); |
606 | $desc = $this->translate('od_resource_page'); |
607 | $permlink = "$libraryURL/media/" . $data->crossRefId; |
608 | return [['url' => $permlink, 'desc' => $desc ?: $permlink]]; |
609 | } else { |
610 | return []; |
611 | } |
612 | } |
613 | } |