File Coverage

blib/lib/WWW/Noss/DB.pm
Criterion Covered Total %
statement 192 239 80.3
branch 74 124 59.6
condition 14 36 38.8
subroutine 28 30 93.3
pod 14 14 100.0
total 322 443 72.6


line stmt bran cond sub pod time code
1             package WWW::Noss::DB;
2 2     2   806 use 5.016;
  2         8  
3 2     2   13 use strict;
  2         5  
  2         73  
4 2     2   10 use warnings;
  2         4  
  2         200  
5             our $VERSION = '2.02';
6              
7 2     2   13 use List::Util qw(all any max none);
  2         4  
  2         237  
8              
9 2     2   3997 use DBD::SQLite;
  2         114837  
  2         114  
10 2     2   21 use DBI;
  2         3  
  2         97  
11 2     2   734 use JSON;
  2         14806  
  2         27  
12              
13 2     2   2425 use WWW::Noss::FeedReader qw(read_feed);
  2         8  
  2         220  
14 2     2   1214 use WWW::Noss::Util qw(resolve_url);
  2         5  
  2         8279  
15              
16             my %DAY_MAP = (
17             0 => 'Sunday',
18             1 => 'Monday',
19             2 => 'Tuesday',
20             3 => 'Wednesday',
21             4 => 'Thursday',
22             5 => 'Friday',
23             6 => 'Saturday',
24             );
25              
26             sub _initialize {
27              
28 2     2   6 my ($self) = @_;
29              
30 2         25 $self->{ DB } = DBI->connect(
31             "dbi:SQLite:dbname=$self->{ Path }", '', '',
32             {
33             RaiseError => 1,
34             AutoInactiveDestroy => 1,
35             }
36             );
37              
38 2         4408 $self->{ DB }->do('PRAGMA journal_mode = WAL');
39 2         17289 $self->{ DB }->do('PRAGMA cache_size = 10000');
40              
41             $self->{ DB }->do(
42 2         80 q{
43             CREATE TABLE IF NOT EXISTS feeds (
44             nossname TEXT NOT NULL UNIQUE,
45             nosslink TEXT NOT NULL,
46             title TEXT NOT NULL,
47             link TEXT,
48             description TEXT,
49             updated INTEGER,
50             author TEXT,
51             category TEXT,
52             generator TEXT,
53             image TEXT,
54             rights TEXT,
55             skiphours TEXT,
56             skipdays TEXT
57             );
58             }
59             );
60              
61             $self->{ DB }->do(
62 2         13033 q{
63             CREATE INDEX IF NOT EXISTS
64             idx_feeds_nossname
65             ON
66             feeds(nossname);
67             }
68             );
69              
70             $self->{ DB }->do(
71 2         5254 q{
72             CREATE TABLE IF NOT EXISTS posts (
73             nossid INTEGER NOT NULL,
74             status TEXT NOT NULL,
75             feed TEXT NOT NULL,
76             title TEXT,
77             link TEXT,
78             author TEXT,
79             category TEXT,
80             summary TEXT,
81             published INTEGER,
82             updated INTEGER,
83             uid TEXT,
84             nossuid TEXT NOT NULL,
85             displaytitle TEXT
86             );
87             }
88             );
89              
90 26         386 my @post_cols = map { $_->[0] } @{
91 2         5192 $self->{ DB }->selectall_arrayref(
92 2         23 q{
93             SELECT
94             name
95             FROM pragma_table_info('posts');
96             }
97             )
98             };
99              
100             # TODO: Someday, we should be able to get rid of this
101             # Pre-1.06 databases do not have nossuid, add it.
102 2 50   24   25 if (none { $_ eq 'nossuid' } @post_cols) {
  24         30  
103             $self->{ DB }->do(
104 0         0 q{
105             ALTER TABLE
106             posts
107             ADD COLUMN
108             nossuid TEXT NOT NULL DEFAULT ';;;;';
109             }
110             );
111             $self->{ DB }->do(
112 0         0 q{
113             UPDATE
114             posts
115             SET
116             nossuid = (
117             ifnull(uid, '') || ';' ||
118             ifnull(feed, '') || ';' ||
119             ifnull(title, '') || ';' ||
120             ifnull(link, '') || ';' ||
121             ifnull(published, '')
122             );
123             }
124             ); # no concat_ws() in the sqlite version we support :-(
125             }
126              
127             # Pre-1.09 databases do not have display title, add it.
128 2 50   26   12 if (none { $_ eq 'displaytitle' } @post_cols) {
  26         36  
129             $self->{ DB }->do(
130 0         0 q{
131             ALTER TABLE
132             posts
133             ADD COLUMN
134             displaytitle TEXT;
135             }
136             );
137             $self->{ DB }->do(
138 0         0 q{
139             UPDATE
140             posts
141             SET
142             displaytitle = title;
143             }
144             );
145             }
146              
147             $self->{ DB }->do(
148 2         16 q{
149             CREATE INDEX IF NOT EXISTS
150             idx_posts_uid
151             ON
152             posts(uid);
153             }
154             );
155              
156             $self->{ DB }->do(
157 2         4878 q{
158             CREATE INDEX IF NOT EXISTS
159             idx_posts_feedid
160             ON
161             posts(feed, nossid);
162             }
163             );
164              
165             $self->{ DB }->do(
166 2         5124 q{
167             CREATE UNIQUE INDEX IF NOT EXISTS
168             idx_posts_nossuid
169             ON
170             posts(nossuid);
171             }
172             );
173              
174 2         3861 return 1;
175              
176             }
177              
178             sub new {
179              
180 2     2 1 142215 my ($class, $file) = @_;
181              
182 2         10 my $self = {
183             Path => undef,
184             DB => undef,
185             };
186              
187 2         6 bless $self, $class;
188              
189 2         9 $self->{ Path } = $file;
190              
191 2         10 $self->_initialize;
192              
193 2         11 return $self;
194              
195             }
196              
197             sub has_feed {
198              
199 2     2 1 1633 my ($self, $feed) = @_;
200              
201             my $name =
202 2 50       6 eval { $feed->isa('WWW::Noss::FeedConfig') }
  2         22  
203             ? $feed->name
204             : $feed;
205              
206             my $row = $self->{ DB }->selectrow_arrayref(
207 2         32 q{
208             SELECT
209             rowid
210             FROM
211             feeds
212             WHERE
213             nossname = ?;
214             },
215             undef,
216             $name
217             );
218              
219 2         2372 return defined $row;
220              
221             }
222              
223             sub load_feed {
224              
225 2     2 1 4 my ($self, $feed) = @_;
226              
227 2         11 my ($feedref, $postref) = read_feed($feed);
228              
229             $self->{ DB }->do(
230             q{
231             INSERT INTO feeds(
232             nossname,
233             nosslink,
234             title,
235             link,
236             description,
237             updated,
238             author,
239             category,
240             generator,
241             image,
242             rights,
243             skiphours,
244             skipdays
245             )
246             VALUES(
247             ?,
248             ?,
249             ?,
250             ?,
251             ?,
252             (0 + ?),
253             ?,
254             ?,
255             ?,
256             ?,
257             ?,
258             ?,
259             ?
260             )
261             ON CONFLICT (nossname) DO
262             UPDATE
263             SET
264             nossname = excluded.nossname,
265             nosslink = excluded.nosslink,
266             title = excluded.title,
267             link = excluded.link,
268             description = excluded.description,
269             updated = excluded.updated,
270             author = excluded.author,
271             category = excluded.category,
272             generator = excluded.generator,
273             image = excluded.image,
274             rights = excluded.rights,
275             skiphours = excluded.skiphours,
276             skipdays = excluded.skipdays;
277             },
278             undef,
279             $feedref->{ nossname },
280             $feedref->{ nosslink },
281             $feedref->{ title },
282             $feedref->{ link },
283             $feedref->{ description },
284             $feedref->{ updated },
285             $feedref->{ author },
286             (defined $feedref->{ category } ? encode_json($feedref->{ category }) : undef),
287             $feedref->{ generator },
288             $feedref->{ image },
289             $feedref->{ rights },
290             (defined $feedref->{ skiphours } ? encode_json($feedref->{ skiphours }) : undef),
291 2 50       89 (defined $feedref->{ skipdays } ? encode_json($feedref->{ skipdays }) : undef),
    100          
    100          
292             );
293              
294             my $upsert_post = $self->{ DB }->prepare(
295 2         5797 q{
296             INSERT INTO posts(
297             nossid,
298             status,
299             feed,
300             title,
301             link,
302             author,
303             category,
304             summary,
305             published,
306             updated,
307             uid,
308             nossuid,
309             displaytitle
310             )
311             VALUES (
312             (0 + ?),
313             ?,
314             ?,
315             ?,
316             ?,
317             ?,
318             ?,
319             ?,
320             (0 + ?),
321             (0 + ?),
322             ?,
323             ?,
324             ?
325             )
326             ON CONFLICT (nossuid) DO
327             UPDATE
328             SET
329             nossid = excluded.nossid,
330             author = excluded.author,
331             category = excluded.category,
332             summary = excluded.summary,
333             updated = excluded.updated,
334             displaytitle = excluded.displaytitle;
335             },
336             );
337              
338 2         318 my $new = 0;
339 2         5 my @ok;
340              
341             $self->{ DB }->sqlite_update_hook(
342             sub {
343 20 100   20   1096 $new++ if $_[0] == DBD::SQLite::INSERT;
344 20         79871 push @ok, $_[3];
345             }
346 2         31 );
347              
348 2         29 for my $e (@$postref) {
349             $upsert_post->execute(
350             $e->{ nossid },
351             ($feed->autoread ? 'read' : 'unread'),
352             $e->{ feed },
353             $e->{ title },
354             $e->{ link },
355             $e->{ author },
356             (defined $e->{ category } ? encode_json($e->{ category }) : undef),
357             $e->{ summary },
358             $e->{ published },
359             $e->{ updated },
360             $e->{ uid },
361             $e->{ nossuid },
362             $e->{ displaytitle },
363 8 50       102 );
    50          
364             }
365              
366 2         24 my $ok_set = sprintf "(%s)", join ",", @ok;
367              
368             $self->{ DB }->do(
369 2         30 qq{
370             DELETE FROM
371             posts
372             WHERE
373             feed = ? AND
374             rowid NOT IN $ok_set;
375             },
376             undef,
377             $feed->name,
378             );
379              
380 2         1116 return $new;
381              
382             }
383              
384             sub feed {
385              
386 1     1 1 7 my ($self, $feed, %param) = @_;
387              
388             my $name =
389 1 50       2 eval { $feed->isa('WWW::Noss::FeedConfig') }
  1         12  
390             ? $feed->name
391             : $feed;
392              
393             my $row = $self->{ DB }->selectrow_hashref(
394 1         19 q{
395             SELECT
396             nossname,
397             nosslink,
398             title,
399             link,
400             description,
401             updated,
402             author,
403             category,
404             generator,
405             image,
406             rights,
407             skiphours,
408             skipdays
409             FROM
410             feeds
411             WHERE
412             nossname = ?;
413             },
414             undef,
415             $name
416             );
417              
418 1 50       275 return undef unless defined $row;
419              
420             $row->{ category } =
421             defined $row->{ category }
422             ? decode_json($row->{ category })
423 1 50       40 : [];
424             $row->{ skiphours } =
425             defined $row->{ skiphours }
426             ? decode_json($row->{ skiphours })
427 1 50       7 : [];
428             $row->{ skipdays } =
429             defined $row->{ skipdays }
430             ? decode_json($row->{ skipdays })
431 1 50       6 : [];
432 1 50       5 if (defined $row->{ link }) {
433 1         8 $row->{ link } = resolve_url($row->{ link }, $row->{ nosslink });
434             }
435              
436 1 50       5 if ($param{ post_info }) {
437              
438             my $posts = $self->{ DB }->selectall_arrayref(
439 1         13 q{
440             SELECT
441             status,
442             updated,
443             published
444             FROM
445             posts
446             WHERE
447             feed = ?;
448             },
449             undef,
450             $name
451             );
452              
453             $row->{ updated } //=
454 0         0 (max grep { defined } map { $_->[1] } @$posts) //
  0         0  
455 1   0     157 (max grep { defined } map { $_->[2] } @$posts);
  0   33     0  
  0         0  
456              
457 1         4 $row->{ posts } = scalar @$posts;
458 1         4 $row->{ unread } = scalar grep { $_->[0] eq 'unread' } @$posts;
  5         14  
459              
460             }
461              
462 1         26 return $row;
463              
464             }
465              
466             sub feeds {
467              
468 2     2 1 8 my ($self) = @_;
469              
470             my $feeds = $self->{ DB }->selectall_arrayref(
471 2         106 q{
472             SELECT
473             nossname,
474             nosslink,
475             title,
476             link,
477             description,
478             updated,
479             author,
480             category,
481             generator,
482             image,
483             rights,
484             skiphours,
485             skipdays
486             FROM
487             feeds;
488             },
489             { Slice => {} },
490             );
491              
492 2         655 for my $f (@$feeds) {
493             $f->{ category } =
494             defined $f->{ category }
495             ? decode_json($f->{ category })
496 4 50       35 : [];
497             $f->{ skiphours } =
498             defined $f->{ skiphours }
499             ? decode_json($f->{ skiphours })
500 4 100       24 : [];
501             $f->{ skipdays } =
502             defined $f->{ skipdays }
503             ? decode_json($f->{ skipdays })
504 4 100       16 : [];
505 4 50       13 if (defined $f->{ link }) {
506 4         17 $f->{ link } = resolve_url($f->{ link }, $f->{ nosslink });
507             }
508             }
509              
510 2         50 return @$feeds;
511              
512             }
513              
514             sub del_feeds {
515              
516 0     0 1 0 my ($self, @feeds) = @_;
517              
518             my $feed_set =
519             sprintf '(%s)',
520             join ',',
521 0         0 map { $self->{ DB }->quote($_) }
  0         0  
522             @feeds;
523              
524             $self->{ DB }->do(
525 0         0 qq{
526             DELETE FROM
527             feeds
528             WHERE
529             nossname IN $feed_set;
530             }
531             );
532              
533             $self->{ DB }->do(
534 0         0 qq{
535             DELETE FROM
536             posts
537             WHERE
538             feed IN $feed_set;
539             }
540             );
541              
542 0         0 return 1;
543              
544             }
545              
546             sub post {
547              
548 10     10 1 36 my ($self, $feed, $post) = @_;
549              
550             my $name =
551 10 50       19 eval { $feed->isa('WWW::Noss::FeedConfig') }
  10         98  
552             ? $feed->name
553             : $feed;
554              
555 10         20 my $postref;
556              
557 10 100       26 if ($post < 0) {
558 1         4 my $off = -$post - 1;
559             $postref = $self->{ DB }->selectrow_hashref(
560 1         11 qq{
561             SELECT
562             nossid,
563             status,
564             feed,
565             title,
566             link,
567             author,
568             category,
569             summary,
570             published,
571             updated,
572             uid,
573             nossuid,
574             displaytitle
575             FROM
576             posts
577             WHERE
578             feed = ?
579             ORDER BY
580             nossid DESC
581             LIMIT 1 OFFSET $off;
582             },
583             undef,
584             $feed
585             );
586             } else {
587             $postref = $self->{ DB }->selectrow_hashref(
588 9         81 q{
589             SELECT
590             nossid,
591             status,
592             feed,
593             title,
594             link,
595             author,
596             category,
597             summary,
598             published,
599             updated,
600             uid,
601             nossuid,
602             displaytitle
603             FROM
604             posts
605             WHERE
606             feed = ? AND
607             nossid = (0 + ?);
608             },
609             undef,
610             $feed,
611             $post
612             );
613             }
614              
615 10 50       2641 return undef unless defined $postref;
616              
617             $postref->{ category } =
618             defined $postref->{ category }
619             ? decode_json($postref->{ category })
620 10 50       89 : [];
621             # Convert '/foo' and '//foo' relative links
622 10 50 33     64 if (defined $postref->{ link } and $postref->{ link } =~ /^\//) {
623 0         0 my $feed_info = $self->feed($feed);
624             my $conf = resolve_url(
625             $postref->{ link },
626             $feed_info->{ link } // $feed_info->{ nosslink }
627 0   0     0 );
628 0 0       0 $postref->{ link } = $conf if defined $conf;
629             }
630              
631 10         99 return $postref;
632              
633             }
634              
635             sub first_unread {
636              
637 1     1 1 5 my ($self, $feed) = @_;
638              
639             my $name =
640 1 50       3 eval { $feed->isa('WWW::Noss::FeedConfig') }
  1         12  
641             ? $feed->name
642             : $feed;
643              
644             my $postref = $self->{ DB }->selectrow_hashref(
645 1         9 q{
646             SELECT
647             nossid,
648             status,
649             feed,
650             title,
651             link,
652             author,
653             category,
654             summary,
655             published,
656             updated,
657             uid,
658             nossuid,
659             displaytitle
660             FROM
661             posts
662             WHERE
663             feed = ? AND
664             status = 'unread'
665             ORDER BY
666             nossid DESC;
667             },
668             undef,
669             $feed,
670             );
671              
672 1 50       262 return undef unless defined $postref;
673              
674             $postref->{ category } =
675             defined $postref->{ category }
676             ? decode_json($postref->{ category })
677 1 50       11 : [];
678             # Convert '/foo' and '//foo' relative links
679 1 50 33     10 if (defined $postref->{ link } and $postref->{ link } =~ /^\//) {
680 0         0 my $feed_info = $self->feed($feed);
681             my $conf = resolve_url(
682             $postref->{ link },
683             $feed_info->{ link } // $feed_info->{ nosslink }
684 0   0     0 );
685 0 0       0 $postref->{ link } = $conf if defined $conf;
686             }
687              
688 1         8 return $postref;
689              
690             }
691              
692             sub largest_id {
693              
694 2     2 1 1523 my ($self, @feeds) = @_;
695              
696 2         4 my $where = '';
697              
698 2 100       8 if (@feeds) {
699             $where = sprintf
700             "WHERE feed IN (%s)",
701 1         4 join(',', map { $self->{ DB }->quote($_) } @feeds);
  1         12  
702             }
703              
704             my $row = $self->{ DB }->selectrow_arrayref(
705 2         43 qq{
706             SELECT
707             nossid
708             FROM
709             posts
710             $where
711             ORDER BY
712             nossid DESC
713             LIMIT 1;
714             },
715             );
716              
717 2 50       470 return defined $row ? $row->[0] : undef;
718              
719             }
720              
721             sub look {
722              
723 13     13 1 2185 my ($self, %param) = @_;
724              
725 13         25 my @posts;
726              
727 13         33 my $title = $param{ title };
728             my @feeds =
729             ref $param{ feeds } eq 'ARRAY'
730 13 100       74 ? @{ $param{ feeds } }
  1         3  
731             : ();
732 13         24 my $status = $param{ status };
733             my @tags =
734             ref $param{ tags } eq 'ARRAY'
735 13 100       32 ? @{ $param{ tags } }
  1         5  
736             : ();
737             my @content =
738             ref $param{ content } eq 'ARRAY'
739 13 100       33 ? @{ $param{ content } }
  1         5  
740             : ();
741 13   100     66 my $order = $param{ order } // 'feed';
742 13   100     51 my $reverse = $param{ reverse } // 0;
743 13   100     44 my $limit = $param{ limit } // 0;
744             my $callback = $param{ callback } // sub {
745 74     74   150 push @posts, $_[0];
746 13   66     82 };
747              
748 13 50 66     74 if (defined $status and $status !~ /^(un)?read$/) {
749 0         0 die "status must be 'read' or 'unread'";
750             }
751              
752 13 50       45 unless (ref $callback eq 'CODE') {
753 0         0 die "callback must be a code ref";
754             }
755              
756 13         19 my @wheres;
757              
758 13 100       33 if (defined $title) {
759 1         12 push @wheres, 'displaytitle REGEXP ' . $self->{ DB }->quote($title);
760             }
761              
762 13 100       49 if (@feeds) {
763             my $feed_set =
764             sprintf '(%s)',
765             join ',',
766 1         4 map { $self->{ DB }->quote($_) }
  1         10  
767             @feeds;
768 1         19 push @wheres, "feed IN $feed_set";
769             }
770              
771 13 100       30 if (defined $status) {
772 1         12 push @wheres, 'status = ' . $self->{ DB }->quote($status);
773             }
774              
775 13 100       46 if (@content) {
776 1         3 push @wheres, map { 'summary REGEXP ' . $self->{ DB }->quote($_) } @content;
  1         10  
777             }
778              
779 13 100       51 my $where = @wheres ? 'WHERE ' . join ' AND ', @wheres : '';
780              
781 13 100       44 my ($asc, $desc, $first, $last) =
782             $reverse
783             ? ('DESC', 'ASC' , 'LAST', 'FIRST')
784             : ('ASC', 'DESC', 'FIRST', 'LAST');
785              
786 13         20 my $order_clause;
787              
788 13 100       36 if ($order eq 'feed') {
    100          
    50          
789 11         22 $order_clause = qq{
790             feed COLLATE NOCASE $asc,
791             nossid $asc
792             };
793             } elsif ($order eq 'title') {
794 1         5 $order_clause = qq{
795             displaytitle COLLATE NOCASE $asc NULLS $last,
796             feed COLLATE NOCASE $asc,
797             nossid $asc
798             };
799             } elsif ($order eq 'date') {
800 1         5 $order_clause = qq{
801              
802             CASE
803             WHEN updated IS NOT NULL THEN updated
804             ELSE published
805             END $asc NULLS $first,
806             feed COLLATE NOCASE $asc,
807             nossid $asc
808             };
809             } else {
810 0         0 die "Cannot order posts by '$order'";
811             }
812              
813 13 100       35 my $limit_clause = $limit > 0 ? "LIMIT $limit" : '';
814              
815             my $sth = $self->{ DB }->prepare(
816 13         125 qq{
817             SELECT
818             nossid,
819             status,
820             feed,
821             title,
822             link,
823             author,
824             category,
825             summary,
826             published,
827             updated,
828             uid,
829             nossuid,
830             displaytitle
831             FROM
832             posts
833             $where
834             ORDER BY
835             $order_clause
836             $limit_clause;
837             }
838             );
839              
840 13         2686 $sth->execute;
841              
842 13         205 my $n = 0;
843              
844 13         24 my %cache;
845              
846 13         536 while (my $p = $sth->fetchrow_hashref) {
847             $p->{ category } =
848             defined $p->{ category }
849             ? decode_json($p->{ category })
850 82 50       515 : [];
851 82 50 33     401 if (defined $p->{ link } and $p->{ link } =~ /^\//) {
852 0         0 my $feed_info;
853 0 0       0 if (exists $cache{ $p->{ feed } }) {
854 0         0 $feed_info = $cache{ $p->{ feed } };
855             } else {
856 0         0 $feed_info = $self->feed($p->{ feed });
857 0         0 $cache{ $p->{ feed } } = $feed_info;
858             }
859             my $conv = resolve_url(
860             $p->{ link },
861             $feed_info->{ link } // $feed_info->{ nosslink }
862 0   0     0 );
863             }
864             next unless all {
865 24     24   61 my $t = $_;
866 24         66 any { $_ =~ $t } @{ $p->{ category } };
  48         338  
  24         86  
867 82 50       496 } @tags;
868 82         344 $callback->($p);
869 82         2211 $n++;
870             }
871              
872 13 100       531 return defined $param{ callback } ? $n : @posts;
873              
874             }
875              
876             sub mark {
877              
878 3     3 1 1464 my ($self, $mark, $feed, @post) = @_;
879              
880             my $name =
881 3 50       10 eval { $feed->isa('WWW::Noss::FeedConfig') }
  3         42  
882             ? $feed->name
883             : $feed;
884              
885 3 50       30 unless ($mark =~ /^(un)?read$/) {
886 0         0 die "Posts can only be marked as either 'read' or 'unread'";
887             }
888              
889 3         29 my @wheres = ("feed = " . $self->{ DB }->quote($feed));
890              
891 3 100       52 if (@post) {
892 1         7 push @wheres, sprintf "nossid IN (%s)", join ',', @post;
893             }
894              
895 3         11 push @wheres, "status != '$mark'";
896              
897 3         10 my $where = join ' AND ', @wheres;
898              
899             my $num = $self->{ DB }->do(
900 3         25 qq{
901             UPDATE
902             posts
903             SET
904             status = ?
905             WHERE
906             $where;
907             },
908             undef,
909             $mark
910             );
911              
912 3         209 return $num;
913              
914             }
915              
916             sub skip {
917              
918 0     0 1 0 my ($self, $feed) = @_;
919              
920             my $name =
921 0 0       0 eval { $feed->isa('WWW::Noss::FeedConfig') }
  0         0  
922             ? $feed->name
923             : $feed;
924              
925             my $row = $self->{ DB }->selectrow_hashref(
926 0         0 q{
927             SELECT
928             skiphours,
929             skipdays
930             FROM
931             feeds
932             WHERE
933             nossname = ?;
934             },
935             undef,
936             $name
937             );
938              
939 0 0       0 return undef unless defined $row;
940              
941 0         0 my ($hour, $day) = (gmtime)[2, 6];
942              
943             my @skip_hours =
944             defined $row->{ skiphours }
945 0 0       0 ? @{ decode_json($row->{ skiphours }) }
  0         0  
946             : ();
947             my @skip_days =
948             defined $row->{ skipdays }
949 0 0       0 ? @{ decode_json($row->{ skipdays }) }
  0         0  
950             : ();
951              
952 0 0       0 if (grep { $hour eq $_ } @skip_hours) {
  0         0  
953 0         0 return 1;
954             }
955              
956 0 0       0 if (grep { $DAY_MAP{ $day } eq $_ } @skip_days) {
  0         0  
957 0         0 return 1;
958             }
959              
960 0         0 return 0;
961              
962             }
963              
964             sub vacuum {
965              
966 1     1 1 3 my ($self) = @_;
967              
968 1         13 $self->{ DB }->do(q{ VACUUM; });
969              
970 1         7581 return 1;
971              
972             }
973              
974             sub finish {
975              
976 4     4 1 12 my ($self) = @_;
977              
978 4         8259 $self->{ DB }->disconnect;
979              
980             }
981              
982             DESTROY {
983              
984 2     2   1239 my ($self) = @_;
985              
986 2         75 $self->finish;
987              
988             }
989              
990             1;
991              
992             =head1 NAME
993              
994             WWW::Noss::DB - noss SQLite database interface
995              
996             =head1 USAGE
997              
998             use WWW::Noss::DB;
999              
1000             my $db = WWW::Noss::DB->new('path/to/database');
1001              
1002             =head1 DESCRIPTION
1003              
1004             B is a module that provides an object-oriented interface to
1005             L's SQLite feed database. This is a private module, please consult the
1006             L manual for user documentation.
1007              
1008             =head1 METHODS
1009              
1010             =over 4
1011              
1012             =item $db = WWW::Noss::DB->new($file)
1013              
1014             Loads a L database from C<$file> or initializes it if ones does not
1015             exist, then returns a blessed B object.
1016              
1017             =item $bool = $db->has_feed($feed)
1018              
1019             Returns true if C<$db> has the feed C<$feed>.
1020              
1021             =item $new = $db->load_feed($feed_conf)
1022              
1023             Loads the L object C<$feed_conf> into the database.
1024             Returns the number of new posts loaded if successful, dies on failure.
1025              
1026             =item \%feed = $db->feed($feed, [ %param ])
1027              
1028             Returns a hash ref of information about the feed C<$feed>. C<$feed> can either
1029             be the name of a feed or a L object. C<%param> is an
1030             optional hash of additional parameters.
1031              
1032             C<\%feed> will look something like this:
1033              
1034             {
1035             nossname => ...,
1036             nosslink => ...,
1037             title => ...,
1038             link => ...,
1039             description => ...,
1040             updated => ...,
1041             author => ...,
1042             category => [ ... ],
1043             generator => ...,
1044             image => ...,
1045             rights => ...,
1046             skiphours => [ ... ],
1047             skipdays => [ ... ],
1048             posts => ..., # only with post_info set
1049             unread => ..., # only with post_info set
1050             }
1051              
1052             The following is a list of valid fields for C<%param>:
1053              
1054             =over 4
1055              
1056             =item post_info
1057              
1058             Boolean determining whether to also retrieve the total number of posts and
1059             number of unread posts. This causes C to be slower. Defaults to false.
1060              
1061             =back
1062              
1063             =item @feeds = $db->feeds()
1064              
1065             Returns an array of feed hash refs of each feed loaded in the database. The
1066             hash refs follow the same format as the one returned by the C method,
1067             minus the C and C fields.
1068              
1069             =item $rt = $db->del_feeds(@feeds)
1070              
1071             Deletes the feeds C<@feeds> from the database. Returns C<1> on success.
1072              
1073             =item \%post = $db->post($feed, $post)
1074              
1075             Returns the hash ref C<\%post> representing post number C<$post> in feed
1076             C<$feed>. C<$feed> can be a feed name or a L object.
1077             If C<$post> is negative, returns the nth post from the end of the feed.
1078              
1079             C<\%post> will look something like this:
1080              
1081             {
1082             nossid => ...,
1083             status => ...,
1084             feed => ...,
1085             title => ...,
1086             link => ...,
1087             author => ...,
1088             category => [ ... ],
1089             summary => ...,
1090             published => ...,
1091             updated => ...,
1092             uid => ...,
1093             nossuid => ...,
1094             displaytitle => ...,
1095             }
1096              
1097             Returns C if no matching post exists.
1098              
1099             =item \%post = $db->first_unread($feed)
1100              
1101             Returns the first unread post in C<$feed>. C<$feed> can be a feed name or a
1102             L object. C<\%post> follows the same format as the one
1103             returned by C. Returns C if no unread post exists.
1104              
1105             =item $id = $db->largest_id([ @feeds ])
1106              
1107             Returns the largest ID in the specified feeds. If feeds is not provided, all
1108             feeds are searched.
1109              
1110             =item @posts = $db->look([ %param ])
1111              
1112             Returns a list of posts matching the parameters specified in C<%param>. If no
1113             parameters are provided, returns a list of every post in the database.
1114              
1115             The following are a list of valid fields to C<%param>:
1116              
1117             =over 4
1118              
1119             =item title
1120              
1121             Only return posts whose titles match the given regex.
1122              
1123             =item feeds
1124              
1125             Only return posts that are in the feeds of the given array ref.
1126              
1127             =item status
1128              
1129             Only return posts that are of the given status. Can either be C<'read'> or
1130             C<'unread'>.
1131              
1132             =item tags
1133              
1134             Only return posts containing the tags specified by the given array ref.
1135              
1136             =item content
1137              
1138             Only return posts whose content match all regexes in the given array ref.
1139              
1140             =item order
1141              
1142             How C should order the returned posts. The following are valid values:
1143              
1144             =over 4
1145              
1146             =item feed
1147              
1148             Order by feed alphabetically.
1149              
1150             =item title
1151              
1152             Order by post title alphabetically.
1153              
1154             =item date
1155              
1156             Order by post date.
1157              
1158             =back
1159              
1160             =item reverse
1161              
1162             Return the post list in reverse order.
1163              
1164             =item limit
1165              
1166             Limit the number of posts that are selected. If equal to or less than C<0>,
1167             there is no limit. Default is C<0> (no limit).
1168              
1169             =item callback
1170              
1171             Subroutine reference to a callback to call on each post reference. The post
1172             reference is available via the C<@_> array. When this option is set,
1173             C will return the number of posts processed instead of the post list.
1174              
1175             =back
1176              
1177             =item $num = $db->mark($mark, $feed, @post)
1178              
1179             Mark the posts C<@post> in feed C<$feed> as C<$mark>. Returns the number of posts
1180             updated. C<$feed> can be either a feed name or L object.
1181             C<$mark> can either be C<'read'> or C<'unread'>. C<@post> is a list of post IDs
1182             to update. If C<@post> is empty, all posts in C<$feed> are updated.
1183              
1184             =item $bool = $db->skip($feed)
1185              
1186             Check whether you are supposed to skip updating C<$feed> right now. C<$feed>
1187             can either be a feed name or L object. C is
1188             returned if C<$feed> does not exist.
1189              
1190             =item $db->vacuum()
1191              
1192             Runs the C L command on the database, which frees up any
1193             unused space within the database and reduces its total size.
1194              
1195             =item $db->finish()
1196              
1197             Closes the connection to the local database. Is automatically called when a
1198             B object is destroyed.
1199              
1200             =back
1201              
1202             =head1 AUTHOR
1203              
1204             Written by Samuel Young, Esamyoung12788@gmail.comE.
1205              
1206             This project's source can be found on its
1207             L. Comments and pull
1208             requests are welcome!
1209              
1210             =head1 COPYRIGHT
1211              
1212             Copyright (C) 2025-2026 Samuel Young
1213              
1214             This program is free software: you can redistribute it and/or modify
1215             it under the terms of the GNU General Public License as published by
1216             the Free Software Foundation, either version 3 of the License, or
1217             (at your option) any later version.
1218              
1219             =head1 SEE ALSO
1220              
1221             L, L, L
1222              
1223             =cut
1224              
1225             # vim: expandtab shiftwidth=4