File Coverage

blib/lib/App/BarnesNoble/WishListMinder.pm
Criterion Covered Total %
statement 21 248 8.4
branch 0 78 0.0
condition 0 50 0.0
subroutine 7 39 17.9
pod 0 18 0.0
total 28 433 6.4


line stmt bran cond sub pod time code
1             #---------------------------------------------------------------------
2             package App::BarnesNoble::WishListMinder;
3             #
4             # Copyright 2014 Christopher J. Madsen
5             #
6             # Author: Christopher J. Madsen
7             # Created: 15 Jun 2014
8             #
9             # This program is free software; you can redistribute it and/or modify
10             # it under the same terms as Perl itself.
11             #
12             # This program is distributed in the hope that it will be useful,
13             # but WITHOUT ANY WARRANTY; without even the implied warranty of
14             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either the
15             # GNU General Public License or the Artistic License for more details.
16             #
17             # ABSTRACT: Monitor a Barnes & Noble wishlist for price changes
18             #---------------------------------------------------------------------
19              
20 1     1   15793 use 5.010;
  1         3  
  1         31  
21 1     1   3 use strict;
  1         2  
  1         25  
22 1     1   3 use warnings;
  1         1  
  1         42  
23              
24             our $VERSION = '0.002';
25             # This file is part of App-BarnesNoble-WishListMinder 0.002 (October 18, 2014)
26              
27 1     1   700 use Path::Tiny;
  1         12338  
  1         52  
28             #use Smart::Comments;
29              
30 1     1   504 use Moo;
  1         11074  
  1         6  
31 1     1   1747 use namespace::clean;
  1         9121  
  1         5  
32              
33             # Like == but undef equals only itself
34             sub _numEq
35             {
36 0     0     my ($one, $two) = @_;
37              
38 0 0 0       return !1 if (defined($one) xor defined($two));
39 0 0         return 1 unless defined $one;
40 0           $one == $two;
41             } # end _numEq
42              
43             # Like eq but undef equals only itself
44             sub _eq
45             {
46 0     0     my ($one, $two) = @_;
47              
48 0 0 0       return !1 if (defined($one) xor defined($two));
49 0 0         return 1 unless defined $one;
50 0           $one eq $two;
51             } # end _numEq
52              
53             sub _format_price {
54 0     0     my $price = shift;
55 0 0         if (defined $price) {
56 0           $price = sprintf '$%03d', $price;
57 0           substr($price, -2, 0, '.');
58 0           $price;
59             } else {
60 0           'unavailable';
61             }
62             } # end _format_price
63              
64             sub _format_timestamp {
65 0     0     require Time::Piece;
66              
67 0           Time::Piece->gmtime(shift)->strftime("%Y-%m-%d %H:%M:%S");
68             }
69              
70             #=====================================================================
71              
72             has mech => qw(is lazy);
73             sub _build_mech {
74 0     0     require WWW::Mechanize;
75 0           WWW::Mechanize->new(
76             autocheck => 1,
77             cookie_jar => { file => shift->cookie_file, autosave => 1 },
78             );
79             } # end _build_mech
80              
81             has dir => qw(is lazy);
82             sub _build_dir {
83 0     0     require File::HomeDir;
84 0           File::HomeDir->VERSION(0.93); # my_dist_data
85              
86 0   0       path(File::HomeDir->my_dist_data('App-BarnesNoble-WishListMinder',
87             { create => 1 })
88             or die "Can't determine data directory");
89             } # end _build_dir
90              
91             has config_file => qw(is lazy);
92             sub _build_config_file {
93 0     0     shift->dir->child('config.ini');
94             } # end _build_config_file
95              
96             has config => qw(is lazy);
97             sub _build_config {
98 0     0     my $self = shift;
99 0           require Config::Tiny;
100 0           my $fn = $self->config_file;
101              
102 0 0         Config::Tiny->read("$fn", 'utf8')
103             or die "Unable to read $fn: " . Config::Tiny->errstr;
104             } # end _build_config
105              
106             has cookie_file => qw(is lazy);
107             sub _build_cookie_file {
108 0     0     shift->dir->child('cookies.txt');
109             } # end _build_cookie_file
110              
111             has db_file => qw(is lazy);
112             sub _build_db_file {
113 0     0     shift->dir->child('wishlist.sqlite');
114             } # end _build_db_file
115              
116             has dbh => qw(is lazy predicate 1 clearer _clear_dbh);
117             sub _build_dbh {
118 0     0     my $self = shift;
119              
120 0           require DBI;
121 0           DBI->VERSION(1.38); # last_insert_id
122              
123 0           my $fn = $self->db_file;
124 0           my $exists = $fn->exists;
125              
126 0           my $dbh = DBI->connect("dbi:SQLite:dbname=$fn","","",
127             { AutoCommit => 0, PrintError => 0, RaiseError => 1,
128             sqlite_unicode => 1 });
129              
130 0 0         $self->create_database_schema($dbh) unless $exists;
131              
132 0           $dbh;
133             } # end _build_dbh
134              
135             sub close_dbh
136             {
137 0     0 0   my $self = shift;
138              
139 0 0         if ($self->has_dbh) {
140 0           my $dbh = $self->dbh;
141 0           $dbh->rollback;
142 0           $dbh->disconnect;
143 0           $self->_clear_dbh;
144             }
145             } # end close_dbh
146              
147             has scraper => qw(is lazy);
148             sub _build_scraper {
149 0     0     require Web::Scraper::BarnesNoble::WishList;
150              
151 0           Web::Scraper::BarnesNoble::WishList::bn_scraper();
152             } # end _build_scraper
153              
154             has updates => qw(is ro default) => sub { {} };
155              
156             #---------------------------------------------------------------------
157             sub configure
158             {
159 0     0 0   my ($self) = @_;
160              
161 0           my $config_file = $self->config_file;
162              
163 0           say "Your config file is:\n $config_file";
164              
165 0 0         unless ($config_file->is_file) {
166 0 0         die "$config_file is a directory!\n" if $config_file->is_dir;
167 0           $config_file->spew_utf8(<<'END CONFIG');
168             ; -*-conf-windows-*-
169             ; Your credentials for logging in to the Barnes & Noble website go here:
170             email = YOUR EMAIL HERE
171             password = YOUR PASSWORD HERE
172              
173             ; If you want the Price Drop Alert emails to go to a different address,
174             ; uncomment the next line and set the email address.
175             ;report = EMAIL ADDRESS FOR ALERTS
176              
177             ; Next, you need one or more wishlists to monitor.
178             ; Each wishlist must have a unique name in [brackets].
179              
180             [My Wishlist]
181             wishlist = WISHLIST URL HERE
182             END CONFIG
183 0           say "\nYou need to replace the ALL CAPS placeholders with the correct values.";
184             }
185              
186 0 0 0       if (my $editor = $ENV{VISUAL} || $ENV{EDITOR}) {
187 0           require Text::ParseWords;
188 0           system(Text::ParseWords::shellwords($editor), "$config_file");
189             }
190             } # end configure
191              
192             #---------------------------------------------------------------------
193             sub create_database_schema
194             {
195 0     0 0   my ($self, $dbh) = @_;
196              
197 0           $dbh->do("PRAGMA foreign_keys = ON");
198              
199 0           $dbh->do(<<'');
200             CREATE TABLE books (
201             ean INTEGER PRIMARY KEY,
202             title TEXT NOT NULL,
203             author TEXT
204             )
205              
206 0           $dbh->do(<<'');
207             CREATE TABLE wishlists (
208             wishlist_id INTEGER PRIMARY KEY,
209             url TEXT NOT NULL UNIQUE,
210             last_fetched TIMESTAMP
211             )
212              
213 0           $dbh->do(<<'');
214             CREATE TABLE wishlist_books (
215             wishlist_id INTEGER NOT NULL REFERENCES wishlists,
216             ean INTEGER NOT NULL REFERENCES books,
217             priority INTEGER,
218             date_added DATE NOT NULL DEFAULT CURRENT_DATE,
219             date_removed DATE,
220             PRIMARY KEY (wishlist_id,ean)
221             )
222              
223 0           $dbh->do(<<'');
224             CREATE TABLE prices (
225             ean INTEGER NOT NULL REFERENCES books,
226             first_recorded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
227             last_checked TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
228             current TINYINT NOT NULL DEFAULT 1,
229             price INTEGER,
230             list_price INTEGER,
231             discount INTEGER,
232             PRIMARY KEY (ean,first_recorded)
233             )
234              
235 0           $dbh->commit;
236              
237             } # end create_database_schema
238              
239             #---------------------------------------------------------------------
240             sub login
241             {
242 0     0 0   my ($self) = shift;
243              
244 0           my ($config, $m) = ($self->config->{_}, $self->mech);
245              
246 0           $m->get('https://www.barnesandnoble.com/signin');
247              
248             #path("/tmp/login.html")->spew_utf8($m->content);
249              
250 0           $m->submit_form(
251             with_fields => {
252             'login.email' => $config->{email},
253             'login.password' => $config->{password},
254             },
255             );
256             } # end login
257             #---------------------------------------------------------------------
258              
259             sub scrape_response
260             {
261 0     0 0   my ($self, $response) = @_;
262              
263 0           my $books = $self->scraper->scrape($response);
264              
265 0           for my $book (@$books) {
266 0   0       $book->{priority} //= 3;
267 0           for ($book->{discount}) {
268 0 0         next unless defined $_;
269 0           s/^\s+//;
270 0           s/\s+\z//;
271 0           s/^\((.+)\)\z/$1/;
272 0           s/^You save\s*//i;
273             }
274 0           $book->{date_added} =~ s!^(\d\d)/(\d\d)/(\d\d)$!20$3-$1-$2!;
275             }
276              
277 0           $books;
278             } # end scrape_response
279             #---------------------------------------------------------------------
280              
281             sub write_db
282             {
283 0     0 0   my ($self, $wishlist_url, $time_fetched, $books) = @_;
284              
285 0           $time_fetched = _format_timestamp($time_fetched);
286              
287 0           my $dbh = $self->dbh;
288 0           my $updates = $self->updates;
289              
290 0           my $wishlist_id = $self->get_wishlist_id($wishlist_url);
291              
292 0           my $existing_priority = $self->get_existing_books($wishlist_id);
293              
294 0           my $get_book = $dbh->prepare(<<'');
295             SELECT title, author FROM books WHERE ean = ?
296              
297 0           my $get_price = $dbh->prepare(<<'');
298             SELECT price, list_price, discount, first_recorded FROM prices
299             WHERE ean = ? AND current == 1
300              
301 0           for my $book (@$books) {
302 0           my $ean = $book->{ean};
303 0           my $current_price_row;
304              
305             # Update or add the book to the books table
306 0           my $book_row = $dbh->selectrow_hashref($get_book, undef, $ean);
307 0 0         if ($book_row) {
308             # The book exists. Update title & author if necessary
309 0 0 0       unless (_eq($book_row->{title}, $book->{title}) and
310             _eq($book_row->{author}, $book->{author})) {
311 0           $dbh->do(<<'', undef, @$book{qw(title author ean)});
312             UPDATE books SET title = ?, author = ? WHERE ean = ?
313              
314             }
315             # Since the book exists, it might have a price
316 0           $current_price_row = $dbh->selectrow_hashref($get_price, undef, $ean);
317             } else {
318             # The book doesn't exist; add it
319 0           $dbh->do(<<'', undef, @$book{qw(title author ean)});
320             INSERT INTO books (title, author, ean) VALUES (?,?,?)
321              
322             }
323              
324             # Update or add the book to the wishlist_books table
325 0 0         if (exists $existing_priority->{ $ean }) {
326             # The book is already in the wishlist. Update priority if necessary
327 0 0         unless (_numEq($book->{priority}, $existing_priority->{ $ean })) {
328 0           $dbh->do(<<'', undef, $book->{priority}, $wishlist_id, $ean);
329             UPDATE wishlist_books SET priority = ?
330             WHERE wishlist_id = ? AND ean = ?
331              
332             }
333             } else {
334             # Add book to this wishlist
335             ### Inserting: @$book{qw(ean priority date_added)}
336 0           $dbh->do(<<'', undef, $wishlist_id, @$book{qw(ean priority date_added)});
337             INSERT INTO wishlist_books (wishlist_id, ean, priority, date_added)
338             VALUES (?,?,?,?)
339              
340             }
341              
342 0           for my $price (@$book{qw(price list_price)}) {
343 0 0         next unless defined $price;
344 0           $price =~ s/^\s*\$//;
345 0           $price = int($price * 100 + 0.5);
346             }
347              
348 1     1   1506 { no warnings 'uninitialized'; $book->{discount} =~ s/\%// }
  1         2  
  1         2268  
  0            
  0            
349              
350             # Update or add the prices entry
351 0 0 0       if ($current_price_row and
      0        
      0        
352             _numEq($current_price_row->{price}, $book->{price}) and
353             _numEq($current_price_row->{list_price}, $book->{list_price}) and
354             _numEq($current_price_row->{discount}, $book->{discount})) {
355 0           $dbh->do(<<'', undef, $time_fetched, $ean, $current_price_row->{first_recorded});
356             UPDATE prices SET last_checked = ?
357             WHERE ean = ? AND first_recorded = ?
358              
359             } else {
360 0 0         if ($current_price_row) {
361 0           $dbh->do(<<'', undef, $ean, $current_price_row->{first_recorded});
362             UPDATE prices SET current = 0 WHERE ean = ? AND first_recorded = ?
363              
364             }
365 0           $updates->{$ean} = {
366             old => $current_price_row,
367             new => $book,
368             };
369             ### Inserting: $ean
370 0           $dbh->do(<<'', undef, @$book{qw(ean price list_price discount)}, ($time_fetched)x2);
371             INSERT INTO prices (ean, price, list_price, discount, first_recorded, last_checked)
372             VALUES (?,?,?,?,?,?)
373              
374             }
375             } # end for each $book in @$books
376              
377 0           $dbh->commit;
378             } # end write_db
379              
380             sub reduced_price_eans
381             {
382 0     0 0   my $updates = shift->updates;
383              
384 0 0         sort {
385 0           $updates->{$a}{new}{price} <=> $updates->{$b}{new}{price} or
386             $updates->{$a}{new}{title} cmp $updates->{$b}{new}{title}
387             } grep {
388 0           my ($old, $new) = @{$updates->{$_}}{qw(old new)};
  0            
389 0 0 0       $old and defined($new->{price})
      0        
390             and (!defined($old->{price}) or $new->{price} < $old->{price});
391             } keys %$updates;
392             } # end reduced_price_eans
393              
394             sub get_existing_books
395             {
396 0     0 0   my ($self, $wishlist_id) = @_;
397              
398 0           my %existing_priority;
399              
400 0           my $s = $self->dbh->prepare(<<'');
401             SELECT ean, priority FROM wishlist_books
402             WHERE wishlist_id = ? AND date_removed IS NULL
403              
404 0           $s->execute($wishlist_id);
405 0           $s->bind_columns( \( my ($ean, $priority) ) );
406 0           while ($s->fetch) {
407 0           $existing_priority{$ean} = $priority;
408             }
409              
410 0           \%existing_priority;
411             } # end get_existing_books
412              
413             sub get_wishlist_id
414             {
415 0     0 0   my ($self, $wishlist_url) = @_;
416              
417 0           my $dbh = $self->dbh;
418              
419 0           my ($wishlist_id) = $dbh->selectrow_array(<<'', undef, $wishlist_url);
420             SELECT wishlist_id FROM wishlists WHERE url = ?
421              
422 0 0         unless (defined $wishlist_id) {
423 0           $dbh->do(<<'', undef, $wishlist_url);
424             INSERT INTO wishlists (url) VALUES (?)
425              
426 0   0       $wishlist_id = $dbh->last_insert_id((undef)x4)
427             // die "Unable to insert wishlist $wishlist_url";
428             }
429              
430 0           $wishlist_id;
431             } # end get_wishlist_id
432             #---------------------------------------------------------------------
433              
434             sub describe_selected_updates
435             {
436 0     0 0   my $self = shift;
437              
438 0           my $updates = $self->updates;
439              
440 0           map {
441 0           my $book = $updates->{$_}{new};
442 0           my $price = _format_price($book->{price});
443 0 0         if (my $old = $updates->{$_}{old}) {
444 0           $price .= sprintf ' (was %s)', _format_price($old->{price});
445             }
446 0           <<"END UPDATE";
447             Title: $book->{title} ($_)
448             Author: $book->{author}
449             Price: $price
450             END UPDATE
451             } @_;
452             } # end describe_selected_updates
453              
454             #---------------------------------------------------------------------
455              
456             sub email_price_drop_alert
457             {
458 0     0 0   my ($self) = @_;
459              
460 0 0         my @eans = $self->reduced_price_eans or return;
461              
462 0           require Email::Sender::Simple;
463 0           require Email::Simple;
464 0           require Email::Simple::Creator;
465 0           require Encode;
466              
467 0           my $updates = $self->updates;
468 0           my $config = $self->config->{_};
469              
470 0   0       my $address = $config->{report} || $config->{email};
471 0           my @body = $self->describe_selected_updates(@eans);
472              
473 0           my $subject = (@eans > 2)
474             ? sprintf('%d books', scalar @eans)
475             : Encode::encode('MIME-Header',
476 0 0         join(' & ', map { $updates->{$_}{new}{title} } @eans)
477             );
478              
479 0           my $email = Email::Simple->create(
480             header => [
481             To => $address,
482             From => qq'"Barnes & Noble Wishlist" <$address>',
483             Subject => "Price Drop Alert: $subject",
484             'MIME-Version' => '1.0',
485             'Content-Type' => 'text/plain; charset=UTF-8',
486             'Content-Transfer-Encoding' => '8bit',
487             ],
488             body => Encode::encode('utf8', join("\n", @body)),
489             );
490              
491 0           Email::Sender::Simple->send($email);
492             } # end email_price_drop_alert
493             #---------------------------------------------------------------------
494              
495             sub print_matching_books
496             {
497 0     0 0   my ($self, $search) = @_;
498              
499 0           my $books = $self->dbh->selectall_arrayref(<<'END SEARCH', undef, ("%$search%")x2);
500             SELECT ean, price, title, author FROM books NATURAL JOIN prices
501             WHERE prices.current AND (title LIKE ? OR author LIKE ?)
502             ORDER by title, author
503             END SEARCH
504              
505 0 0         if (@$books == 1) {
506 0           print "$books->[0][0] ";
507 0           $self->print_price_history($books->[0][0]);
508             } else {
509 0           foreach my $row (@$books) {
510 0           $row->[1] = _format_price($row->[1]);
511 0           printf "%s %6s %s by %s\n", @$row;
512             }
513 0           print "\n";
514             }
515             } # end print_matching_books
516             #---------------------------------------------------------------------
517              
518             sub print_updates_since
519             {
520 0     0 0   my ($self, $since_date) = @_;
521              
522 0           my $s = $self->dbh->prepare(<<'END SEARCH');
523             SELECT ean, price, title, author FROM books NATURAL JOIN prices
524             WHERE prices.current AND first_recorded >= ?
525             ORDER by first_recorded, price, title, author
526             END SEARCH
527              
528 0           $s->execute($since_date);
529              
530 0           while (my $row = $s->fetch) {
531 0           $row->[1] = _format_price($row->[1]);
532 0           printf "%s %6s %s by %s\n", @$row;
533             }
534              
535 0           print "\n";
536             } # end print_updates_since
537             #---------------------------------------------------------------------
538              
539             sub print_price_history
540             {
541 0     0 0   my ($self, $ean) = @_;
542              
543 0           my $dbh = $self->dbh;
544              
545 0           my $book = $dbh->selectrow_hashref(
546             'SELECT title, author FROM books WHERE ean = ?', undef, $ean
547             );
548              
549 0           print "$book->{title} by $book->{author}\n";
550              
551 0           my $history = $dbh->prepare(<<'END HISTORY');
552             SELECT first_recorded, last_checked, price, list_price, discount
553             FROM prices WHERE ean = ? ORDER BY first_recorded
554             END HISTORY
555              
556 0           $history->execute($ean);
557              
558 0           while (my $row = $history->fetchrow_hashref) {
559 0           $_ =~ s/ .+// for @$row{qw(first_recorded last_checked)};
560 0 0         printf("%s - %s %6s%s%s\n", @$row{qw(first_recorded last_checked)},
    0          
561             _format_price($row->{price}),
562             $row->{list_price}
563             ? " (list " . _format_price($row->{list_price}) . ")"
564             : '',
565             $row->{discount} ? " ($row->{discount}% off)" : '');
566             }
567              
568 0           print "\n";
569             } # end print_price_history
570              
571             #---------------------------------------------------------------------
572              
573             sub print_updates
574             {
575 0     0 0   my $self = shift;
576              
577 0           my $updates = $self->updates;
578              
579 0 0         my @eans = sort {
580 0           $updates->{$a}{new}{title} cmp $updates->{$b}{new}{title} or
581             $updates->{$a}{new}{author} cmp $updates->{$b}{new}{author}
582             } keys %$updates;
583              
584 0           print join("\n", $self->describe_selected_updates(@eans));
585             } # end print_updates
586             #---------------------------------------------------------------------
587              
588             sub update_wishlists
589             {
590 0     0 0   my $self = shift;
591              
592 0           my $config = $self->config;
593 0           my $m = $self->mech;
594              
595             # Ensure we can open the database before we start making web requests
596 0           $self->dbh;
597              
598 0           $self->login;
599              
600 0           for my $wishlist (sort keys %$config) {
601 0 0         next if $wishlist eq '_'; # the root INI section
602              
603 0           my $response = $m->get( $config->{$wishlist}{wishlist} );
604 0           my $books = $self->scrape_response($response);
605             # path("/tmp/wishlist.html")->spew_utf8($response->content);
606 0   0       $self->write_db($config->{$wishlist}{wishlist}, $response->last_modified // $response->date, $books);
607             }
608             } # end update_wishlists
609              
610             #---------------------------------------------------------------------
611              
612             sub usage {
613 0     0 0   my $name = $0;
614 0           $name =~ s!^.*[/\\]!!;
615              
616 0           shift->close_dbh;
617              
618 0           print "$name $VERSION\n";
619 0 0 0       exit if $_[0] and $_[0] eq 'version';
620 0           print <<"END USAGE";
621             \nUsage: $name [options] [EAN_or_TITLE_or_AUTHOR] ...
622             -e, --email Send Price Drop Alert email (implies --update)
623             -q, --quiet Don't print list of updates
624             -s, --since=DATE Print books whose price changed on or after DATE
625             -u, --update Download current prices from wishlist
626             --configure Create and/or edit the config file
627             --help Display this help message
628             --version Display version information
629             END USAGE
630              
631 0           exit;
632             } # end usage
633             #---------------------------------------------------------------------
634              
635             sub run
636             {
637 0     0 0   my ($self, @args) = @_;
638              
639             # Process command line options
640 0           my ($fetch_wishlist, $quiet, $send_email, $since_date);
641             {
642 0           require Getopt::Long; Getopt::Long->VERSION(2.24); # object-oriented
  0            
  0            
643 0           my $getopt = Getopt::Long::Parser->new(
644             config => [qw(bundling no_getopt_compat)]
645             );
646 0     0     my $usage = sub { $self->usage(@_) };
  0            
647              
648             $getopt->getoptionsfromarray(\@args,
649             'email|e' => \$send_email,
650             'quiet|q' => \$quiet,
651             'since|s=s' => \$since_date,
652             'update|u' => \$fetch_wishlist,
653 0     0     'configure' => sub { $self->configure; exit },
  0            
654 0 0         'help' => $usage,
655             'version' => $usage
656             ) or $self->usage;
657             }
658              
659             # Update database & send email if requested
660 0 0 0       if ($fetch_wishlist or $send_email) {
    0 0        
661 0           $self->update_wishlists;
662              
663 0 0         $self->email_price_drop_alert if $send_email;
664 0 0         $self->print_updates unless $quiet;
665             } elsif (not @args and not $since_date) {
666             # Didn't fetch updates and no request to display book data
667 0 0         if ($self->config_file->is_file) {
668 0           $self->usage;
669             } else {
670 0           $self->configure;
671             }
672             }
673              
674 0 0         if ($since_date) {
675 0           $self->print_updates_since($since_date);
676             }
677              
678             # Display data from the database about requested books
679 0           foreach my $arg (@args) {
680 0 0         if ($arg =~ /^[0-9]{13}\z/) {
681 0           $self->print_price_history($arg);
682             } else {
683 0           $self->print_matching_books($arg);
684             }
685             }
686              
687             # Disconnect from the database
688 0           $self->close_dbh;
689             } # end run
690              
691             #=====================================================================
692             # Package Return Value:
693              
694             1;
695              
696             __END__