File Coverage

blib/lib/EBook/Gutenberg/Catalog.pm
Criterion Covered Total %
statement 75 90 83.3
branch 13 28 46.4
condition 2 6 33.3
subroutine 17 18 94.4
pod 6 6 100.0
total 113 148 76.3


line stmt bran cond sub pod time code
1             package EBook::Gutenberg::Catalog;
2 3     3   120082 use 5.016;
  3         14  
3             our $VERSION = '1.00';
4 3     3   21 use strict;
  3         6  
  3         108  
5 3     3   17 use warnings;
  3         8  
  3         198  
6              
7 3     3   1189 use File::Copy;
  3         12285  
  3         263  
8 3     3   2346 use File::Fetch;
  3         301088  
  3         170  
9 3     3   40 use File::Spec;
  3         8  
  3         84  
10 3     3   14 use File::Temp qw(tempdir);
  3         5  
  3         398  
11 3     3   26 use List::Util qw(all);
  3         7  
  3         212  
12              
13 3     3   3461 use Text::CSV_XS qw(csv);
  3         52799  
  3         3378  
14              
15             my $CATALOG_URI = "https://www.gutenberg.org/cache/epub/feeds/pg_catalog.csv";
16             my $CATALOG_HEAD = 'Text#,Type,Issued,Title,Language,Authors,Subjects,LoCC,Bookshelves';
17              
18             sub _catalog_ok {
19              
20 49     49   90 my $file = shift;
21              
22 49 50       1971 open my $fh, '<', $file
23             or die "Failed to open $file for reading: $!\n";
24 49         1053 my $head = readline $fh;
25 49         641 close $fh;
26              
27 49         786 return $head =~ /^\Q$CATALOG_HEAD\E/;
28              
29             }
30              
31             sub _bookify {
32              
33 65     65   136 my $book = shift;
34              
35 65         184 my @b = map { s/\s+/ /gr } @$book;
  585         3506  
36              
37             return {
38 65         1590 'Text#' => $b[0],
39             'Type' => $b[1],
40             'Issued' => $b[2],
41             'Title' => $b[3],
42             'Language' => $b[4],
43             'Authors' => $b[5],
44             'Subjects' => $b[6],
45             'LoCC' => $b[7],
46             'Bookshelves' => $b[8],
47             };
48              
49             }
50              
51             sub new {
52              
53 48     48 1 260452 my $class = shift;
54 48         79 my $file = shift;
55              
56 48         76 my $self;
57              
58 48         601 $$self = File::Spec->rel2abs($file);
59              
60 48         113 bless $self, $class;
61              
62 48 50       716 if (-d $$self) {
63 0         0 die "$$self is a directory\n";
64             }
65              
66 48 50 33     468 if (-f $$self and !_catalog_ok($$self)) {
67 0         0 die "$$self is not a valid Project Gutenberg catalog file\n";
68             }
69              
70 48         172 return $self;
71              
72             }
73              
74             sub path {
75              
76 2     2 1 805 my $self = shift;
77              
78 2         58 return $$self;
79              
80             }
81              
82             sub set_path {
83              
84 1     1 1 3 my $self = shift;
85 1         24 my $file = shift;
86              
87 1         34 $$self = File::Spec->rel2abs($file);
88              
89 1 50       28 if (-d $$self) {
90 0         0 die "$$self is a directory\n";
91             }
92              
93 1 50 33     17 if (-f $$self and !_catalog_ok($$self)) {
94 0         0 die "$$self is not a valid Project Gutenberg catalog file\n";
95             }
96              
97 1         5 return $$self;
98              
99             }
100              
101             sub fetch {
102              
103 0     0 1 0 my $self = shift;
104              
105 0         0 my $tmp = tempdir(CLEANUP => 1);
106              
107 0         0 my $ff = File::Fetch->new(uri => $CATALOG_URI);
108              
109 0 0       0 my $fetch = $ff->fetch(to => $tmp)
110             or die $ff->error;
111              
112 0 0       0 unless (_catalog_ok($fetch)) {
113 0         0 die "Downloaded catalog was not a valid Project Gutenberg catalog file\n";
114             }
115              
116 0 0       0 move($fetch, $$self)
117             or die "Failed to move $fetch to $$self: $!\n";
118              
119 0         0 rmdir $tmp;
120              
121 0         0 return $$self;
122              
123             }
124              
125             sub book {
126              
127 65     65 1 100673 my $self = shift;
128 65         125 my $id = shift;
129              
130 65 50       1401 unless (-f $$self) {
131 0         0 die "$$self is not a regular file\n";
132             }
133              
134 65         698 my $csv = Text::CSV_XS->new({ binary => 1, auto_diag => 1 });
135              
136 65 50       13563 open my $fh, '<', $$self
137             or die "Failed to open $$self for reading: $!\n";
138              
139 65         175 my $book;
140              
141 65         3313 while (my $row = $csv->getline($fh)) {
142 710 100       12635 if ($row->[0] eq $id) {
143 65         126 $book = $row;
144 65         161 last;
145             }
146             }
147              
148 65         1278 close $fh;
149              
150 65 50       290 return defined $book ? _bookify($book) : undef;
151              
152             }
153              
154             sub books {
155              
156 16     16 1 9082 my $self = shift;
157 16         30 my $param = shift;
158              
159 16 50       396 unless (-f $$self) {
160 0         0 die "$$self is not a regular file\n";
161             }
162              
163 16         39 my $filter = {};
164              
165 16         90 for my $p (split /,/, $CATALOG_HEAD) {
166 144 100       333 next unless ref $param->{ $p } eq 'CODE';
167 23         57 $filter->{ $p } = $param->{ $p };
168             }
169              
170             my $books = csv(
171             in => $$self,
172             filter => $filter,
173             # Convert all whitespace to single space
174             after_parse => sub {
175 136     136   18271 for my $k (keys %{ $_[1] }) {
  136         466  
176 1224         5841 $_[1]->{ $k } =~ s/\s+/ /g;
177             }
178             }
179 16         147 );
180              
181 16         789 return $books;
182              
183             }
184              
185             1;
186              
187             =head1 NAME
188              
189             EBook::Gutenberg::Catalog - Project Gutenberg catalog interface
190              
191             =head1 SYNOPSIS
192              
193             use EBook::Gutenberg::Catalog;
194              
195             my $catalog = EBook::Gutenberg::Catalog->new($path);
196              
197             =head1 DESCRIPTION
198              
199             B is a module that provides an interface for reading
200             Project Gutenberg CSV catalog files. This is developer documentation, for
201             L user documentation you should consult its manual.
202              
203             =head1 METHODS
204              
205             =over 4
206              
207             =item $cat = EBook::Gutenberg::Catalog->new($path)
208              
209             Returns a blessed B object representing a Project
210             Gutenberg catalog file stored in C<$path>. C<$path> doesn't actually have to
211             exist, it can be fetched later via the C method.
212              
213             =item $path = $cat->path()
214              
215             Returns path to C<$cat>'s catalog file.
216              
217             =item $path = $cat->set_path($new)
218              
219             Set C<$cat>'s catalog file to C<$new>. Returns newly set path.
220              
221             =item $fetch = $cat->fetch()
222              
223             Fetches Project Gutenberg catalog file and writes it to the path specified in
224             C. Returns the path to the fetched file.
225              
226             =item $book = $cat->book($id)
227              
228             Get hash ref representing the book with an ID C<$id> from the catalog file. The
229             hash ref has the following format:
230              
231             {
232             'Text#' => '...',
233             'Type' => '...',
234             'Issued' => '...',
235             'Title' => '...',
236             'Language' => '...',
237             'Authors' => '...',
238             'Subjects' => '...',
239             'LoCC' => '...',
240             'Bookshelves' => '...',
241             }
242              
243             =item $books = $cat->books([\%params])
244              
245             Returns array ref of hash refs representing books from catalog that conform to
246             the parameters supplied by C<\%params>. The hash refs follow the same format
247             used by the ones returned by C.
248              
249             C<\%params> is a hash ref of ebook fields and subroutine references that are
250             used to C for specific ebooks. The subroutine will have C<$_> set to the
251             value of the field. If the subroutine does not return true when given a value,
252             the ebook will be filtered out. The following are valid fields to use for
253             C<\%params>.
254              
255             =over 4
256              
257             =item Text#
258              
259             =item Type
260              
261             =item Issued
262              
263             =item Title
264              
265             =item Language
266              
267             =item Authors
268              
269             =item Subjects
270              
271             =item LoCC
272              
273             =item Bookshelves
274              
275             =back
276              
277             =back
278              
279             =head1 AUTHOR
280              
281             Written by Samuel Young, Esamyoung12788@gmail.comE.
282              
283             This project's source can be found on its
284             L. Comments and pull
285             requests are welcome!
286              
287             =head1 COPYRIGHT
288              
289             Copyright (C) 2025 Samuel Young
290              
291             This program is free software: you can redistribute it and/or modify
292             it under the terms of the GNU General Public License as published by
293             the Free Software Foundation, either version 3 of the License, or
294             (at your option) any later version.
295              
296             =head1 SEE ALSO
297              
298             L
299              
300             =cut
301              
302             # vim: expandtab shiftwidth=4