#!/usr/bin/perl # Filename: myfilm # Author: David Ljung Madison # See License: http://MarginalHacks.com/License/ # Description: Download iFilm/youtube movies given a film ID. # Converts youtube films from flash to .mov if ffmpeg is available. my $VERSION= '1.05'; # # Requires that ifilm/youtube's obfuscation doesn't change drastically. # # youtube: http://youtube.com/watch?v=[video_id] # video at: http://youtube.com/get_video.php?l=165&video_id=[video_id]&t=[??] # Need to find &t=[code] in HTML # # ifilm: http://www.ifilm.com/ifilmdetail/2667392 # -> http://secure.ifilm.com/qt/portal/2667392_300.mov?e=1159050000&h=6c97297693c58d818e86aba0819abf10 # Update: Don't know how to look this up with iFilm's new Flash player # # Also see: http://javimoya.com/blog/youtube_en.php ################################################## # CHANGELOG ################################################## # 1.05 2012/05/15 # ---------------- # + Cleaner output and menu option # # 1.04 2011/12/23 # ---------------- # + Updated for new youtube obfuscation (super complicated!) # # 1.03 2008/01/01 # ---------------- # * Added "-acodec copy" to avoid "Unsupported codec" errors # # 1.02 2006/05/13 # ---------------- # + Updated for new youtube obfuscation (&t=[code]) # # 1.01 2006/04/08 # ---------------- # + Released as myfilm # + Now handles youtube as well as iFilm # # 1.00 2005/12/08 # ---------------- # + Released as diFilm # ################################################## use strict; ################################################## # Setup the variables ################################################## my $PROGNAME = $0; $PROGNAME =~ s|.*/||; my ($BASENAME,$PROGNAME) = ($0 =~ m|(.*)/(.+)|) ? ($1?$1:'/',$2) : ('.',$0); # This will probably change.. my $IFILM = "http://www.ifilm.com/player/mac.jsp?ifilmId="; #my $YOUTUBE = "http://youtube.com/get_video.php?l=165&video_id="; # How to fetch web files - pick one # Actual Safari agent example: # Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13 my $GET = "GET -H 'user-agent: Mozilla/5.0'"; my $LYNX = "lynx -source"; my $GET_URL = $LYNX; my $POST = "mov"; # For converting youtube to .mov my $FFMPEG = "ffmpeg"; # Thanks to: http://users.ohiohills.com/fmacall/YTCRACK.TXT my $YOUTUBE_ITAG = { 5 => ['FLV','320x240'], 18 => ['MP4','480x360'], 22 => ['MP4','1280x720'], 34 => ['FLV','480x360'], 35 => ['FLV','640x480'], 37 => ['MP4','1920x1080'], 38 => ['MP4','2048x1080'], 43 => ['WEB','480x360'], 44 => ['WEB','640x480'], 45 => ['WEB','1280x720'], 46 => ['WEB','1920x1080'], # These are SS3D formats'], 82 => ['MP4','480x360'], 84 => ['MP4','1280x720'], 100 => ['WEB','480x360'], 102 => ['WEB','1280x720'], }; ################################################## # Usage ################################################## sub fatal { foreach my $msg (@_) { print STDERR "[$PROGNAME] ERROR: $msg\n"; } exit(-1); } sub usage { foreach my $msg (@_) { print STDERR "ERROR: $msg\n"; } print STDERR < Download a youtube or ifilm movie -o Save to file -d Set debug mode -v Verbose (show URLS) -bg Run ffmpeg in the background -flv Keep as flv instead of converting Youtube options: -nomenu Automatically choose which format to download iFilm options: -bw <56,200,300> Force a bandwidth other than your iFilm settings USAGE exit -1; } sub parse_args { my $opt = {}; while (my $arg=shift(@ARGV)) { if ($arg =~ /^-h$/) { usage(); } if ($arg =~ /^-d$/) { $MAIN::DEBUG=1; next; } if ($arg =~ /^-v$/) { $opt->{v}=1; next; } if ($arg =~ /^-no_?menu$/) { $opt->{nomenu}=1; next; } if ($arg =~ /^-bg$/) { $opt->{bg} = 1; next; } if ($arg =~ /^-bw$/) { $opt->{bw} = shift @ARGV; next; } if ($arg =~ /^-o$/) { $opt->{save} = shift @ARGV; next; } if ($arg =~ /^-flv$/) { $opt->{flv} = shift @ARGV; next; } if ($arg =~ /^-/) { usage("Unknown option: $arg"); } if ($arg =~ m|^http://([^/]+)/.*?([^/]+)$|) { ($opt->{url}, $opt->{dom}, $opt->{film}) = ($arg, $1, $2); next; } if ($arg =~ /^[0-9]+$/) { ($opt->{arg}, $opt->{dom},$opt->{film}) = ("ifilm.com", $arg); next; } usage("Unknown film id. I need an iFilm or youtube URL"); } usage("No film URL specified.") unless $opt->{film}; $opt; } sub debug { return unless $MAIN::DEBUG; foreach my $msg (@_) { print STDERR "[$PROGNAME] $msg\n"; } } ################################################## # Utils ################################################## sub title_embed { my ($opt, $url) = @_; debug("URL: $url"); my $open = "$GET_URL $url"; debug("GET: $open"); open(FILM,"$open |") || usage("Couldn't get URL: $url\nMake sure [$GET_URL] is installed"); # Find the film location my $dl; my $title; my $bw = $opt->{bw}; my $embed; my $and_t; while() { $dl = $1 if /var\s+dl\s*=\s*"?(-?\d+)"?/; $bw = $1 if !$bw && /var\s+bw\s*=\s*"?(\d+)"?/; $embed = $1 if /"]+)/i; $title = $1 if /span class="title">([^<]+)/; $title = $1 if /(.*)<\/title>/; $and_t = $1 if /video_id=.*&t=([^&"]+)/; } close(FILM); ($title,$embed,$dl,$bw,$and_t); } ################################################## # iFilm ################################################## sub iFilm { my ($opt,$film) = @_; print STDERR "Fetching iFilm ID: $film\n"; # Read the ifilm HTML my $url = $IFILM.$film; my ($title,$smil,$dl,$bw) = title_embed($opt, $url); $title = $title || $film; usage("Couldn't find 'dl' key in URL [$url]") unless $dl; usage("Couldn't find 'bw' key in URL [$url]") unless $bw; usage("Couldn't find 'embed' tag in URL [$url]") unless $smil; $smil =~ s/'\s+\+\s+dl\s+\+\s+'/$dl/g; $smil =~ s/'\s+\+\s+bw\s+\+\s+'/$bw/g; $smil =~ s/"$//; debug("SMIL: $smil"); # Get the smil file my $open = "$GET_URL \Q$smil\E"; open(SMIL,"$open |") || usage("Couldn't get SMIL: $smil"); my $video; while (<SMIL>) { $video = $1 if /video src="(http[^"]+)"/; } usage("Couldn't find video source key in SMIL [$smil]") unless $video; close SMIL; debug("Found: $video"); $title =~ s/[:\s]+/_/g; my $save = $opt->{save} || "$title.$POST"; $save =~ s|/|_of_|g; print "Saving to: $save\n"; debug("$GET_URL \Q$video\E > \Q$save\E\n"); system("$GET_URL \Q$video\E > \Q$save\E"); } ################################################## # youtube ################################################## sub unquote { my ($str) = @_; # I think this is right $str =~ s/\+/ /g; $str =~ s/%([a-f0-9]{2})/chr(hex($1))/eig; $str; } sub dict { my ($str) = @_; my %dict; foreach my $kv ( split('&',$str) ) { my ($k,$v) = split('=',$kv,2); $dict{$k} = unquote($v); } \%dict; } sub youtube { my ($opt, $film) = @_; print STDERR "Fetching youtube ID: $film\n"; # Get vid info (contains token=...) my $info = { author => 'unknown', title => 'unknown', }; foreach my $el ('&el=embedded', '&el=detailpage', '&el=vevo', '') { my $vid_info_url = "http://www.youtube.com/get_video_info?&video_id=$film$el&ps=default&eurl=&gl=US&hl=en"; my $show = $opt->{v} ? $vid_info_url : $el; print " Get Vid Info: $show\n"; open(INFO,"$GET_URL \Q$vid_info_url\E |") || die("Can't open video info URL [with $GET_URL]\n"); while (<INFO>) { chomp; $info = dict($_); } close INFO; last if $info->{token}; } die("Couldn't find video info\n") unless $info->{token}; #print "TOKEN: $info->{token}\n"; #print "author: $info->{author}\n"; #print "title: $info->{title}\n"; #print "conn: $info->{conn}\n"; print "-"x50,"\n"; my (@urls,@formats,$choose); if ($info->{conn} && $info->{conn} =~ /^rtmp/) { push(@urls,$info->{conn}); # Haven't tested this - for RMTP protocol push(@formats,'rmtp'); # Don't know what the real format should be... $choose = 0; } elsif ($info->{url_encoded_fmt_stream_map}) { # General case my $cnt = -1; foreach my $uefsm ( split(',',$info->{url_encoded_fmt_stream_map}) ) { $cnt++; my $d = dict($uefsm); next unless $d->{itag}; my $itag = $YOUTUBE_ITAG->{$d->{itag}}; next unless $itag && ref $itag eq 'ARRAY'; my ($form,$res) = @$itag; print "$cnt: " unless $opt->{nomenu}; print "$res $form"; print ": $d->{url}\n" if $opt->{v}; print "\n"; push(@urls,$d->{url}); push(@formats,$form); # MP4s seem to work better # Choose last item, stopping if we find an MP4 next unless $opt->{nomenu}; next if defined $choose && $formats[$choose] eq 'MP4'; $choose = $cnt; } # Menu unless (defined $choose) { print STDERR "\nSelect [0-$#urls] "; my $ans = <>; chomp $ans; fatal("Need to choose between 0 and $#urls") unless $ans eq "0" || ($ans>0 && $ans<=$#urls); } die("Couldn't find known itag/urls in 'url_encoded_fmt_stream_map'\n") unless $urls[$choose]; } else { die("No 'conn' or 'url_encoded_fmt_stream_map' in video info?\n") } my ($url,$format) = ($urls[$choose],$formats[$choose]); # Get name my $save = $opt->{save}; unless ($save) { $format = lc($format); $save = "$info->{title}.$format"; $save =~ s/\s+/_/g; $save =~ s|/|_of_|g; # ffmpeg gets confused easily $save =~ s/[\*\?\'\"]//g; # Get rid of troublesome characters. } print STDERR "Found film '$save'\n"; # Get video #print "TITLE; $title -> $save\n"; #my $video = $YOUTUBE.$film."&t=$and_t"; my $video = $url; debug("Video: $video"); print "Saving to: $save\n"; system("$GET_URL \Q$video\E > \Q$save\E"); return unless $save =~ /\.flv$/; return if $opt->{flv}; # Attempt convert to .mov my $mov = $save; $mov =~ s/\.flv$/.mov/; print "Convert to: $mov\n"; # "-acodec copy" avoids "Unsupported codec for output stream #0.1" exec($FFMPEG,"-v",0,"-i",$save,"-acodec","copy",$mov) unless fork; if ($opt->{bg}) { sleep 1; # To let the intial ffmpeg output go through.. } else { wait; } } ################################################## # Main code ################################################## sub main { my $opt = parse_args(); if ($opt->{dom} =~ /youtube/i) { $opt->{film} = "watch?v=$opt->{film}" if $opt->{url} =~ s|www.youtube.com/v/|www.youtube.com/watch?v=|; usage("Couldn't find youtube video (URL must contain 'v=[video_id]')") unless $opt->{film} =~ /v=([a-z0-9_-]+)/i; return youtube($opt, $1); } if ($opt->{dom} =~ /ifilm/i) { usage("Couldn't find iFilm video (URL must contain a number/film id)") unless $opt->{film} =~ /([0-9]+)/; return iFilm($opt, $1); } } main();