#!/usr/bin/perl # # get_iplayer # # Lists and downloads BBC iPlayer audio and video streams # # Author: Phil Lewis # Email: iplayer (at sign) linuxcentre.net # Web: http://linuxcentre.net/iplayer # License: GPLv3 (see LICENSE.txt) # my $version = 1.01; # # Help: # ./get_iplayer --help # # Changelog: # http://linuxcentre.net/get_iplayer/CHANGELOG.txt # # Example Usage and Documentation: # http://linuxcentre.net/getiplayer/documentation # # Todo: # * Use non-shell tee? # * Fix non-uk detection - iphone auth? # * Index/Download live radio streams w/schedule feeds to assist timing # * Podcasts for 'local' stations are missing (only a handful). They use a number of different station ids which will involve reading html to determine rss feed. # * Remove all rtsp/mplayer/lame/tee dross when realaudio streams become obselete (not quite yet) # # Known Issues: # * In ActivePerl/windows downloaded iPhone video files do not get renamed (remain with .partial.mov) # * vlc does not quit after downloading an rtsp N95 video stream (ctrl-c is required) - need a --play-and-quit option if such a thing exists # use Env qw[@PATH]; use Fcntl; use File::Copy; use File::Path; use File::stat; use Getopt::Long; use HTML::Entities; use HTTP::Cookies; use HTTP::Headers; use IO::Seekable; use IO::Socket; use LWP::ConnCache; #use LWP::Debug qw(+); use LWP::UserAgent; use POSIX qw(mkfifo); use strict; #use warnings; use Time::Local; use URI; $|=1; my %opt = (); my %opt_cmdline = (); # a hash of which options came from the cmdline rather than the options files my %opt_file = (); # a hash of which options came from the options files rather than the cmdline # Print to STDERR if not quiet unless verbose or debug sub logger(@) { # Make sure quiet can be overridden by verbose and debug options print STDERR $_[0] if (! $opt{quiet}) || $opt{verbose} || $opt{debug}; } sub usage { logger <] [ ...] Download files: get_iplayer --get [] ... get_iplayer --pid [] Stream Downloads: get_iplayer --stdout [] | mplayer -cache 2048 - Update get_iplayer: get_iplayer --update Search Options: Search programme names based on given pattern -l, --long Additionally search in long programme descriptions / episode names --channel Narrow search to matched channel(s) --category Narrow search to matched categories --versions Narrow search to matched programme version(s) --exclude-channel Narrow search to exclude matched channel(s) --exclude-category Narrow search to exclude matched catogories --type Only search in these types of programmes (tv is default) --since Limit search to programmes added to the cache in the last N hours Display Options: -l, --long Display long programme descriptions / episode names and other data --terse Only show terse programme info (does not affect searching) --tree Display Programme listings in a tree view -i, --info Show full programme metadata (only if number of matches < 50) --list Show a list of available categories/channels for the selected type and exit --hide Hide previously downloaded programmes Download Options: -g, --get Download matching programmes -x, --stdout Additionally stream to STDOUT (so you can pipe output to a player) -p, --proxy Web proxy URL spec --partial-proxy Works around for some broken web proxies (try this extra option if your proxy fails) --pid Download an arbitrary pid that does not appear in the index --force-download Ignore download history (unsets --hide option also) --realaudio Use the RealAudio radio stream and not the MP3 stream --mp3audio Use the MP3 radio stream for radio and dont fallback to the RealAudio stream --wav In radio realaudio mode output as wav and don't transcode to mp3 --raw In radio/realaudio or iPhone/video mode don't transcode or change the downloaded stream in any way --n95 In TV mode download/stream low quality Nokia N95 H.264 stream (alpha) --bandwidth In radio realaudio mode specify the link bandwidth in bps for rtsp streaming (default 512000) --subtitles In TV mode, download subtitles into srt/SubRip format if available --suboffset Offset the subtitle timestamps by the specified number of milliseconds --version-list Override the version of programme to download (e.g. '--version-list signed,default') -t, --test Test only - no download (will show programme type) PVR Options: --pvr Runs the PVR download using all saved PVR searches (intended to be run every hour from cron etc) --pvradd Add the current search terms to the named PVR search --pvrdel Remove the named search from the PVR searches --pvr-enable Enable a previously disabled named PVR search --pvr-disable Disable (not delete) a named PVR search --pvrlist Show the PVR search list Output Options: -o, --output Default Download output directory for all downloads --outputradio Download output directory for radio --outputtv Download output directory for tv --outputpodcast Download output directory for podcasts --file-prefix The filename prefix (excluding dir and extension) using formatting fields. e.g. '--' -s, --subdir Downloaded files into Programme name subdirectory -n, --nowrite No writing of file to disk (use with -x to prevent a copy being stored on disk) -w, --whitespace Keep whitespace (and escape chars) in filenames -q, --quiet No logging output -c, --command Run user command after successful download using args such as , etc Config Options: -f, --flush, --refresh Refresh cache -e, --expiry Cache expiry in seconds (default 4hrs) --symlink Create symlink to once we have the header of the download --fxd Create Freevo FXD XML in specified file --mythtv Create Mythtv streams XML in specified file --xml-channels Create freevo/Mythtv menu of channels -> programme names -> episodes --xml-names Create freevo/Mythtv menu of programme names -> episodes --xml-alpha Create freevo/Mythtv menu sorted alphabetically by programme name --html Create basic HTML index of programmes in specified file --mplayer Location of mplayer binary --lame Location of lame binary --id3v2 Location of id3v2 binary --vlc Location of vlc binary --streaminfo Returns all of the media stream urls of the programme(s) -v, --verbose Verbose -u, --update Update get_iplayer if a newer one exists -h, --help Help --save Save specified options as default in .get_iplayer/config EOF exit 1; } # Get cmdline params my $save; # This is where all profile data/caches/cookies etc goes my $profile_dir; # This is where system-wide default options are specified my $optfile_system; # Options on unix-like systems if ( defined $ENV{HOME} ) { $profile_dir = $ENV{HOME}.'/.get_iplayer'; $optfile_system = '/etc/get_iplayer/options'; # Otherwise look for windows style file locations } elsif ( defined $ENV{USERPROFILE} ) { $profile_dir = $ENV{USERPROFILE}.'/.get_iplayer'; $optfile_system = $ENV{ALLUSERSPROFILE}.'/get_iplayer/options'; } # Make profile dir if it doesnt exist mkpath $profile_dir if ! -d $profile_dir; # Personal options go here my $optfile = "${profile_dir}/options"; # PVR Lockfile location my $lockfile; # Parse options if we're not saving options (system-wide options are overridden by personal options) if ( ! grep /\-\-save/, @ARGV ) { $opt{debug} = 1 if grep /\-\-debug/, @ARGV; read_options_file($optfile_system); read_options_file($optfile); } # Allow bundling of single char options Getopt::Long::Configure ("bundling"); # cmdline opts take precedence GetOptions( "bandwidth=n" => \$opt_cmdline{bandwidth}, "category=s" => \$opt_cmdline{category}, "channel=s" => \$opt_cmdline{channel}, "c|command=s" => \$opt_cmdline{command}, "debug" => \$opt_cmdline{debug}, "exclude-category=s" => \$opt_cmdline{excludecategory}, "exclude-channel=s" => \$opt_cmdline{excludechannel}, "expiry|e=n" => \$opt_cmdline{expiry}, "file-prefix|fileprefix=s" => \$opt_cmdline{fileprefix}, "flush|refresh|f" => \$opt_cmdline{flush}, "force-download" => \$opt_cmdline{forcedownload}, "fxd=s" => \$opt_cmdline{fxd}, "get|g" => \$opt_cmdline{get}, "help|h" => \$opt_cmdline{help}, "hide" => \$opt_cmdline{hide}, "html=s" => \$opt_cmdline{html}, "id3v2=s" => \$opt_cmdline{id3v2}, "i|info" => \$opt_cmdline{info}, "lame=s" => \$opt_cmdline{lame}, "list=s" => \$opt_cmdline{list}, "long|l" => \$opt_cmdline{long}, "mp3audio" => \$opt_cmdline{mp3audio}, "mplayer=s" => \$opt_cmdline{mplayer}, "mythtv=s" => \$opt_cmdline{mythtv}, "n95" => \$opt_cmdline{n95}, "no-write|nowrite|n" => \$opt_cmdline{nowrite}, "output|o=s" => \$opt_cmdline{output}, "outputpodcast=s" => \$opt_cmdline{outputpodcast}, "outputradio=s" => \$opt_cmdline{outputradio}, "outputtv=s" => \$opt_cmdline{outputtv}, "partial-proxy" => \$opt_cmdline{partialproxy}, "pid=s" => \$opt_cmdline{pid}, "proxy|p=s" => \$opt_cmdline{proxy}, "pvr" => \$opt_cmdline{pvr}, "pvradd|pvr-add=s" => \$opt_cmdline{pvradd}, "pvrdel|pvr-del=s" => \$opt_cmdline{pvrdel}, "pvrdisable|pvr-disable=s" => \$opt_cmdline{pvrdisable}, "pvrenable|pvr-enable=s" => \$opt_cmdline{pvrenable}, "pvrlist|pvr-list" => \$opt_cmdline{pvrlist}, "q|quiet" => \$opt_cmdline{quiet}, "raw" => \$opt_cmdline{raw}, "realaudio" => \$opt_cmdline{realaudio}, "save" => \$save, "since=n" => \$opt_cmdline{since}, "stdout|stream|x" => \$opt_cmdline{stdout}, "streaminfo" => \$opt_cmdline{streaminfo}, "subdirs|subdir|s" => \$opt_cmdline{subdir}, "suboffset=n" => \$opt_cmdline{suboffset}, "subtitles" => \$opt_cmdline{subtitles}, "symlink|freevo=s" => \$opt_cmdline{symlink}, "test|t" => \$opt_cmdline{test}, "terse" => \$opt_cmdline{terse}, "tree" => \$opt_cmdline{tree}, "type=s" => \$opt_cmdline{type}, "update|u" => \$opt_cmdline{update}, "versionlist|version-list=s" => \$opt_cmdline{versionlist}, "versions=s" => \$opt_cmdline{versions}, "verbose|v" => \$opt_cmdline{verbose}, "vlc=s" => \$opt_cmdline{vlc}, "wav" => \$opt_cmdline{wav}, "whitespace|ws|w" => \$opt_cmdline{whitespace}, "xml-channels|fxd-channels" => \$opt_cmdline{xmlchannels}, "xml-names|fxd-names" => \$opt_cmdline{xmlnames}, "xml-alpha|fxd-alpha" => \$opt_cmdline{xmlalpha}, ) || die usage(); usage() if $opt_cmdline{help}; # Merge cmdline options into %opt for ( keys %opt_cmdline ) { $opt{$_} = $opt_cmdline{$_} if defined $opt_cmdline{$_}; } # Save opts if specified save_options_file( $optfile ) if $save; # Global vars # Programme data structure # $prog{$pid} = { # 'index' => , # 'name' => , # 'episode' => , # 'desc' => , # 'available' => , # 'duration' => # 'versions' => # 'thumbnail' => # 'channel => # 'categories' => # 'type' => # 'timeadded' => # 'longname' => , # 'version' => # 'filename' => # 'dir' => # 'fileprefix' => # 'ext' => #}; my %prog; my %pids_history; my %index_pid; # Hash to obtain pid given an index my $now; my $childpid; # Static URLs my $channel_feed_url = 'http://feeds.bbc.co.uk/iplayer'; # /$channel/list/limit/400 my $prog_feed_url = 'http://feeds.bbc.co.uk/iplayer/episode/'; # $pid my $prog_iplayer_metadata = 'http://www.bbc.co.uk/iplayer/playlist/'; # $pid my $media_stream_data_prefix = 'http://www.bbc.co.uk/mediaselector/4/mtis/stream/'; # $verpid my $iphone_download_prefix = 'http://www.bbc.co.uk/mediaselector/3/auth/iplayer_streaming_http_mp4'; my $prog_page_prefix = 'http://www.bbc.co.uk/programmes'; my $thumbnail_prefix = 'http://www.bbc.co.uk/iplayer/images/episode'; my $metadata_xml_prefix = 'http://www.bbc.co.uk/iplayer/metafiles/episode'; # /${pid}.xml my $metadata_mobile_prefix = 'http://www.bbc.co.uk/iplayer/widget/episodedetail/episode'; # /${pid}/template/mobile/service_type/tv/ my $podcast_index_feed_url = 'http://downloads.bbc.co.uk/podcasts/ppg.xml'; my $version_url = 'http://linuxcentre.net/get_iplayer/VERSION-get_iplayer'; my $update_url = 'http://linuxcentre.net/get_iplayer/get_iplayer'; # Static hash definitions my %channels; $channels{tv} = { 'bbc_one' => 'tv|BBC One', 'bbc_two' => 'tv|BBC Two', 'bbc_three' => 'tv|BBC Three', 'bbc_four' => 'tv|BBC Four', 'cbbc' => 'tv|CBBC', 'cbeebies' => 'tv|CBeebies', 'bbc_news24' => 'tv|BBC News 24', 'bbc_parliament' => 'tv|BBC Parliament', 'bbc_one_northern_ireland' => 'tv|BBC One Northern Ireland', 'bbc_one_scotland' => 'tv|BBC One Scotland', 'bbc_one_wales' => 'tv|BBC One Wales', 'bbc_webonly' => 'tv|BBC Web Only', 'bbc_hd' => 'tv|BBC HD', 'bbc_alba' => 'tv|BBC Alba', 'categories/news/tv' => 'tv|BBC News', 'categories/sport/tv' => 'tv|BBC Sport', # 'categories/tv' => 'tv|All', 'categories/signed' => 'tv|Signed', }; $channels{radio} = { 'bbc_1xtra' => 'radio|BBC 1Xtra', 'bbc_radio_one' => 'radio|BBC Radio 1', 'bbc_radio_two' => 'radio|BBC Radio 2', 'bbc_radio_three' => 'radio|BBC Radio 3', 'bbc_radio_four' => 'radio|BBC Radio 4', 'bbc_radio_five_live' => 'radio|BBC Radio 5 live', 'bbc_radio_five_live_sports_extra' => 'radio|BBC 5 live Sports Extra', 'bbc_6music' => 'radio|BBC 6 Music', 'bbc_7' => 'radio|BBC 7', 'bbc_asian_network' => 'radio|BBC Asian Network', 'bbc_radio_foyle' => 'radio|BBC Radio Foyle', 'bbc_radio_scotland' => 'radio|BBC Radio Scotland', 'bbc_radio_nan_gaidheal' => 'radio|BBC Radio Nan Gaidheal', 'bbc_radio_ulster' => 'radio|BBC Radio Ulster', 'bbc_radio_wales' => 'radio|BBC Radio Wales', 'bbc_radio_cymru' => 'radio|BBC Radio Cymru', 'bbc_world_service' => 'radio|BBC World Service', # 'categories/radio' => 'radio|All', }; # User Agents my %user_agent = ( coremedia => 'Apple iPhone v1.1.1 CoreMedia v1.0.0.3A110a', safari => 'Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/3A110a Safari/419.3', update => "get_iplayer updater (v${version} - $^O)", desktop => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9) Gecko/2008052906 Firefox/3.0', get_iplayer => "get_iplayer/$version $^O", ); # Setup signal handlers $SIG{INT} = $SIG{PIPE} =\&cleanup; # Other Non-option dependant vars my %cachefile = ( 'tv' => "${profile_dir}/tv.cache", 'radio' => "${profile_dir}/radio.cache", 'podcast' => "${profile_dir}/podcast.cache", ); my $get_iplayer_stream = 'get_iplayer_freevo_wrapper'; # Location of wrapper script for streaming with mplayer/xine on freevo my $historyfile = "${profile_dir}/download_history"; my $pvr_dir = "${profile_dir}/pvr/"; my $cookiejar = "${profile_dir}/cookies"; my $namedpipe = "${profile_dir}/namedpipe.$$"; my $lwp_request_timeout = 20; my $info_limit = 40; my $iphone_block_size = 0x2000000; # 32MB # Option dependant var definitions my %download_dir; my $cache_secs; my $mplayer; my $mplayer_opts; my $lame; my $lame_opts; my $vlc; my $vlc_opts; my $id3v2; my $tee; my $bandwidth; my @version_search_list; my $proxy_url; my @search_args = @ARGV; # Assume search term is '.*' if nothing is specified - i.e. lists all programmes push @search_args, '.*' if ! $search_args[0]; # PVR functions my %pvrsearches; # $pvrsearches{searchname}{