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 ###########################################################################
28 binmode(STDOUT
, ":utf8");
40 use Getopt
::Long
qw(:config bundling);
41 use Digest
::SHA
qw(sha1_hex);
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
75 my $conf = Config
::Tiny
->read($CONFIGFILE);
76 my $prefs = Config
::Tiny
->read($PREFSFILE);
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
},
91 # Define those not read from config, init with defaults
99 $opts{mainfont
} = "{helvetica} -12 bold" unless $opts{mainfont
};
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',
105 # Workaround since '$longopt|shortopt' is a no-no.
106 'noproxy|X' => sub { $opts{proxy
} = ""; },
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
};
121 select STDERR
; $| = 1; # Go unbuffered
122 select STDOUT
; $| = 1;
126 unless ( $opts{all
} ) { init_gui
(); }
130 ## Subroutines: Connection
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);
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
);
159 process_feed
($url, $response);
161 print STDERR
"\nerror: " .$curl->strerror($rc)." (http/$rc)\n";
167 ## Subroutines: 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();
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
188 fetch_feed
($_) foreach (@queue);
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
};
204 foreach my $rss ( @channels ) {
205 foreach my $item ( @
{$rss->{items
}} ) {
206 push @q, $item->{link};
213 ## Subroutines: Helpers
218 return if $url =~ /^$/;
221 $url = "http://$url" if $url !~ m!^http://!i;
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; }
237 system "$opts{clive} $opts{opts} " . join(' ', @q);
242 my $perl_v = sprintf "%vd", $^V
;
243 my $clipb_v = $opted_mods{Clipboard
} ?
$Clipboard::VERSION
: "-";
245 "clivefeed version $VERSION. Copyright (C) 2008 Toni Gundogdu.
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
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;
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 } );
283 $mw->configure(-menu
=> $mb);
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
);
290 $file->command(-label
=> 'Quit', -underline
=> 0, -command
=> sub {exit});
293 my $edit = $mb->cascade(-label
=> 'Edit', -underline
=> 0, -tearoff
=> 0);
294 $edit->command(-label
=> 'Preferences...',
295 -underline
=> 0, -command
=> \
&on_prefs
);
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);
306 $pwtop = $pwmain->Panedwindow(-orient
=> 'h', -opaqueresize
=> 0);
309 my $lbar = $pwtop->Frame;
311 $lbchann = $lbar->Scrolled('Tree',
312 -scrollbars
=> 'osoe',
314 -selectmode
=> 'extended',
315 -browsecmd
=> \
&on_chann
,
318 )->pack(-side
=> 'top', -expand
=> 1, -fill
=> 'both');
320 foreach my $rss ( @channels ) {
321 my $chann = $rss->{channel
}->{title
};
324 $lbchann->add($chann);
325 $lbchann->itemCreate($chann, 0, -text
=> $chann, -itemtype
=> 'text');
327 foreach my $item ( @
{$rss->{items
}} ) {
328 my $title = $item->{title
};
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);
354 $pwbottom = $pwmain->Panedwindow(-orient
=> 'h', -opaqueresize
=> 0);
356 $lbqueue = $pwbottom->Scrolled('Tree',
357 -scrollbars
=> 'osoe',
359 -selectmode
=> 'extended',
360 -browsecmd
=> \
&on_queue
,
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');
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);
404 set_descr
($lbchann, shift);
408 set_descr
($lbqueue, 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');
431 queue_item
($_) foreach ( $lbchann->infoSelection );
432 $lbqueue->autosetmode;
436 foreach ( $lbchann->infoSelection ) {
437 my ($parent) = split /\./;
439 foreach ( $lbchann->infoChildren($parent) );
441 $lbqueue->autosetmode;
445 foreach ( $lbchann->infoChildren("") ) {
446 my ($parent) = split /\./;
448 foreach ($lbchann->infoChildren($parent) );
450 $lbqueue->autosetmode;
454 $lbqueue->deleteEntry($_)
455 foreach ( $lbqueue->infoSelection );
463 my $dlg = $mw->DialogBox(-title
=> 'About', -buttons
=> ['OK']);
464 my $txt = $dlg->add('Text')->pack;
465 $txt->insert('end', print_version
(1));
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);
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';
498 ($opts{mainfont
}) = @_;
499 $mw->RefontTree(-font
=> $opts{mainfont
});
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);
521 foreach ( $lbqueue->infoChildren('') ) {
522 foreach ( $lbqueue->infoChildren($_) ) {
523 my $item = $lbqueue->infoData($_);
524 push @q, $item->{link};
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;
557 clivefeed - the feed parsing utility for clive
561 clivefeed [option]... [URL]...
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.
575 You may freely specify options after the command-line arguments. For example:
577 clivefeed -a URL --opts=--noextract
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
604 Grab all videos without prompting the GUI.
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).
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.
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.
666 =item ~/.config/clivefeed/config
670 =item ~/.config/clivefeed/prefs
672 GUI preferences (e.g. fonts, window position, sash coords, ...).
678 ## Example config file for clivefeed.
681 path = /usr/local/bin/clive
686 proxy = http://foo:1234
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
704 Written by Toni Gundogdu <legatvs@gmail.com>