]> git.donarmstrong.com Git - bin.git/blob - bibtex_to_paper
use transactions and only commit after loading all of the files
[bin.git] / bibtex_to_paper
1 #!/usr/bin/perl
2 # bibtex_to_paper opens the paper corresponding to a bibtex key
3 # and is released under the terms of the GNU GPL version 3, or any
4 # later version, at your option. See the file README and COPYING for
5 # more information.
6 # Copyright 2014 by Don Armstrong <don@donarmstrong.com>.
7
8
9 use warnings;
10 use strict;
11
12 use Getopt::Long;
13 use Pod::Usage;
14
15 use File::Find;
16 use File::Basename qw(basename);
17 use File::Spec qw(rel2abs);
18 use Text::BibTeX;
19 use User;
20 use Data::Printer;
21 use POSIX;
22
23 use DBI;
24
25 =head1 NAME
26
27 bibtex_to_paper - opens the paper corresponding to a bibtex key
28
29 =head1 SYNOPSIS
30
31 bibtex_to_paper [options] bibtexkey
32
33  Options:
34   --bibtex, -b bibtex file to look up key in
35   --bibtex-cache, -c bibtex cache file
36   --build-cache, -B build cache using bibtex files
37   --search-by-pmid Search term is a pmid instead of a bibtex key
38   --pdf-dir pdf directory
39   --pdfviewer, -p pdf viewer to use
40   --only-print Only print PDF file name
41   --debug, -d debugging level (Default 0)
42   --help, -h display this help
43   --man, -m display manual
44
45 =head1 OPTIONS
46
47 =over
48
49 =item B<--bibtex, -b>
50
51 Bibtex file to look key up in
52
53 =item B<--bibtex-cache, -c>
54
55 Bibtex cache file; rebuilt if bibtex file changes
56
57 =item B<--pdfviewer, -p>
58
59 PDF viewer to use; defaults to evince unless a .xoj exists, in which
60 case xournal is used.
61
62 =item B<--only-print>
63
64 Only print the PDF file name, don't open it.
65
66 =item B<--debug, -d>
67
68 Debug verbosity. (Default 0)
69
70 =item B<--help, -h>
71
72 Display brief usage information.
73
74 =item B<--man, -m>
75
76 Display this manual.
77
78 =back
79
80 =head1 EXAMPLES
81
82 bibtex_to_paper
83
84 =cut
85
86
87 use vars qw($DEBUG);
88
89 my %options = (debug           => 0,
90                help            => 0,
91                man             => 0,
92                only_print      => 0,
93                search_by_pmid  => 0,
94                'bibtex_cache'  => File::Spec->catfile(User->Home,'.bibtex_to_paper_cache'),
95               );
96
97 GetOptions(\%options,
98            'build_cache|build-cache!',
99            'bibtex|b=s@',
100            'bibtex_cache|bibtex-cache|c=s',
101            'pdfviewer|p=s',
102            'only_print|only-print!',
103            'search_by_pmid|search-by-pmid!',
104            'clear_cache|clear-cache!',
105            'papers_directory|papers-directory=s@',
106            'debug|d+','help|h|?','man|m');
107
108 pod2usage() if $options{help};
109 pod2usage({verbose=>2}) if $options{man};
110
111 $DEBUG = $options{debug};
112
113 my @USAGE_ERRORS;
114 if (not exists $options{bibtex} and
115     not exists $options{bibtex_cache}) {
116     push @USAGE_ERRORS,
117         "You must give at least one of --bibtex".
118         "or --bibtex-cache";
119 }
120
121 pod2usage(join("\n",@USAGE_ERRORS)) if @USAGE_ERRORS;
122
123 main();
124
125 sub main{
126
127     my $dbh;
128     my $sth;
129     if (exists $options{bibtex_cache}) {
130         my $initialize = 0;
131         if (-e $options{bibtex_cache}) {
132             ($dbh,$sth) = open_cache($options{bibtex_cache});
133         } else {
134             ($dbh,$sth) = initialize_database($options{bibtex_cache});
135         }
136     }
137
138     if (exists $options{clear_cache}) {
139         clear_cache($dbh,$sth);
140     }
141     my %entries;
142     if (exists $options{build_cache}) {
143         $options{bibtex} //= [];
144         $options{bibtex} =
145             [@ARGV,
146              @{ref $options{bibtex}?$options{bibtex}:[$options{bibtex}]},
147             ];
148         @ARGV = ();
149     }
150     if (exists $options{bibtex}) {
151         for my $bibtex_file (@{ref $options{bibtex}?$options{bibtex}:[$options{bibtex}]}) {
152             parse_bibtex_file($bibtex_file,\%entries);
153         }
154     }
155
156     if (exists $options{papers_directory} and
157         defined $dbh
158        ) {
159         $dbh->begin_work;
160         load_papers_into_database($dbh,$sth,$options{papers_directory});
161         $dbh->commit;
162     }
163
164     p %entries if $DEBUG;
165     if (keys %entries and
166         defined $dbh) {
167         $dbh->begin_work;
168         load_bibtex_entries_into_database($dbh,$sth,\%entries);
169         $dbh->commit;
170     }
171
172     p @ARGV if $DEBUG;
173     for my $bibtex_key (@ARGV) {
174         open_bibtex_key(\%options,$dbh,$sth,\%entries,$bibtex_key);
175     }
176
177 }
178
179 sub clear_cache {
180     my ($dbh,$sth) = @_;
181     $sth->{clear_papers_cache}->execute();
182     $sth->{clear_bibtex_cache}->execute();
183 }
184
185 sub load_papers_into_database {
186     my ($dbh,$sth,$dir) = @_;
187
188     my @dirs = ref($dir)?@{$dir}:$dir;
189
190     my $actually_load_it = sub {
191         if (/\.git/) {
192             $File::Find::prune = 1;
193             return;
194         }
195         return unless /\.pdf$/;
196         my $xoj = 0;
197         if (-e "${_}.xoj") {
198             $xoj = 1;
199         }
200         insert_or_replace_papers($dbh,$sth,basename($File::Find::name),File::Spec->rel2abs($_),$xoj);
201     };
202
203     my @pdfs;
204     find($actually_load_it,@dirs);
205 }
206
207 sub insert_or_replace_papers {
208     my ($dbh,$sth,$file_name,$file_loc,$has_xoj) = @_;
209     $sth->{insert_papers}->execute($file_name,$file_loc,$has_xoj);
210     $sth->{insert_papers}->finish();
211 }
212
213 sub load_bibtex_entries_into_database {
214     my ($dbh,$sth,$entries) = @_;
215     for my $entry (keys %{$entries}) {
216         next unless defined $entries->{$entry};
217         $sth->{insert_bibtex}->execute($entry,@{$entries->{$entry}}{qw(file_name pmid doi html)});
218         $sth->{insert_bibtex}->finish();
219         print STDERR "inserted $entry {".join(',',map {defined $_?"'$_'":"'undef'"} %{$entries->{$entry}})."}\n" if $DEBUG;
220     }
221 }
222
223 sub open_bibtex_key {
224     my ($options,$dbh,$sth,$entries,$bibtex_key) = @_;
225     if (not defined $dbh) {
226         open_entry($dbh,$sth,$entries->{$bibtex_key},$options);
227     } else {
228         my $entry;
229         if ($options->{search_by_pmid}) {
230             $entry = select_entry_from_pmid($dbh,$sth,$bibtex_key);
231         } else {
232             $entry = select_entry_from_bibtex_key($dbh,$sth,$bibtex_key);
233         }
234         p $entry if $DEBUG;
235         open_entry($dbh,$sth,$entry,$options);
236     }
237 }
238
239 sub fork_exec {
240     my (@cmd) = @_;
241     my $child = fork();
242     if (not defined $child) {
243         die "Unable to fork for some reason: $!";
244     }
245     if ($child == 0) {
246         foreach (0 .. (POSIX::sysconf (&POSIX::_SC_OPEN_MAX) || 1024))
247            { POSIX::close $_ }
248         open (STDIN, "</dev/null");
249         open (STDOUT, ">/dev/null");
250         open (STDERR, ">&STDOUT");
251         exec(@cmd);
252     } else {
253         return $child;
254     }
255
256 }
257
258 sub open_pdf {
259     my ($file_name,$options,$has_xoj) = @_;
260     print STDERR "opening $file_name\n" if $DEBUG;
261     if ($has_xoj) {
262         fork_exec('xournal',$file_name);
263     } else {
264         fork_exec('xournal',$file_name)
265     }
266 }
267
268 sub open_browser{
269     my ($file) = @_;
270     fork_exec('sensible-browser',$file);
271 }
272
273 sub open_entry{
274     my ($dbh,$sth,$entry,$options) = @_;
275
276     return unless defined $entry and ref $entry and keys %{$entry};
277     if ($DEBUG) {
278         print STDERR "Entry: \n";
279         p $entry;
280     }
281     if (defined $entry->{file_name} and length $entry->{file_name}) {
282         my $paper = select_one($dbh,$sth->{select_papers_by_name},$entry->{file_name});
283         if (not defined $paper) {
284             my ($pmid) = $entry->{file_name} =~ /pmid_(\d+)/;
285             if (defined $pmid and length $pmid) {
286                 $paper = select_one($dbh,$sth->{select_papers_by_pmid},'%pmid_'.$pmid.'.%');
287             }
288         }
289         p $paper if $DEBUG;
290         print STDERR $entry->{file_name} if $DEBUG;
291         if (defined $paper) {
292             if ($options->{only_print}) {
293                 print $paper->{path};
294                 return;
295             }
296             open_pdf($paper->{path},$options,$paper->{has_xoj});
297             return;
298         } else {
299             print STDERR "Unable to find paper\n" if $DEBUG;
300         }
301     }
302     if (defined $entry->{doi}) {
303         if ($options->{only_print}) {
304             print $entry->{doi};
305             return;
306         }
307         my $url = $entry->{doi};
308         $url =~ s{^doi://}{http://dx.doi.org/};
309         open_browser($url,$options);
310         return;
311     }
312     if (defined $entry->{html}) {
313         if ($options->{only_print}) {
314             print $entry->{html};
315             return;
316         }
317         open_browser($entry->{html},$options);
318         return;
319     }
320 }
321
322 sub select_entry_from_pmid{
323     my ($dbh,$sth,$pmid) = @_;
324
325     return select_one($dbh,$sth->{select_bibtex_by_pmid},$pmid);
326 }
327
328
329 sub select_entry_from_bibtex_key{
330     my ($dbh,$sth,$bibtex_key) = @_;
331
332     my $entry = select_one($dbh,$sth->{select_bibtex_by_key},$bibtex_key);
333     if (not defined $entry) {
334         print STDERR "Unable to find entry by exact search\n" if $DEBUG;
335         $bibtex_key =~ s/:.*$//;
336         $entry = select_one($dbh,$sth->{select_bibtex_by_approximate_key},$bibtex_key.'%');
337     }
338     print STDERR "Found entry\n" if $DEBUG and defined $entry;
339     return $entry;
340 }
341
342 sub select_one{
343     my ($dbh,$sth,@bind_vals) = @_;
344     $sth->execute(@bind_vals) or
345         die "Unable to select one: ".$dbh->errstr();
346     my $results = $sth->fetchall_arrayref({});
347     $sth->finish();
348     return ref($results)?$results->[0]:undef;
349 }
350
351 sub parse_bibtex_file {
352     my ($file,$entries) = @_;
353
354     my $bibfile = Text::BibTeX::File->new($file)
355         or die "Unable to open $file for reading: $!";
356     my @entry_comments;
357     my $entry;
358     while ($entry = Text::BibTeX::Entry->new($bibfile)) {
359         print STDERR "In Entry ".$entry->metatype() if $DEBUG;
360         if ($entry->metatype() == BTE_COMMENT) {
361             push @entry_comments,$entry->value();
362         } elsif ($entry->metatype() == BTE_REGULAR) {
363             my $entry_key = $entry->key();
364             if (not defined $entry_key) {
365                 @entry_comments = ();
366                 next;
367             }
368             my %entry_data;
369             # if there is a file comment, use it as the file name
370             for my $comment (@entry_comments) {
371                 next unless $comment =~ /^\s*file(?:name)?:?\s*(.+?)\s*$/i;
372                 next unless length $1;
373                 $entry_data{file_name} = $1.'.pdf';
374                 last;
375             }
376             my %field_prefix = (doi => 'doi://',
377                                 html => 'http://',
378                                 file => '',
379                                 pmid => '',
380                                );
381             my %field_name = (doi => 'doi',
382                               html => 'html',
383                               pmid => 'pmid',
384                               file => 'file_name',);
385             for my $field (qw(file doi html pmid)) {
386                 my $field_value = $entry->get($field);
387                 if (defined $field_value and $field_value =~ /\S+/) {
388                     $entry_data{$field_name{$field}} =
389                         $field_prefix{$field}.$field_value if
390                         not defined $entry_data{$field_name{$field}};
391                 }
392             }
393             $entries->{$entry_key} = {} if not defined $entries->{$entry_key};
394             for my $field (keys %entry_data) {
395                 $entries->{$entry_key}{$field} = $entry_data{$field} if
396                     defined $entry_data{$field};
397             }
398             # reset the entry comments
399             @entry_comments = ();
400         } else {
401             # do nothing
402         }
403         print STDERR "\n" if $DEBUG;
404     }
405     return $entries;
406 }
407
408
409 sub initialize_database {
410     my ($cache) = @_;
411     return open_cache($cache,1);
412 }
413
414 sub open_cache {
415     my ($cache,$initialize) = @_;
416     my $dbh = DBI->connect("dbi:SQLite:dbname=$cache","","") or
417         die "Unable to open/create database $cache";
418     if ($initialize) {
419         $dbh->do("DROP TABLE IF EXISTS bibtex;");
420         $dbh->do("DROP TABLE IF EXISTS papers;");
421         $dbh->do(<<EOF);
422 CREATE TABLE bibtex (
423 bibtex_key TEXT PRIMARY KEY,
424 file_name TEXT,
425 pmid TEXT,
426 doi TEXT,
427 html TEXT
428 );
429 EOF
430         $dbh->do(<<EOF);
431 CREATE UNIQUE INDEX bibtex_file_name ON bibtex(file_name);
432 EOF
433         $dbh->do(<<EOF);
434 CREATE UNIQUE INDEX bibtex_bibtex_key ON bibtex(bibtex_key);
435 EOF
436         $dbh->do(<<EOF);
437 CREATE INDEX bibtex_pmid ON bibtex(pmid);
438 EOF
439         $dbh->do(<<EOF);
440 CREATE TABLE papers (
441 file_name TEXT PRIMARY KEY,
442 path TEXT,
443 has_xoj BOOLEAN
444 );
445 EOF
446         $dbh->do(<<EOF);
447 CREATE UNIQUE INDEX papers_path ON papers(path);
448 EOF
449         $dbh->do(<<EOF);
450 CREATE UNIQUE INDEX papers_file_name ON papers(file_name);
451 EOF
452     }
453     my %s =
454         (insert_papers => <<'EOF',
455 INSERT OR REPLACE INTO papers(file_name,path,has_xoj) VALUES (?,?,?);
456 EOF
457          insert_bibtex => <<'EOF',
458 INSERT OR REPLACE INTO bibtex (bibtex_key,file_name,pmid,doi,html) VALUES (?,?,?,?,?);
459 EOF
460          select_papers_by_name => <<'EOF',
461 SELECT * FROM papers WHERE file_name = ?;
462 EOF
463          select_papers_by_pmid => <<'EOF',
464 SELECT * FROM papers WHERE file_name LIKE ?;
465 EOF
466          select_papers_by_path => <<'EOF',
467 SELECT * FROM papers WHERE path = ?;
468 EOF
469          select_bibtex_by_key => <<'EOF',
470 SELECT * FROM bibtex WHERE bibtex_key = ?;
471 EOF
472          select_bibtex_by_approximate_key => <<'EOF',
473 SELECT * FROM bibtex WHERE bibtex_key LIKE ?;
474 EOF
475          select_bibtex_by_file_name => <<'EOF',
476 SELECT * FROM bibtex WHERE file_name = ?;
477 EOF
478          select_bibtex_by_pmid => <<'EOF',
479 SELECT * FROM bibtex WHERE pmid = ?;
480 EOF
481          clear_papers_cache => <<'EOF',
482 DELETE FROM papers;
483 EOF
484          clear_bibtex_cache => <<'EOF',
485 DELETE FROM bibtex;
486 EOF
487         );
488     my $st;
489     for my $key (keys %s) {
490         $st->{$key}=$dbh->prepare($s{$key}) //
491             die "Unable to prepare sql statement: ".$dbh->errstr;
492     }
493     return ($dbh,$st);
494 }
495
496
497 __END__