File Coverage

blib/lib/WWW/Zotero.pm
Criterion Covered Total %
statement 27 233 11.5
branch 0 112 0.0
condition 0 18 0.0
subroutine 9 40 22.5
pod 23 25 92.0
total 59 428 13.7


line stmt bran cond sub pod time code
1             package WWW::Zotero;
2              
3             =pod
4              
5             =encoding utf-8
6              
7             =head1 NAME
8              
9             WWW::Zotero - Perl interface to the Zotero API
10              
11             =head1 SYNOPSIS
12              
13             use WWW::Zotero;
14              
15             my $client = WWW::Zotero->new;
16             my $client = WWW::Zotero->new(key => 'API-KEY');
17              
18             my $data = $client->itemTypes();
19              
20             for my $item (@$data) {
21             print "%s\n" , $item->itemType;
22             }
23              
24             my $data = $client->itemFields();
25             my $data = $client->itemTypeFields('book');
26             my $data = $client->itemTypeCreatorTypes('book');
27             my $data = $client->creatorFields();
28             my $data = $client->itemTemplate('book');
29             my $key = $client->keyPermissions();
30             my $groups = $client->userGroups($userID);
31              
32             my $data = $client->listItems(user => '475425', limit => 5);
33             my $data = $client->listItems(user => '475425', format => 'atom');
34             my $generator = $client->listItems(user => '475425', generator => 1);
35              
36             while (my $item = $generator->()) {
37             print "%s\n" , $item->{title};
38             }
39              
40             my $data = $client->listItemsTop(user => '475425', limit => 5);
41             my $data = $client->listItemsTrash(user => '475425');
42             my $data = $client->getItem(user => '475425', itemKey => 'TTJFTW87');
43             my $data = $client->getItemTags(user => '475425', itemKey => 'X42A7DEE');
44             my $data = $client->listTags(user => '475425');
45             my $data = $client->listTags(user => '475425', tag => 'Biography');
46             my $data = $client->listCollections(user => '475425');
47             my $data = $client->listCollectionsTop(user => '475425');
48             my $data = $client->getCollection(user => '475425', collectionKey => 'A5G9W6AX');
49             my $data = $client->listSubCollections(user => '475425', collectionKey => 'QM6T3KHX');
50             my $data = $client->listCollectionItems(user => '475425', collectionKey => 'QM6T3KHX');
51             my $data = $client->listCollectionItemsTop(user => '475425', collectionKey => 'QM6T3KHX');
52             my $data = $client->listCollectionItemsTags(user => '475425', collectionKey => 'QM6T3KHX');
53             my $data = $client->listSearches(user => '475425');
54              
55             =cut
56              
57 1     1   13524 use Moo;
  1         9274  
  1         6  
58 1     1   1570 use JSON;
  1         9506  
  1         7  
59 1     1   509 use URI::Escape;
  1         1113  
  1         58  
60 1     1   408 use REST::Client;
  1         32540  
  1         33  
61 1     1   518 use Data::Dumper;
  1         4738  
  1         56  
62 1     1   413 use POSIX qw(strftime);
  1         4275  
  1         5  
63 1     1   1114 use Carp;
  1         2  
  1         41  
64 1     1   371 use Log::Any ();
  1         5819  
  1         19  
65 1     1   14 use feature 'state';
  1         2  
  1         2050  
66              
67             our $VERSION = '0.04';
68              
69             =head1 CONFIGURATION
70              
71             =over 4
72              
73             =item baseurl
74              
75             The base URL for all API requests. Default 'https://api.zotero.org'.
76              
77             =item version
78              
79             The API version. Default '3'.
80              
81             =item key
82              
83             The API key which can be requested via https://api.zotero.org.
84              
85             =item modified_since
86              
87             Include a UNIX time to be used in a If-Modified-Since header to allow for caching
88             of results by your application.
89              
90             =back
91              
92             =cut
93             has baseurl => (is => 'ro' , default => sub { 'https://api.zotero.org' });
94             has modified_since => (is => 'ro');
95             has version => (is => 'ro' , default => sub { '3'});
96             has key => (is => 'ro');
97             has code => (is => 'rw');
98             has sleep => (is => 'rw' , default => sub { 0 });
99             has log => (is => 'lazy');
100             has client => (is => 'lazy');
101              
102             sub _build_client {
103 0     0     my ($self) = @_;
104 0           my $client = REST::Client->new();
105              
106 0           $self->log->debug("< Zotero-API-Version: " . $self->version);
107 0           $client->addHeader('Zotero-API-Version', $self->version);
108              
109 0 0         if (defined $self->key) {
110 0           my $authorization = 'Bearer ' . $self->key;
111 0           $self->log->debug("< Authorization: " . $authorization);
112 0           $client->addHeader('Authorization', $authorization);
113             }
114              
115 0 0         if (defined $self->modified_since) {
116 0           my $date = strftime "%a, %d %b %Y %H:%M:%S GMT" , gmtime($self->modified_since);
117 0           $self->log->debug("< If-Modified-Since: " . $date);
118 0           $client->addHeader('If-Modified-Since',$date);
119             }
120              
121 0           $client;
122             }
123              
124             sub _build_log {
125 0     0     my ($self) = @_;
126 0           Log::Any->get_logger(category => ref($self));
127             }
128              
129             sub _zotero_get_request {
130 0     0     my ($self,$path,%param) = @_;
131              
132 0           my $url = sprintf "%s%s" , $self->baseurl, $path;
133              
134 0           my @params = ();
135 0           for my $name (keys %param) {
136 0           my $value = $param{$name};
137 0           push @params , uri_escape($name) . "=" . uri_escape($value);
138             }
139              
140 0 0         $url .= '?' . join("&",@params) if @params > 0;
141              
142             # The server asked us to sleep..
143 0 0         if ($self->sleep > 0) {
144 0           $self->log->debug("sleeping: " . $self->sleep . " seconds");
145 0           sleep $self->sleep;
146 0           $self->sleep(0)
147             }
148              
149 0           $self->log->debug("requesting: $url");
150 0           my $response = $self->client->GET($url);
151              
152 0   0       my $backoff = $response->responseHeader('Backoff') // 0;
153 0   0       my $retryAfter = $response->responseHeader('Retry-After') // 0;
154 0           my $code = $response->responseCode();
155              
156 0           $self->log->debug("> Code: $code");
157 0           $self->log->debug("> Backoff: $backoff");
158 0           $self->log->debug("> Retry-After: $retryAfter");
159              
160 0 0 0       if ($backoff > 0) {
    0          
161 0           $self->sleep($backoff);
162             }
163             elsif ($code eq '429' || $code eq '503') {
164 0   0       $self->sleep($retryAfter // 60);
165 0           return undef;
166             }
167              
168 0           $self->log->debug("> Content: " . $response->responseContent);
169              
170 0           $self->code($code);
171              
172 0 0         return undef unless $code eq '200';
173              
174 0           $response;
175             }
176              
177             =head1 METHODS
178              
179             =cut
180              
181             =head2 username2userID
182              
183             Find the userID based on a username
184              
185             =cut
186             sub username2userID {
187 0     0 1   my ($self,$username) = @_;
188              
189 0 0         croak "username2userID: need username" unless defined $username;
190              
191 0           my $url = sprintf "https://www.zotero.org/%s" , uri_escape($username);
192              
193 0           my $response = $self->client->GET($url);
194              
195 0 0         return undef unless $response->responseCode() eq '200';
196              
197 0           my $content = $response->responseContent;
198              
199 0 0         if ($content =~ /profileUserID:\s*(\d+)/) {
200 0           return $1;
201             }
202             else {
203 0           return undef;
204             }
205             }
206              
207             =head2 itemTypes()
208              
209             Get all item types. Returns a Perl array.
210              
211             =cut
212             sub itemTypes {
213 0     0 1   my ($self) = @_;
214              
215 0           my $response = $self->_zotero_get_request('/itemTypes');
216              
217 0 0         return undef unless $response;
218              
219 0           decode_json $response->responseContent;
220             }
221              
222             =head2 itemTypes()
223              
224             Get all item fields. Returns a Perl array.
225              
226             =cut
227             sub itemFields {
228 0     0 0   my ($self) = @_;
229              
230 0           my $response = $self->_zotero_get_request('/itemFields');
231              
232 0 0         return undef unless $response;
233              
234 0           decode_json $response->responseContent;
235             }
236              
237             =head2 itemTypes($type)
238              
239             Get all valid fields for an item type. Returns a Perl array.
240              
241             =cut
242             sub itemTypeFields {
243 0     0 0   my ($self,$itemType) = @_;
244              
245 0 0         croak "itemTypeFields: need itemType" unless defined $itemType;
246              
247 0           my $response = $self->_zotero_get_request('/itemTypeFields', itemType => $itemType);
248              
249 0 0         return undef unless $response;
250              
251 0           decode_json $response->responseContent;
252             }
253              
254             =head2 itemTypeCreatorTypes($type)
255              
256             Get valid creator types for an item type. Returns a Perl array.
257              
258             =cut
259             sub itemTypeCreatorTypes {
260 0     0 1   my ($self,$itemType) = @_;
261              
262 0 0         croak "itemTypeCreatorTypes: need itemType" unless defined $itemType;
263              
264 0           my $response = $self->_zotero_get_request('/itemTypeCreatorTypes', itemType => $itemType);
265              
266 0 0         return undef unless $response;
267              
268 0           decode_json $response->responseContent;
269             }
270              
271             =head2 creatorFields()
272              
273             Get localized creator fields. Returns a Perl array.
274              
275             =cut
276             sub creatorFields {
277 0     0 1   my ($self) = @_;
278              
279 0           my $response = $self->_zotero_get_request('/creatorFields');
280              
281 0 0         return undef unless $response;
282              
283 0           decode_json $response->responseContent;
284             }
285              
286             =head2 itemTemplate($type)
287              
288             Get a template for a new item. Returns a Perl hash.
289              
290             =cut
291             sub itemTemplate {
292 0     0 1   my ($self,$itemType) = @_;
293              
294 0 0         croak "itemTemplate: need itemType" unless defined $itemType;
295              
296 0           my $response = $self->_zotero_get_request('/items/new', itemType => $itemType);
297              
298 0 0         return undef unless $response;
299              
300 0           decode_json $response->responseContent;
301             }
302              
303             =head2 keyPermissions($key)
304              
305             Return the userID and premissions for the given API key.
306              
307             =cut
308             sub keyPermissions {
309 0     0 1   my ($self,$key) = @_;
310              
311 0 0         $key = $self->key unless defined $key;
312              
313 0 0         croak "keyPermissions: need key" unless defined $key;
314              
315 0           my $response = $self->_zotero_get_request("/keys/$key");
316              
317 0 0         return undef unless $response;
318              
319 0           decode_json $response->responseContent;
320             }
321              
322             =head2 userGroups($userID)
323              
324             Return an array of the set of groups the current API key as access to.
325              
326             =cut
327             sub userGroups {
328 0     0 1   my ($self,$userID) = @_;
329              
330 0 0         croak "userGroups: need userID" unless defined $userID;
331              
332 0           my $response = $self->_zotero_get_request("/users/$userID/groups");
333              
334 0 0         return undef unless $response;
335              
336 0           decode_json $response->responseContent;
337             }
338              
339             =head2 listItems(user => $userID, %options)
340              
341             =head2 listItems(group => $groupID, %options)
342              
343             List all items for a user or ar group. Optionally provide a list of options:
344              
345             sort - dateAdded, dateModified, title, creator, type, date, publisher,
346             publicationTitle, journalAbbreviation, language, accessDate,
347             libraryCatalog, callNumber, rights, addedBy, numItems (default dateModified)
348             direction - asc, desc
349             limit - integer 1-100* (default 25)
350             start - integer
351             format - perl, atom, bib, json, keys, versions , bibtex , bookmarks,
352             coins, csljson, mods, refer, rdf_bibliontology , rdf_dc ,
353             rdf_zotero, ris , tei , wikipedia (default perl)
354              
355             when format => 'json'
356              
357             include - bib, data
358              
359             when format => 'atom'
360              
361             content - bib, html, json
362              
363             when format => 'bib' or content => 'bib'
364              
365             style - chicago-note-bibliography, apa, ... (see: https://www.zotero.org/styles/)
366              
367              
368             itemKey - A comma-separated list of item keys. Valid only for item requests. Up to
369             50 items can be specified in a single request.
370             itemType - Item type search
371             q - quick search
372             qmode - titleCreatorYear, everything
373             since - integer
374             tag - Tag search
375              
376             See: https://www.zotero.org/support/dev/web_api/v3/basics#user_and_group_library_urls
377             for the search syntax.
378              
379             Returns a Perl HASH containing the total number of hits plus the results:
380              
381             {
382             total => '132',
383             results =>
384             }
385              
386             =head2 listItems(user => $userID | group => $groupID, generator => 1 , %options)
387              
388             Same as listItems but this return a generator for every record found. Use this
389             method to sequentially read the complete resultset. E.g.
390              
391             my $generator = $self->listItems(user => '231231', generator);
392              
393             while (my $record = $generator->()) {
394             printf "%s\n" , $record->{title};
395             }
396              
397             The format is implicit 'perl' in this case.
398              
399             =cut
400             sub listItems {
401 0     0 1   my ($self,%options) = @_;
402              
403 0           $self->_listItems(%options, path => 'items');
404             }
405              
406             sub _listItems {
407 0     0     my ($self,%options) = @_;
408              
409 0           my $userID = $options{user};
410 0           my $groupID = $options{group};
411              
412 0 0 0       croak "listItems: need user or group" unless defined $userID || defined $groupID;
413              
414 0 0         my $id = defined $userID ? $userID : $groupID;
415 0 0         my $type = defined $userID ? 'users' : 'groups';
416              
417 0           my $generator = $options{generator};
418 0           my $path = $options{path};
419              
420 0           delete $options{generator};
421 0           delete $options{path};
422 0           delete $options{user};
423 0           delete $options{group};
424 0 0 0       delete $options{format} if exists $options{format} && $options{format} eq 'perl';
425              
426 0 0         $options{limit} = 25 unless defined $options{limit};
427              
428 0 0         if ($generator) {
429 0           delete $options{format};
430 0 0         $options{start} = 0 unless defined $options{start};
431              
432             return sub {
433 0     0     state $response = $self->_listItems_request("/$type/$id/$path", %options);
434 0           state $idx = 0;
435              
436 0 0         return undef unless defined $response;
437 0 0         return undef if $response->{total} == 0;
438 0 0         return undef if $options{start} + $idx + 1 > $response->{total};
439              
440 0 0         unless (defined $response->{results}->[$idx]) {
441 0           $options{start} += $options{limit};
442 0           $response = $self->_listItems_request("/$type/$id/$path", %options);
443 0           $idx = 0;
444             }
445              
446 0 0         return undef unless defined $response;
447              
448 0           my $doc = $response->{results}->[$idx];
449 0           my $id = $doc->{key};
450              
451 0           $idx++;
452              
453 0           { _id => $id , %$doc };
454 0           };
455             }
456             else {
457 0           return $self->_listItems_request("/$type/$id/$path", %options);
458             }
459             }
460              
461             sub _listItems_request {
462 0     0     my ($self,$path,%options) = @_;
463 0           my $response = $self->_zotero_get_request($path, %options);
464              
465 0 0         return undef unless defined $response;
466              
467 0           my $total = $response->responseHeader('Total-Results');
468 0           my $link = $response->responseHeader('Link');
469              
470 0 0         $self->log->debug("> Total-Results: $total") if defined $total;
471 0 0         $self->log->debug("> Link: $link") if defined $link;
472              
473 0           my $results = $response->responseContent;
474              
475 0 0         return undef unless $results;
476              
477 0 0 0       if (! defined $options{format} || $options{format} eq 'perl') {
478 0           $results = decode_json $results;
479             }
480              
481             return {
482 0           total => $total,
483             results => $results
484             };
485             }
486              
487             =head2 listItemsTop(user => $userID | group => $groupID, %options)
488              
489             The set of all top-level items in the library, excluding trashed items.
490              
491             See 'listItems(...)' functions above for all the execution options.
492              
493             =cut
494             sub listItemsTop {
495 0     0 1   my ($self,%options) = @_;
496              
497 0           $self->_listItems(%options, path => 'items/top');
498             }
499              
500             =head2 listItemsTrash(user => $userID | group => $groupID, %options)
501              
502             The set of items in the trash.
503              
504             See 'listItems(...)' functions above for all the execution options.
505              
506             =cut
507             sub listItemsTrash {
508 0     0 1   my ($self,%options) = @_;
509              
510 0           $self->_listItems(%options, path => 'items/trash');
511             }
512              
513             =head2 getItem(itemKey => ... , user => $userID | group => $groupID, %options)
514              
515             A specific item in the library.
516              
517             See 'listItems(...)' functions above for all the execution options.
518              
519             Returns the item if found.
520              
521             =cut
522             sub getItem {
523 0     0 1   my ($self,%options) = @_;
524              
525 0           my $key = $options{itemKey};
526              
527 0 0         croak "getItem: need itemKey" unless defined $key;
528              
529 0           delete $options{itemKey};
530              
531 0           my $result = $self->_listItems(%options, path => "items/$key");
532              
533 0 0         return undef unless defined $result;
534              
535 0           $result->{results};
536             }
537              
538             =head2 getItemChildren(itemKey => ... , user => $userID | group => $groupID, %options)
539              
540             The set of all child items under a specific item.
541              
542             See 'listItems(...)' functions above for all the execution options.
543              
544             Returns the children if found.
545              
546             =cut
547             sub getItemChildren {
548 0     0 1   my ($self,%options) = @_;
549              
550 0           my $key = $options{itemKey};
551              
552 0 0         croak "getItem: need itemKey" unless defined $key;
553              
554 0           delete $options{itemKey};
555              
556 0           my $result = $self->_listItems(%options, path => "items/$key/children");
557              
558 0 0         return undef unless defined $result;
559              
560 0           $result->{results};
561             }
562              
563             =head2 getItemTags(itemKey => ... , user => $userID | group => $groupID, %options)
564              
565             The set of all tags associated with a specific item.
566              
567             See 'listItems(...)' functions above for all the execution options.
568              
569             Returns the tags if found.
570              
571             =cut
572             sub getItemTags {
573 0     0 1   my ($self,%options) = @_;
574              
575 0           my $key = $options{itemKey};
576              
577 0 0         croak "getItem: need itemKey" unless defined $key;
578              
579 0           delete $options{itemKey};
580              
581 0           my $result = $self->_listItems(%options, path => "items/$key/tags");
582              
583 0 0         return undef unless defined $result;
584              
585 0           $result->{results};
586             }
587              
588             =head2 listTags(user => $userID | group => $groupID, [tag => $name] , %options)
589              
590             The set of tags (i.e., of all types) matching a specific name.
591              
592             See 'listItems(...)' functions above for all the execution options.
593              
594             Returns the list of tags.
595              
596             =cut
597             sub listTags {
598 0     0 1   my ($self,%options) = @_;
599              
600 0           my $tag = $options{tag};
601              
602 0           delete $options{tag};
603              
604 0 0         my $path = defined $tag ? "tags/" . uri_escape($tag) : "tags";
605              
606 0           $self->_listItems(%options, path => $path);
607             }
608              
609             =head2 listCollections(user => $userID | group => $groupID , %options)
610              
611             The set of all collections in the library.
612              
613             See 'listItems(...)' functions above for all the execution options.
614              
615             Returns the list of collections.
616              
617             =cut
618             sub listCollections {
619 0     0 1   my ($self,%options) = @_;
620              
621 0           $self->_listItems(%options, path => "/collections");
622             }
623              
624             =head2 listCollectionsTop(user => $userID | group => $groupID , %options)
625              
626             The set of all top-level collections in the library.
627              
628             See 'listItems(...)' functions above for all the execution options.
629              
630             Returns the list of collections.
631              
632             =cut
633             sub listCollectionsTop {
634 0     0 1   my ($self,%options) = @_;
635              
636 0           $self->_listItems(%options, path => "collections/top");
637             }
638              
639             =head2 getCollection(collectionKey => ... , user => $userID | group => $groupID, %options)
640              
641             A specific item in the library.
642              
643             See 'listItems(...)' functions above for all the execution options.
644              
645             Returns the collection if found.
646              
647             =cut
648             sub getCollection {
649 0     0 1   my ($self,%options) = @_;
650              
651 0           my $key = $options{collectionKey};
652              
653 0 0         croak "getCollection: need collectionKey" unless defined $key;
654              
655 0           delete $options{collectionKey};
656              
657 0           my $result = $self->_listItems(%options, path => "collections/$key");
658              
659 0 0         return undef unless defined $result;
660              
661 0           $result->{results};
662             }
663              
664             =head2 listSubCollections(collectionKey => ...., user => $userID | group => $groupID , %options)
665              
666             The set of subcollections within a specific collection in the library.
667              
668             See 'listItems(...)' functions above for all the execution options.
669              
670             Returns the list of (sub)collections.
671              
672             =cut
673             sub listSubCollections {
674 0     0 1   my ($self,%options) = @_;
675              
676 0           my $key = $options{collectionKey};
677              
678 0 0         croak "listSubCollections: need collectionKey" unless defined $key;
679              
680 0           delete $options{collectionKey};
681              
682 0           $self->_listItems(%options, path => "collections/$key/collections");
683             }
684              
685             =head2 listCollectionItems(collectionKey => ...., user => $userID | group => $groupID , %options)
686              
687             The set of all items within a specific collection in the library.
688              
689             See 'listItems(...)' functions above for all the execution options.
690              
691             Returns the list of items.
692              
693             =cut
694             sub listCollectionItems {
695 0     0 1   my ($self,%options) = @_;
696              
697 0           my $key = $options{collectionKey};
698              
699 0 0         croak "listCollectionItems: need collectionKey" unless defined $key;
700              
701 0           delete $options{collectionKey};
702              
703 0           $self->_listItems(%options, path => "collections/$key/items");
704             }
705              
706             =head2 listCollectionItemsTop(collectionKey => ...., user => $userID | group => $groupID , %options)
707              
708             The set of top-level items within a specific collection in the library.
709              
710             See 'listItems(...)' functions above for all the execution options.
711              
712             Returns the list of items.
713              
714             =cut
715             sub listCollectionItemsTop {
716 0     0 1   my ($self,%options) = @_;
717              
718 0           my $key = $options{collectionKey};
719              
720 0 0         croak "listCollectionItemsTop: need collectionKey" unless defined $key;
721              
722 0           delete $options{collectionKey};
723              
724 0           $self->_listItems(%options, path => "collections/$key/items/top");
725             }
726              
727             =head2 listCollectionItemsTags(collectionKey => ...., user => $userID | group => $groupID , %options)
728              
729             The set of tags within a specific collection in the library.
730              
731             See 'listItems(...)' functions above for all the execution options.
732              
733             Returns the list of items.
734              
735             =cut
736             sub listCollectionItemsTags {
737 0     0 1   my ($self,%options) = @_;
738              
739 0           my $key = $options{collectionKey};
740              
741 0 0         croak "listCollectionItemsTop: need collectionKey" unless defined $key;
742              
743 0           delete $options{collectionKey};
744              
745 0           $self->_listItems(%options, path => "collections/$key/tags");
746             }
747              
748             =head2 listSearches(user => $userID | group => $groupID , %options)
749              
750             The set of all saved searches in the library.
751              
752             See 'listItems(...)' functions above for all the execution options.
753              
754             Returns the list of saved searches.
755              
756             =cut
757             sub listSearches {
758 0     0 1   my ($self,%options) = @_;
759              
760 0           $self->_listItems(%options, path => "searches");
761             }
762              
763             =head2 getSearch(searchKey => ... , user => $userID | group => $groupID, %options)
764              
765             A specific saved search in the library.
766              
767             See 'listItems(...)' functions above for all the execution options.
768              
769             Returns the saved search if found.
770              
771             =cut
772             sub getSearch {
773 0     0 1   my ($self,%options) = @_;
774              
775 0           my $key = $options{searchKey};
776              
777 0 0         croak "getSearch: need searchKey" unless defined $key;
778              
779 0           delete $options{searchKey};
780              
781 0           my $result = $self->_listItems(%options, path => "search/$key");
782              
783 0 0         return undef unless defined $result;
784              
785 0           $result->{results};
786             }
787              
788             =head1 AUTHOR
789              
790             Patrick Hochstenbach, C<< >>
791              
792             =head1 CONTRIBUTORS
793              
794             François Rappaz
795              
796             =head1 LICENSE AND COPYRIGHT
797              
798             Copyright 2015 Patrick Hochstenbach
799              
800             This program is free software; you can redistribute it and/or modify it
801             under the terms of either: the GNU General Public License as published
802             by the Free Software Foundation; or the Artistic License.
803              
804             See http://dev.perl.org/licenses/ for more information.
805              
806             =cut
807              
808             1;