File Coverage

blib/lib/App/WatchLater.pm
Criterion Covered Total %
statement 29 67 43.2
branch 0 20 0.0
condition 0 3 0.0
subroutine 10 20 50.0
pod 1 1 100.0
total 40 111 36.0


line stmt bran cond sub pod time code
1             package App::WatchLater;
2              
3 1     1   54153 use 5.016;
  1         3  
4 1     1   5 use strict;
  1         2  
  1         17  
5 1     1   4 use warnings;
  1         1  
  1         30  
6              
7 1     1   5 use Carp;
  1         1  
  1         53  
8 1     1   1256 use DBI;
  1         14749  
  1         58  
9 1     1   671 use Getopt::Long qw(:config auto_help gnu_getopt);
  1         10252  
  1         5  
10 1     1   577 use Pod::Usage;
  1         40031  
  1         118  
11 1     1   466 use Try::Tiny;
  1         1672  
  1         47  
12              
13 1     1   369 use App::WatchLater::YouTube;
  1         2  
  1         51  
14 1     1   350 use App::WatchLater::Browser;
  1         2  
  1         626  
15              
16             =head1 NAME
17              
18             App::WatchLater - Manage your YouTube Watch Later videos
19              
20             =head1 VERSION
21              
22             Version 0.03
23              
24             =cut
25              
26             our $VERSION = '0.03';
27              
28              
29             =head1 SYNOPSIS
30              
31             exit App::WatchLater::main();
32              
33             =head1 DESCRIPTION
34              
35             Manages a Watch Later queue of YouTube videos, in case you're one of the kinds
36             of people whose Watch Later lists get too out of hand. Google has deprecated the
37             ability to access the B playlist via the YouTube Data API, which means we
38             have to go to a bit more effort.
39              
40             An API key is required to access the YouTube Data API. Alternatively, requests
41             may be authorized by providing an OAuth2 access token.
42              
43             =head1 SUBROUTINES/METHODS
44              
45             =cut
46              
47             sub _ensure_schema {
48 0     0     my $dbh = shift;
49 0 0         $dbh->do(<<'SQL') or die $dbh->errstr;
50             CREATE TABLE IF NOT EXISTS videos(
51             video_id TEXT PRIMARY KEY,
52             video_title TEXT,
53             channel_id TEXT,
54             channel_title TEXT,
55             watched INTEGER NOT NULL DEFAULT 0
56             );
57             SQL
58             }
59              
60             sub _add {
61 0     0     my ($dbh, $api, $opts, @video_ids) = @_;
62              
63 0 0         my $on_conflict = $opts->{force} ? 'REPLACE' : 'IGNORE';
64              
65 0           my $sth = $dbh->prepare_cached(<
66             INSERT OR $on_conflict INTO videos
67             (video_id, video_title, channel_id, channel_title, watched)
68             VALUES (?, ?, ?, ?, 0);
69             SQL
70              
71 0           for my $vid (@video_ids) {
72             try {
73 0     0     my $snippet = $api->get_video($vid);
74             $sth->execute($vid, $snippet->{title},
75 0           $snippet->{channelId}, $snippet->{channelTitle});
76             } catch {
77             # warn the user and continue
78 0     0     print STDERR;
79 0           };
80             }
81             }
82              
83             sub _get_random_video {
84 0     0     my ($dbh) = @_;
85 0           my $sth = $dbh->prepare_cached(<<'SQL');
86             SELECT video_id, video_title, channel_id, channel_title FROM videos
87             WHERE NOT watched
88             ORDER BY RANDOM()
89             LIMIT 1;
90             SQL
91 0 0         $sth->execute or die $sth->errstr;
92 0 0         my $row = $sth->fetchrow_hashref or croak 'no videos';
93 0           $row->{video_id};
94             }
95              
96             sub _mark_watched {
97 0     0     my ($dbh, $vid) = @_;
98 0           my $sth = $dbh->prepare(<<'SQL');
99             UPDATE videos SET watched=1
100             WHERE video_id = ?;
101             SQL
102 0 0         $sth->execute($vid) or die $sth->errstr;
103             }
104              
105             sub _watch {
106 0     0     my ($dbh, $api, $opts, @video_ids) = @_;
107              
108 0 0         if (!@video_ids) {
109 0           push @video_ids, _get_random_video($dbh);
110             }
111              
112 0           for my $vid (@video_ids) {
113             try {
114 0 0   0     open_url("https://youtu.be/$vid") if $opts->{open};
115 0           _mark_watched($dbh, $vid);
116             } catch {
117             # warn the user and continue
118 0     0     print STDERR;
119 0           };
120             }
121             }
122              
123             =head2 main
124              
125             main();
126              
127             C runs the watch-later command line interface. It reads arguments
128             directly from C<@ARGV> using L.
129              
130             =cut
131              
132             # TODO a better module interface to main()
133             sub main {
134 0     0 1   my %opts = (
135             'db-path' => "$ENV{HOME}/.watch-later.db",
136             force => 0,
137             open => 1,
138             );
139              
140 0 0         GetOptions(
141             \%opts,
142             'db-path|d=s',
143             'add|a',
144             'watch|w',
145             'force|f!',
146             'open|o!',
147             ) or pod2usage(2);
148              
149 0 0 0       croak "Add and Watch modes both specified" if $opts{add} && $opts{watch};
150              
151 0           my @video_ids = map { find_video_id($_) } @ARGV;
  0            
152              
153 0           my $dbpath = $opts{'db-path'};
154 0           my $dbh = DBI->connect("dbi:SQLite:dbname=$dbpath");
155 0           _ensure_schema($dbh);
156              
157             my $api = App::WatchLater::YouTube->new(
158             api_key => $ENV{YT_API_KEY},
159             access_token => $ENV{YT_ACCESS_TOKEN},
160 0           );
161              
162 0 0         if ($opts{watch}) {
163 0           _watch($dbh, $api, \%opts, @video_ids);
164             } else {
165 0           _add($dbh, $api, \%opts, @video_ids);
166             }
167             }
168              
169             =head1 AUTHOR
170              
171             Aaron L. Zeng, C<< >>
172              
173             =head1 BUGS
174              
175             Please report any bugs or feature requests to C, or through
176             the web interface at L. I will be notified, and then you'll
177             automatically be notified of progress on your bug as I make changes.
178              
179              
180              
181              
182             =head1 SUPPORT
183              
184             You can find documentation for this module with the perldoc command.
185              
186             perldoc App::WatchLater
187              
188              
189             You can also look for information at:
190              
191             =over 4
192              
193             =item * RT: CPAN's request tracker (report bugs here)
194              
195             L
196              
197             =item * AnnoCPAN: Annotated CPAN documentation
198              
199             L
200              
201             =item * CPAN Ratings
202              
203             L
204              
205             =item * Search CPAN
206              
207             L
208              
209             =back
210              
211              
212             =head1 ACKNOWLEDGEMENTS
213              
214              
215             =head1 LICENSE AND COPYRIGHT
216              
217             Copyright 2017 Aaron L. Zeng.
218              
219             This program is distributed under the MIT (X11) License:
220             L
221              
222             Permission is hereby granted, free of charge, to any person
223             obtaining a copy of this software and associated documentation
224             files (the "Software"), to deal in the Software without
225             restriction, including without limitation the rights to use,
226             copy, modify, merge, publish, distribute, sublicense, and/or sell
227             copies of the Software, and to permit persons to whom the
228             Software is furnished to do so, subject to the following
229             conditions:
230              
231             The above copyright notice and this permission notice shall be
232             included in all copies or substantial portions of the Software.
233              
234             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
235             EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
236             OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
237             NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
238             HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
239             WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
240             FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
241             OTHER DEALINGS IN THE SOFTWARE.
242              
243              
244             =cut
245              
246             1; # End of App::WatchLater