]> git.donarmstrong.com Git - bin.git/blob - bibtex_to_paper
use git to locate 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                use_git         => 1,
95                'bibtex_cache'  => File::Spec->catfile(User->Home,'.bibtex_to_paper_cache'),
96               );
97
98 GetOptions(\%options,
99            'build_cache|build-cache!',
100            'bibtex|b=s@',
101            'bibtex_cache|bibtex-cache|c=s',
102            'pdfviewer|p=s',
103            'use_git|use-git!',
104            'only_print|only-print!',
105            'search_by_pmid|search-by-pmid!',
106            'clear_cache|clear-cache!',
107            'papers_directory|papers-directory=s@',
108            'debug|d+','help|h|?','man|m');
109
110 pod2usage() if $options{help};
111 pod2usage({verbose=>2}) if $options{man};
112
113 $DEBUG = $options{debug};
114
115 my @USAGE_ERRORS;
116 if (not exists $options{bibtex} and
117     not exists $options{bibtex_cache}) {
118     push @USAGE_ERRORS,
119         "You must give at least one of --bibtex".
120         "or --bibtex-cache";
121 }
122
123 pod2usage(join("\n",@USAGE_ERRORS)) if @USAGE_ERRORS;
124
125 main();
126
127 sub main{
128
129     my $dbh;
130     my $sth;
131     if (exists $options{bibtex_cache}) {
132         my $initialize = 0;
133         if (-e $options{bibtex_cache}) {
134             ($dbh,$sth) = open_cache($options{bibtex_cache});
135         } else {
136             ($dbh,$sth) = initialize_database($options{bibtex_cache});
137         }
138     }
139
140     if (exists $options{clear_cache}) {
141         clear_cache($dbh,$sth);
142     }
143     my %entries;
144     if (exists $options{build_cache}) {
145         $options{bibtex} //= [];
146         $options{bibtex} =
147             [@ARGV,
148              @{ref $options{bibtex}?$options{bibtex}:[$options{bibtex}]},
149             ];
150         @ARGV = ();
151     }
152     if (exists $options{bibtex}) {
153         for my $bibtex_file (@{ref $options{bibtex}?$options{bibtex}:[$options{bibtex}]}) {
154             parse_bibtex_file($bibtex_file,\%entries);
155         }
156     }
157
158     if (exists $options{papers_directory} and
159         defined $dbh
160        ) {
161         $dbh->begin_work;
162         load_papers_into_database($dbh,$sth,$options{papers_directory});
163         $dbh->commit;
164     }
165
166     p %entries if $DEBUG;
167     if (keys %entries and
168         defined $dbh) {
169         $dbh->begin_work;
170         load_bibtex_entries_into_database($dbh,$sth,\%entries);
171         $dbh->commit;
172     }
173
174     p @ARGV if $DEBUG;
175     for my $bibtex_key (@ARGV) {
176         open_bibtex_key(\%options,$dbh,$sth,\%entries,$bibtex_key);
177     }
178
179 }
180
181 sub clear_cache {
182     my ($dbh,$sth) = @_;
183     $sth->{clear_papers_cache}->execute();
184     $sth->{clear_bibtex_cache}->execute();
185 }
186
187 sub load_papers_into_database {
188     my ($dbh,$sth,$dir) = @_;
189
190     my @dirs = ref($dir)?@{$dir}:$dir;
191
192     if ($options{use_git}) {
193         my @files = grep /\.pdf$/, split /\n/, qx(git ls-tree HEAD -r --full-name --name-only);
194         for my $file  (@files) {
195             insert_or_replace_papers($dbh,$sth,basename($file),File::Spec->rel2abs($file), -e "${file}.xoj");
196         }
197     } else {
198         my $actually_load_it = sub {
199             if (/\.git/) {
200                 $File::Find::prune = 1;
201                 return;
202             }
203             return unless /\.pdf$/;
204             my $xoj = 0;
205             if (-e "${_}.xoj") {
206                 $xoj = 1;
207             }
208             insert_or_replace_papers($dbh,$sth,basename($File::Find::name),File::Spec->rel2abs($_),$xoj);
209         };
210         find($actually_load_it,@dirs);
211     }
212 }
213
214 sub insert_or_replace_papers {
215     my ($dbh,$sth,$file_name,$file_loc,$has_xoj) = @_;
216     $sth->{insert_papers}->execute($file_name,$file_loc,$has_xoj);
217     $sth->{insert_papers}->finish();
218 }
219
220 sub load_bibtex_entries_into_database {
221     my ($dbh,$sth,$entries) = @_;
222     for my $entry (keys %{$entries}) {
223         next unless defined $entries->{$entry};
224         $sth->{insert_bibtex}->execute($entry,@{$entries->{$entry}}{qw(file_name pmid doi html)});
225         $sth->{insert_bibtex}->finish();
226         print STDERR "inserted $entry {".join(',',map {defined $_?"'$_'":"'undef'"} %{$entries->{$entry}})."}\n" if $DEBUG;
227     }
228 }
229
230 sub open_bibtex_key {
231     my ($options,$dbh,$sth,$entries,$bibtex_key) = @_;
232     if (not defined $dbh) {
233         open_entry($dbh,$sth,$entries->{$bibtex_key},$options);
234     } else {
235         my $entry;
236         if ($options->{search_by_pmid}) {
237             $entry = select_entry_from_pmid($dbh,$sth,$bibtex_key);
238         } else {
239             $entry = select_entry_from_bibtex_key($dbh,$sth,$bibtex_key);
240         }
241         p $entry if $DEBUG;
242         open_entry($dbh,$sth,$entry,$options);
243     }
244 }
245
246 sub fork_exec {
247     my (@cmd) = @_;
248     my $child = fork();
249     if (not defined $child) {
250         die "Unable to fork for some reason: $!";
251     }
252     if ($child == 0) {
253         foreach (0 .. (POSIX::sysconf (&POSIX::_SC_OPEN_MAX) || 1024))
254            { POSIX::close $_ }
255         open (STDIN, "</dev/null");
256         open (STDOUT, ">/dev/null");
257         open (STDERR, ">&STDOUT");
258         exec(@cmd);
259     } else {
260         return $child;
261     }
262
263 }
264
265 sub open_pdf {
266     my ($file_name,$options,$has_xoj) = @_;
267     print STDERR "opening $file_name\n" if $DEBUG;
268     if ($has_xoj) {
269         fork_exec('xournal',$file_name);
270     } else {
271         fork_exec('xournal',$file_name)
272     }
273 }
274
275 sub open_browser{
276     my ($file) = @_;
277     fork_exec('sensible-browser',$file);
278 }
279
280 sub open_entry{
281     my ($dbh,$sth,$entry,$options) = @_;
282
283     return unless defined $entry and ref $entry and keys %{$entry};
284     if ($DEBUG) {
285         print STDERR "Entry: \n";
286         p $entry;
287     }
288     if (defined $entry->{file_name} and length $entry->{file_name}) {
289         my $paper = select_one($dbh,$sth->{select_papers_by_name},$entry->{file_name});
290         if (not defined $paper) {
291             my ($pmid) = $entry->{file_name} =~ /pmid_(\d+)/;
292             if (defined $pmid and length $pmid) {
293                 $paper = select_one($dbh,$sth->{select_papers_by_pmid},'%pmid_'.$pmid.'.%');
294             }
295         }
296         p $paper if $DEBUG;
297         print STDERR $entry->{file_name} if $DEBUG;
298         if (defined $paper) {
299             if ($options->{only_print}) {
300                 print $paper->{path};
301                 return;
302             }
303             open_pdf($paper->{path},$options,$paper->{has_xoj});
304             return;
305         } else {
306             print STDERR "Unable to find paper\n" if $DEBUG;
307         }
308     }
309     if (defined $entry->{doi}) {
310         if ($options->{only_print}) {
311             print $entry->{doi};
312             return;
313         }
314         my $url = $entry->{doi};
315         $url =~ s{^doi://}{http://dx.doi.org/};
316         open_browser($url,$options);
317         return;
318     }
319     if (defined $entry->{html}) {
320         if ($options->{only_print}) {
321             print $entry->{html};
322             return;
323         }
324         open_browser($entry->{html},$options);
325         return;
326     }
327 }
328
329 sub select_entry_from_pmid{
330     my ($dbh,$sth,$pmid) = @_;
331
332     return select_one($dbh,$sth->{select_bibtex_by_pmid},$pmid);
333 }
334
335
336 sub select_entry_from_bibtex_key{
337     my ($dbh,$sth,$bibtex_key) = @_;
338
339     my $entry = select_one($dbh,$sth->{select_bibtex_by_key},$bibtex_key);
340     if (not defined $entry) {
341         print STDERR "Unable to find entry by exact search\n" if $DEBUG;
342         $bibtex_key =~ s/:.*$//;
343         $entry = select_one($dbh,$sth->{select_bibtex_by_approximate_key},$bibtex_key.'%');
344     }
345     print STDERR "Found entry\n" if $DEBUG and defined $entry;
346     return $entry;
347 }
348
349 sub select_one{
350     my ($dbh,$sth,@bind_vals) = @_;
351     $sth->execute(@bind_vals) or
352         die "Unable to select one: ".$dbh->errstr();
353     my $results = $sth->fetchall_arrayref({});
354     $sth->finish();
355     return ref($results)?$results->[0]:undef;
356 }
357
358 sub parse_bibtex_file {
359     my ($file,$entries) = @_;
360
361     my $bibfile = Text::BibTeX::File->new($file)
362         or die "Unable to open $file for reading: $!";
363     my @entry_comments;
364     my $entry;
365     while ($entry = Text::BibTeX::Entry->new($bibfile)) {
366         print STDERR "In Entry ".$entry->metatype() if $DEBUG;
367         if ($entry->metatype() == BTE_COMMENT) {
368             push @entry_comments,$entry->value();
369         } elsif ($entry->metatype() == BTE_REGULAR) {
370             my $entry_key = $entry->key();
371             if (not defined $entry_key) {
372                 @entry_comments = ();
373                 next;
374             }
375             my %entry_data;
376             # if there is a file comment, use it as the file name
377             for my $comment (@entry_comments) {
378                 next unless $comment =~ /^\s*file(?:name)?:?\s*(.+?)\s*$/i;
379                 next unless length $1;
380                 $entry_data{file_name} = $1.'.pdf';
381                 last;
382             }
383             my %field_prefix = (doi => 'doi://',
384                                 html => 'http://',
385                                 file => '',
386                                 pmid => '',
387                                );
388             my %field_name = (doi => 'doi',
389                               html => 'html',
390                               pmid => 'pmid',
391                               file => 'file_name',);
392             for my $field (qw(file doi html pmid)) {
393                 my $field_value = $entry->get($field);
394                 if (defined $field_value and $field_value =~ /\S+/) {
395                     $entry_data{$field_name{$field}} =
396                         $field_prefix{$field}.$field_value if
397                         not defined $entry_data{$field_name{$field}};
398                 }
399             }
400             $entries->{$entry_key} = {} if not defined $entries->{$entry_key};
401             for my $field (keys %entry_data) {
402                 $entries->{$entry_key}{$field} = $entry_data{$field} if
403                     defined $entry_data{$field};
404             }
405             # reset the entry comments
406             @entry_comments = ();
407         } else {
408             # do nothing
409         }
410         print STDERR "\n" if $DEBUG;
411     }
412     return $entries;
413 }
414
415
416 sub initialize_database {
417     my ($cache) = @_;
418     return open_cache($cache,1);
419 }
420
421 sub open_cache {
422     my ($cache,$initialize) = @_;
423     my $dbh = DBI->connect("dbi:SQLite:dbname=$cache","","") or
424         die "Unable to open/create database $cache";
425     if ($initialize) {
426         $dbh->do("DROP TABLE IF EXISTS bibtex;");
427         $dbh->do("DROP TABLE IF EXISTS papers;");
428         $dbh->do(<<EOF);
429 CREATE TABLE bibtex (
430 bibtex_key TEXT PRIMARY KEY,
431 file_name TEXT,
432 pmid TEXT,
433 doi TEXT,
434 html TEXT
435 );
436 EOF
437         $dbh->do(<<EOF);
438 CREATE UNIQUE INDEX bibtex_file_name ON bibtex(file_name);
439 EOF
440         $dbh->do(<<EOF);
441 CREATE UNIQUE INDEX bibtex_bibtex_key ON bibtex(bibtex_key);
442 EOF
443         $dbh->do(<<EOF);
444 CREATE INDEX bibtex_pmid ON bibtex(pmid);
445 EOF
446         $dbh->do(<<EOF);
447 CREATE TABLE papers (
448 file_name TEXT PRIMARY KEY,
449 path TEXT,
450 has_xoj BOOLEAN
451 );
452 EOF
453         $dbh->do(<<EOF);
454 CREATE UNIQUE INDEX papers_path ON papers(path);
455 EOF
456         $dbh->do(<<EOF);
457 CREATE UNIQUE INDEX papers_file_name ON papers(file_name);
458 EOF
459     }
460     my %s =
461         (insert_papers => <<'EOF',
462 INSERT OR REPLACE INTO papers(file_name,path,has_xoj) VALUES (?,?,?);
463 EOF
464          insert_bibtex => <<'EOF',
465 INSERT OR REPLACE INTO bibtex (bibtex_key,file_name,pmid,doi,html) VALUES (?,?,?,?,?);
466 EOF
467          select_papers_by_name => <<'EOF',
468 SELECT * FROM papers WHERE file_name = ?;
469 EOF
470          select_papers_by_pmid => <<'EOF',
471 SELECT * FROM papers WHERE file_name LIKE ?;
472 EOF
473          select_papers_by_path => <<'EOF',
474 SELECT * FROM papers WHERE path = ?;
475 EOF
476          select_bibtex_by_key => <<'EOF',
477 SELECT * FROM bibtex WHERE bibtex_key = ?;
478 EOF
479          select_bibtex_by_approximate_key => <<'EOF',
480 SELECT * FROM bibtex WHERE bibtex_key LIKE ?;
481 EOF
482          select_bibtex_by_file_name => <<'EOF',
483 SELECT * FROM bibtex WHERE file_name = ?;
484 EOF
485          select_bibtex_by_pmid => <<'EOF',
486 SELECT * FROM bibtex WHERE pmid = ?;
487 EOF
488          clear_papers_cache => <<'EOF',
489 DELETE FROM papers;
490 EOF
491          clear_bibtex_cache => <<'EOF',
492 DELETE FROM bibtex;
493 EOF
494         );
495     my $st;
496     for my $key (keys %s) {
497         $st->{$key}=$dbh->prepare($s{$key}) //
498             die "Unable to prepare sql statement: ".$dbh->errstr;
499     }
500     return ($dbh,$st);
501 }
502
503
504 __END__