Added missing Tk::FontDialog prerequisite (--version, INSTALL).
[clive-utils.git] / clivefeed
blob5c934aa9c6585a22ae1e8cda1a2bffae3ea52453
1 #!/usr/bin/env perl
2 # -*- coding: ascii -*-
3 ###########################################################################
4 # clivefeed, the feed parsing utility for clive
5 # Copyright (C) 2008 Toni Gundogdu.
7 # This file is part of clive-utils.
9 # clivefeed is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
14 # clivefeed is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with clivefeed. If not, see <http://www.gnu.org/licenses/>.
21 ###########################################################################
23 # Keep it simple.
25 use warnings;
26 use strict;
28 binmode(STDOUT, ":utf8");
30 use XML::RSS::LibXML;
31 use WWW::Curl::Easy;
32 use Tk::FontDialog;
33 use Tk::DialogBox;
34 use Config::Tiny;
35 use HTML::Strip;
36 use Tk::Tree;
37 use Tk;
39 # Core modules:
40 use Getopt::Long qw(:config bundling);
41 use Digest::SHA qw(sha1_hex);
42 use File::Spec;
43 use File::Find;
44 use File::Path;
45 use Pod::Usage;
46 use Encode;
47 use Cwd;
49 # Non-essentials
50 my %opted_mods = (Clipboard => 1);
51 eval "use Clipboard"; $opted_mods{Clipboard}=0 if $@;
53 my $VERSION = "2.0beta1";
54 my $CONFIGDIR = $ENV{CLIVEFEED_CONFIGDIR}
55 ? $ENV{CLIVEFEED_CONFIGDIR}
56 : File::Spec->catfile($ENV{HOME}, ".config/clivefeed");
57 my $CONFIGFILE = File::Spec->catfile($CONFIGDIR, "config");
58 my $PREFSFILE = File::Spec->catfile($CONFIGDIR, "prefs");
60 my %opts; # Holds the options
61 my @queue; # Holds the current URL queue
62 my $curl; # Holds the curl handle (reused throught lifespan)
63 my @channels; # Holds parsed channel data
64 my $mw; # Holds the main window handle (GUI)
65 my $pwmain; # Holds the handle to the main paned window
66 my $pwtop; # Holds the handle to the top paned window
67 my $pwbottom; # Holds the handle to the bottom paned window
68 my $lbchann; # Listbox: channels
69 my $lbitems; # Listbox: (channel) items
70 my $lbqueue; # Listbox: queued video items
71 my $txtdescr; # Text: video description
72 my %usersel; # Holds user-selected videos
74 # Parse config
75 my $conf = Config::Tiny->read($CONFIGFILE);
76 my $prefs = Config::Tiny->read($PREFSFILE);
77 %opts = (
78 clive => $conf->{clive}->{path},
79 opts => $conf->{clive}->{opts},
80 agent => $conf->{http}->{agent},
81 proxy => $conf->{http}->{proxy},
83 geometry=> $prefs->{gui}->{geometry},
84 pwmain => $prefs->{gui}->{pwmain},
85 pwtop => $prefs->{gui}->{pwtop},
86 pwbottom=> $prefs->{gui}->{pwbottom},
87 mainfont=> $prefs->{gui}->{mainfont},
90 # Parse cmdline
91 # Define those not read from config, init with defaults
92 $opts{quiet} = 0;
93 $opts{paste} = 0;
94 $opts{all} = 0;
95 $opts{debug} = 0;
96 $opts{help} = 0;
97 $opts{manual} = 0;
98 $opts{version} = 0;
99 $opts{mainfont} = "{helvetica} -12 bold" unless $opts{mainfont};
101 GetOptions(\%opts,
102 'debug|d', 'help|h', 'manual|m', 'version|v', 'all|a',
103 'paste|x', 'quiet|q', 'clive|c=s', 'opts|o=s', 'agent|U=s',
104 'proxy|y=s',
105 # Workaround since '$longopt|shortopt' is a no-no.
106 'noproxy|X' => sub { $opts{proxy} = ""; },
107 ) or pod2usage(1);
109 # Since 'version|v' => \&print_version and exit cannot tango with tk
110 print_version(0) if $opts{version};
111 pod2usage(-exitstatus => 0, -verbose => 1) if $opts{help};
112 pod2usage(-exitstatus => 0, -verbose => 2) if $opts{manual};
114 $opts{clive} = $ENV{CLIVE_PATH} unless $opts{clive};
115 find_clive() unless $opts{clive};
117 #init_gui(); exit;
119 get_queue();
121 select STDERR; $| = 1; # Go unbuffered
122 select STDOUT; $| = 1;
124 process_queue();
126 unless ( $opts{all} ) { init_gui(); }
127 else { grab_all(); }
130 ## Subroutines: Connection
132 sub init_curl {
133 $curl = WWW::Curl::Easy->new;
135 $curl->setopt(CURLOPT_USERAGENT,
136 $opts{agent} ? $opts{agent} : "Mozilla/5.0");
138 $curl->setopt(CURLOPT_PROXY, $opts{proxy}) if defined $opts{proxy};
139 $curl->setopt(CURLOPT_VERBOSE, 1) if $opts{debug};
140 $curl->setopt(CURLOPT_FOLLOWLOCATION, 1);
141 $curl->setopt(CURLOPT_AUTOREFERER, 1);
142 $curl->setopt(CURLOPT_HEADER, 0);
143 $curl->setopt(CURLOPT_NOBODY, 0);
146 sub fetch_feed {
147 my ($url, $response, $rc) = (shift, "", 0);
148 open my $rfh, ">", \$response;
150 print "Fetching $url ..." unless $opts{quiet};
151 $curl->setopt(CURLOPT_URL, $url);
152 $curl->setopt(CURLOPT_ENCODING, "");
153 $curl->setopt(CURLOPT_WRITEDATA, $rfh);
154 $rc = $curl->perform;
155 $rc = $curl->getinfo(CURLINFO_RESPONSE_CODE);
157 if ( $rc == 200 ) {
158 print "done.\n";
159 process_feed($url, $response);
160 } else {
161 print STDERR "\nerror: " .$curl->strerror($rc)." (http/$rc)\n";
163 close $rfh;
167 ## Subroutines: Queue
169 sub get_queue {
170 if ( $opts{paste} ) {
171 print STDERR "error: Clipboard module not found" and exit
172 unless $opted_mods{Clipboard};
173 my $data = Clipboard->paste();
174 if ( $data ) {
175 parse_input($_) foreach split/\n/,$data;
179 parse_input($_) foreach @ARGV;
180 unless ( @queue ) { parse_input($_) while ( <STDIN> ); }
182 my %h = map {$_,1} @queue; # Remove duplicates
183 @queue = keys %h;
186 sub process_queue {
187 init_curl();
188 fetch_feed($_) foreach (@queue);
191 sub process_feed {
192 my ($url, $response) = @_;
193 print "=> Processing feed ..." unless $opts{quiet};
195 my $rss = XML::RSS::LibXML->new;
196 $rss->parse($response);
197 push @channels, $rss;
199 print "done.\n" unless $opts{quiet};
202 sub grab_all {
203 my @q;
204 foreach my $rss ( @channels ) {
205 foreach my $item ( @{$rss->{items}} ) {
206 push @q, $item->{link};
209 run_clive(@q);
213 ## Subroutines: Helpers
215 sub parse_input {
216 my $url = shift;
218 return if $url =~ /^$/;
219 chomp $url;
221 $url = "http://$url" if $url !~ m!^http://!i;
222 push @queue, $url;
225 sub find_clive {
226 print "Trying to locate 'clive' ...";
228 find ( sub { $opts{clive} = $File::Find::name if ( $_ eq 'clive' ) },
229 split /:/, $ENV{PATH} || getcwd);
231 if ( $opts{clive} ) { print "$opts{clive}\n"; }
232 else { print STDERR "error: not found, use --clive=path\n"; exit; }
235 sub run_clive {
236 my (@q) = @_;
237 system "$opts{clive} $opts{opts} " . join(' ', @q);
240 sub print_version {
241 my $noexit = shift;
242 my $perl_v = sprintf "%vd", $^V;
243 my $clipb_v = $opted_mods{Clipboard} ? $Clipboard::VERSION : "-";
244 my $s = sprintf
245 "clivefeed version $VERSION. Copyright (C) 2008 Toni Gundogdu.
247 Perl: $perl_v ($^O)
248 Modules:
249 * Config::Tiny/$Config::Tiny::VERSION\t\t* WWW::Curl/$WWW::Curl::VERSION
250 * XML::RSS::LibXML/$XML::RSS::LibXML::VERSION\t* Clipboard/$clipb_v
251 * Tk/$Tk::VERSION\t\t\t* Tk::Tree/$Tk::Tree::VERSION
252 * Tk::DialogBox/$Tk::DialogBox::VERSION\t\t* HTML::Strip/$HTML::Strip::VERSION
253 * Tk::FontDialog/$Tk::FontDialog::VERSION
254 Core modules:
255 * Getopt::Long/$Getopt::Long::VERSION\t\t* Digest::SHA/$Digest::SHA::VERSION
256 * File::Spec/$File::Spec::VERSION\t\t* File::Find/$File::Find::VERSION
257 * File::Path/$File::Path::VERSION\t\t* Encode/$Encode::VERSION
258 * Pod::Usage/$Pod::Usage::VERSION\t\t* Cwd/$Cwd::VERSION
260 This program comes with ABSOLUTELY NO WARRANTY. You may redistribute copies of
261 clivefeed under the terms of the GNU General Public License as published by the
262 Free Software Foundation, either version 3 of the License, or (at your option)
263 any later version. You should have received a copy of the General Public License
264 along with this program. If not, see http://www.gnu.org/licenses/.
266 return $s if $noexit;
267 print $s; exit;
271 # GUI:
273 sub init_gui {
274 return unless @channels;
276 $mw = MainWindow->new;
277 $mw->geometry($opts{geometry}) if defined $opts{geometry};
278 $mw->title('clivefeed');
279 $mw->protocol('WM_DELETE_WINDOW', sub { save_prefs(); $mw->destroy } );
281 # Menubar
282 my $mb = $mw->Menu;
283 $mw->configure(-menu => $mb);
285 # Menu: File
286 my $file = $mb->cascade(-label => 'File', -underline => 0, -tearoff => 0);
287 $file->command(-label => 'Extract videos in queue...',
288 -underline => 0, -command => \&on_extract);
289 $file->separator;
290 $file->command(-label => 'Quit', -underline => 0, -command => sub {exit});
292 # Menu: Edit
293 my $edit = $mb->cascade(-label => 'Edit', -underline => 0, -tearoff => 0);
294 $edit->command(-label => 'Preferences...',
295 -underline => 0, -command => \&on_prefs);
297 # Menu: Help
298 my $help = $mb->cascade(-label => 'Help', -underline => 0, -tearoff => 0);
299 $help->command(-label => 'About...',
300 -underline => 0, -command => \&on_about);
302 # The GUI has an upper and a lower part
303 $pwmain = $mw->Panedwindow(-orient => 'v', -opaqueresize => 0);
305 # Upper part
306 $pwtop = $pwmain->Panedwindow(-orient => 'h', -opaqueresize => 0);
308 # Upper: Channels
309 my $lbar = $pwtop->Frame;
311 $lbchann = $lbar->Scrolled('Tree',
312 -scrollbars => 'osoe',
313 -itemtype => 'text',
314 -selectmode => 'extended',
315 -browsecmd => \&on_chann,
316 -indicator => 1,
317 -drawbranch => 1,
318 )->pack(-side => 'top', -expand => 1, -fill => 'both');
320 foreach my $rss ( @channels ) {
321 my $chann = $rss->{channel}->{title};
322 $chann =~ tr{.}{}d;
324 $lbchann->add($chann);
325 $lbchann->itemCreate($chann, 0, -text => $chann, -itemtype => 'text');
327 foreach my $item ( @{$rss->{items}} ) {
328 my $title = $item->{title};
329 $title =~ tr{.}{}d;
330 my $path = "$chann.$title";
332 $lbchann->add($path, -data => $item);
333 $lbchann->itemCreate($path, 0,
334 -text => $item->{title}, -itemtype => 'text');
337 $lbchann->autosetmode;
338 $lbchann->close($_) foreach ( $lbchann->infoChildren('') );
340 $lbar->Button(-text => 'Grab video', -command => \&on_grab
341 )->pack(-fill => 'x', -side => 'left');
342 $lbar->Button(-text => 'Grab channel', -command => \&on_grab_chann
343 )->pack(-fill => 'x', -side => 'left');
344 $lbar->Button(-text => 'Grab everything', -command => \&on_grab_all
345 )->pack(-fill => 'x', -side => 'left');
347 my $rbar = $pwtop->Frame;
348 $txtdescr = $rbar->Scrolled('Text', -scrollbars => 'osoe',
349 )->pack(-fill => 'both', -expand => 1);
351 $pwtop->add($lbar, $rbar, -width => $opts{pwtop} ? $opts{pwtop}:200);
353 # Lower part
354 $pwbottom = $pwmain->Panedwindow(-orient => 'h', -opaqueresize => 0);
356 $lbqueue = $pwbottom->Scrolled('Tree',
357 -scrollbars => 'osoe',
358 -itemtype => 'text',
359 -selectmode => 'extended',
360 -browsecmd => \&on_queue,
361 -indicator => 1,
362 -drawbranch => 1,
365 my $bar = $pwbottom->Frame; # Button toorbar
366 $bar->Button(-text => 'Remove', -command=> \&on_remove,
367 )->pack(-fill => 'x');
369 $bar->Button(-text => 'Clear', -command=> \&on_clear,
370 )->pack(-fill => 'x');
372 $bar->Button(-text => 'Extract videos...', -command=> \&on_extract,
373 )->pack(-fill => 'x', -side => 'bottom');
375 $pwbottom->add($lbqueue, $bar,
376 -width => $opts{pwbottom} ? $opts{pwbottom} : 200);
378 # Add upper and lower parts to main paned window
379 $pwmain->add($pwtop, $pwbottom,
380 -height => $opts{pwmain} ? $opts{pwmain} : 200);
382 $mw->RefontTree(-font => $opts{mainfont});
383 $pwmain->pack(-expand => 1, -fill => 'both');
385 MainLoop;
388 sub set_descr {
389 my ($lb, $path) = @_;
391 $txtdescr->delete('1.0', 'end');
393 my $item = $lb->infoData($path);
394 return unless defined $item;
396 my $strip = HTML::Strip->new;
397 my $descr = $strip->parse($item->{description});
398 $descr =~ s/^\s+|\s+$//g;
400 $txtdescr->insert('end', $descr);
403 sub on_chann {
404 set_descr($lbchann, shift);
407 sub on_queue {
408 set_descr($lbqueue, shift);
411 sub queue_item {
412 my $path = shift;
413 return if $path !~ /\./;
414 return if $lbqueue->infoExists($path);
416 my $item = $lbchann->infoData($path);
417 my ($chann) = split /\./, $path;
419 unless ( $lbqueue->infoExists($chann) ) {
420 $lbqueue->add($chann);
421 $lbqueue->itemCreate($chann, 0,
422 -text => $chann, -itemtype => 'text');
425 $lbqueue->add($path, -data => $item);
426 $lbqueue->itemCreate($path, 0,
427 -text => $item->{title}, -itemtype => 'text');
430 sub on_grab {
431 queue_item($_) foreach ( $lbchann->infoSelection );
432 $lbqueue->autosetmode;
435 sub on_grab_chann {
436 foreach ( $lbchann->infoSelection ) {
437 my ($parent) = split /\./;
438 queue_item($_)
439 foreach ( $lbchann->infoChildren($parent) );
441 $lbqueue->autosetmode;
444 sub on_grab_all {
445 foreach ( $lbchann->infoChildren("") ) {
446 my ($parent) = split /\./;
447 queue_item($_)
448 foreach ($lbchann->infoChildren($parent) );
450 $lbqueue->autosetmode;
453 sub on_remove {
454 $lbqueue->deleteEntry($_)
455 foreach ( $lbqueue->infoSelection );
458 sub on_clear {
459 $lbqueue->deleteAll;
462 sub on_about {
463 my $dlg = $mw->DialogBox(-title => 'About', -buttons => ['OK']);
464 my $txt = $dlg->add('Text')->pack;
465 $txt->insert('end', print_version(1));
466 $dlg->Show;
469 sub change_font {
470 my ($top, $lblv, $lbl) = @_;
471 my $font = $top->FontDialog(-initfont => $$lblv)->Show;
473 if ( defined $font ) {
474 my $descr = $top->FontDialog->GetDescriptiveFontName($font);
475 $lbl->configure(-font => $descr);
476 $$lblv = $descr;
480 sub on_prefs {
481 my $dlg = $mw->DialogBox(-title => 'clivefeed preferences',
482 -buttons => ['OK','Cancel']);
484 $dlg->add('Label', -text => 'Fonts: press to choose'
485 )->grid(-sticky => 'w', -pady => 10);
487 my ($mainfont) = ($opts{mainfont});
488 my $mainfontl = $dlg->Label(-textvariable => \$mainfont);
490 $dlg->add('Button', -text => 'Main font',
491 -command => sub { change_font_descr($dlg, \$mainfont, $mainfontl) }
492 )->grid($mainfontl, -sticky => 'w', -padx => '5');
494 on_prefs_ok($mainfont) if $dlg->Show eq 'OK';
497 sub on_prefs_ok {
498 ($opts{mainfont}) = @_;
499 $mw->RefontTree(-font => $opts{mainfont});
500 save_prefs();
503 sub save_prefs {
504 mkpath( [$CONFIGDIR], 1, 0700);
506 my $c = Config::Tiny->new;
507 $c->{gui}->{geometry} = $mw->geometry();
509 # FIXME: +7 is added to the coords even if the sashes have not been
510 # dragged. Unsure why. The increase is probably system specific.
511 $c->{gui}->{pwmain} = ($pwmain->sashCoord(0))[1]-7;
512 $c->{gui}->{pwtop} = ($pwtop->sashCoord(0))[0]-7;
513 $c->{gui}->{pwbottom} = ($pwbottom->sashCoord(0))[0]-7;
514 $c->{gui}->{mainfont} = $opts{mainfont};
516 $c->write($PREFSFILE);
519 sub on_extract {
520 my @q;
521 foreach ( $lbqueue->infoChildren('') ) {
522 foreach ( $lbqueue->infoChildren($_) ) {
523 my $item = $lbqueue->infoData($_);
524 push @q, $item->{link};
527 return unless @q;
529 # Prompt for clive(1) options
530 my $dlg = $mw->DialogBox(-title => 'clive(1) options',
531 -buttons => ['OK','Cancel']);
533 $dlg->add('Label', -text => 'Path to clive'
534 )->grid(my $clivepath = $dlg->Entry(-width => 25),
535 -sticky => 'w', -padx => '5');
537 $dlg->add('Label', -text => 'Runtime options'
538 )->grid(my $cliveopts = $dlg->Entry(-width => 25),
539 -sticky => 'w', -padx => '5');
541 $clivepath->insert('end', $opts{clive});
542 $cliveopts->insert('end', $opts{opts});
544 if ( $dlg->Show() eq 'OK' ) {
545 $opts{clive} = $clivepath->get;
546 $opts{opts} = $cliveopts->get;
547 $mw->destroy;
548 run_clive(@q);
553 __END__
555 =head1 NAME
557 clivefeed - the feed parsing utility for clive
559 =head1 SYNOPSIS
561 clivefeed [option]... [URL]...
563 =head1 DESCRIPTION
565 clivefeed is an utility that parses RSS feeds containing video page links and
566 uses L<clive(1)> to extract them.
568 Historically, the feed parsing feature was written in Python/Newt and it was
569 part of the clive 1.x project. The clivefeed utility was written in Perl/Tk to
570 replace the feature that was removed in clive 2.0. The clivefeed utility is
571 part of the B<clive-utils> project.
573 =head1 OPTIONS
575 You may freely specify options after the command-line arguments. For example:
577 clivefeed -a URL --opts=--noextract
579 B<Basic Options>
581 =over 4
583 =item B<-h --help>
585 Show help and exit.
587 =item B<--version>
589 Show version and exit.
591 =item B<--clive=>I<path>
593 I<path> to L<clive(1)> command. If unspecified, clivefeed will attempt to
594 locate it in the $PATH. Additionally, the B<CLIVE_PATH> environment variable
595 can be used. See also L</CONFIG>.
597 =item B<--opts=>I<opts>
599 I<opts> to append to clive call. See L<clive(1)> for more on the available
600 options.
602 =item B<-a --all>
604 Grab all videos without prompting the GUI.
606 =back
608 B<HTTP Options>
610 =over 4
612 =item B<-U --agent=>I<string>
614 Identify as I<string> to the HTTP server. Defaults to "Mozilla/5.0".
616 =item B<-y --proxy=>I<address>
618 Use I<address> for HTTP proxy, e.g. http://foo:1234. If http_proxy
619 environment variable is defined, it will be used.
621 =item B<-X --noproxy>
623 Do not use the defined HTTP proxy (B<--proxy>, config or http_proxy).
625 =back
627 =head1 EXAMPLES
629 =over 4
631 =item clivefeed "http://youtube.com/rss/user/communitychannel/videos.rss"
633 Parses the feed at the specified URL.
635 =item cat E<gt>E<gt> url.lst
637 http://youtube.com/rss/user/googletechtalks/videos.rss
638 http://youtube.com/rss/user/theonion/videos.rss
639 http://youtube.com/rss/user/lisanova/videos.rss
640 http://youtube.com/rss/user/clipcritics/videos.rss
641 http://youtube.com/rss/user/communitychannel/videos.rss
642 http://youtube.com/rss/user/manintheboxshow/videos.rss
644 =item cat url.lst | clivefeed
646 Reads input from UNIX pipe.
648 =item clivefeed --opts="-f mp4"
650 Append the I<opts> to the L<clive(1)> call.
652 =item clivefeed --all URL
654 Grabs all found videos without prompting.
656 =back
658 =head1 FILES
660 By default, clivefeed searches the ~/.config/clivefeed directory for the
661 config file. The B<CLIVEFEED_CONFIGDIR> environment variable can be used
662 to override this behaviour.
664 =over 4
666 =item ~/.config/clivefeed/config
668 Configuration file.
670 =item ~/.config/clivefeed/prefs
672 GUI preferences (e.g. fonts, window position, sash coords, ...).
674 =back
676 =head1 CONFIG
678 ## Example config file for clivefeed.
680 [clive]
681 path = /usr/local/bin/clive
682 opts = -f mp4
684 [http]
685 agent = Mozilla/5.0
686 proxy = http://foo:1234
688 =head1 SEE ALSO
690 L<clive(1)>
692 =head1 OTHER
694 Project: http://googlecode.com/p/clive-utils/
696 A clive-utils development repository can be obtained from:
698 git clone git://repo.or.cz/clive-utils.git
700 Patches welcome.
702 =head1 AUTHOR
704 Written by Toni Gundogdu <legatvs@gmail.com>
706 =cut