File Coverage

blib/lib/App/WatchLater.pm
Criterion Covered Total %
statement 26 77 33.7
branch 0 28 0.0
condition 0 6 0.0
subroutine 9 21 42.8
pod 1 1 100.0
total 36 133 27.0


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