#!/usr/bin/perl
# Filename:	myfilm
# Author:	David Ljung Madison <DaveSource.com>
# 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
# Also see: http://www.jwz.org/hacks/#youtubedown
# Also see:  cclive
##################################################
# 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 <<USAGE;

Usage:\t$PROGNAME [-d] <youtube URL, ifilm URL or id>
  Download a youtube or ifilm movie
  -o <file>         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|^https?://([^/]+)/.*?([^/]+)$|) { ($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(<FILM>) {
		$dl = $1 if /var\s+dl\s*=\s*"?(-?\d+)"?/;
		$bw = $1 if !$bw && /var\s+bw\s*=\s*"?(\d+)"?/;
		$embed = $1 if /<embed.*(http:[^>"]+)/i;
		$title = $1 if /span class="title">([^<]+)/;
		$title = $1 if /<title>(.*)<\/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();
