#!/usr/bin/perl ############################################################################## ## Perl script which takes in a YAML file which contains metadata for ## episodes of "The Simpsons", and uses AtomicParsley to add said metadata ## to selected episodes of "The Simpsons", that you have ripped from DVD ## using HandBrake. The goal is to reduce much of the manual effort in ## ripping your DVDs, for use in iTunes, on Apple TV, and on your iPod. ## ## **NOTE:** When ripping episodes of "The Simpsons" from DVD, you must ## ensure that the Episode ID (i.e. "2F16") appears somewhere in the ## filename. This script understands the format of the episode IDs used on ## "The Simpsons", and will extract the ID from the filename. The episode ID ## is the unique key, which is used to identify the proper metadata in the ## YAML file. ## ## Requires AtomicParsely, which you can download from here: ## http://atomicparsley.sourceforge.net/ ## ## In addition, this script requires the YAML::XS perl CPAN module, as well ## as a YAML input file, as produced by 'mk_simpsons_episode_db.pl'. ## ## By Andy Reitz , January 17th, 2009. ## URL: http://redefine.dyndns.org/~andyr/blog/archives/2009/02/the-simpsons-on-apple-tv.html ############################################################################## use strict; use Getopt::Long; use YAML::XS; # Global variables. my ($DEBUG) = 0; my ($allEpisodesHash) = ""; my ($optDatafile) = ""; my ($optDir) = ""; my ($optFile) = ""; my ($optVerbose) = ""; my ($optHelp) = ""; my ($optAtomicParsley) = "AtomicParsley"; my ($optOverwrite) = ""; # List of files to operate on. my (@files) = (); # Parse the command-line options. if (!GetOptions ("datafile=s" => \$optDatafile, "dir=s" => \$optDir, "file=s" => \$optFile, "atomicParsley=s" => \$optAtomicParsley, "overwrite" => \$optOverwrite, "verbose|v+" => \$optVerbose, "help|h|?" => \$optHelp, # Use --debug or -d, specify multiple times to increase # debugosity. "debug|d+" => \$DEBUG)) { &usage(); exit (3); } if ($optHelp) { &usage(); exit (3); } if (! -x $optAtomicParsley) { print "ERROR: The AtomicParsely program that you've specified, '$optAtomicParsley', isn't executable!\n"; exit (4); } if ($optFile) { if (-f $optFile) { push @files, $optFile; } else { print "WARN: specified file '$optFile' not readable."; } } if ($optDir) { push @files, &extractFilesFromDir ($optDir); } if ($optDatafile) { $allEpisodesHash = &loadYAMLFile ($optDatafile); } else { print "ERROR: You must specify a YAML datafile!\n"; &usage(); exit (3); } my ($f); my ($epid) = ""; foreach $f (@files) { $epid = &extractEpisodeID ($f); if (!$epid) { print "WARN: Filename '$f' doesn't contain a valid Simpsons Episode ID!\n"; } &v("Found file '$f' with Episode ID '$epid'"); if ($allEpisodesHash->{$epid}) { &v ("Found metadata for '$epid'. Episode Number: " . $allEpisodesHash->{$epid}->{"Episode Number"}); # Build up the arguments to the AtomicParsley invocation. my ($cmdArgs) = "--TVNetwork Fox --TVShowName 'The Simpsons' --TVEpisode $epid --stik 'TV Show' --genre 'Comedy'"; $cmdArgs .= " --TVSeasonNum " . $allEpisodesHash->{$epid}->{"Season"}; $cmdArgs .= " --TVEpisodeNum " . $allEpisodesHash->{$epid}->{"Episode Number"}; # FIXME: Add # of episodes per season here. $cmdArgs .= " --tracknum " . $allEpisodesHash->{$epid}->{"Episode Number"}; my ($year) = &parseYearFromDate ($allEpisodesHash->{$epid}->{"Original Airdate"}); if ($year) { $cmdArgs .= " --year $year"; } $cmdArgs .= " --description \"" . &escapeString ($allEpisodesHash->{$epid}->{"Description"}) . "\""; $cmdArgs .= " --title \"" . &escapeString ($allEpisodesHash->{$epid}->{"Episode Title"}) . "\""; $cmdArgs .= " --artist \"" . $allEpisodesHash->{$epid}->{"Written by"} . "\""; # Need to set this to the show name, so that Apple TV will show the # episodes correctly. Stupid Apple TV! $cmdArgs .= " --albumArtist \"The Simpsons\""; $cmdArgs .= " --composer \"" . $allEpisodesHash->{$epid}->{"Directed by"} . "\""; my ($cmd) = "$optAtomicParsley $f $optOverwrite $cmdArgs"; &DBG ("** CMD to execute: $cmd **"); &v("About to execute AtomicParsley..."); system ($cmd); &v("... AtomicParsley finished.\n\n"); # FIXME: check return code of AtomicParsley. } else { print "WARN: No metadata for Episode ID '$epid' found in YAML datafile!\n"; } } # foreach file # End script. exit (0); ############################################################################## ## Subroutines ## # Metadata details end up as command-line arguments to AtomicParsley, so we # have to escape the following special characters (works with 'bash'): # # - " # - $ # sub escapeString { my ($s) = $_[0]; $s =~ s/\$/\\\$/g; $s =~ s/"/\\"/g; return ($s); } # End escapeString(). # Given a date in the form of "Original Airdate" on Wikipedia, this # subroutine returns just the year (which is all that iTunes can store). sub parseYearFromDate { my ($s) = $_[0]; if ($s =~ /(19\d\d)/) { return ($1); } elsif ($s =~ /(20\d\d)/) { return ($1); } return; } # End parseYearFromDate(). # Reads in the YAML file from disk, and re-constitutes it as an in-memory # hash. sub loadYAMLFile { my ($fn) = $_[0]; if (!open (FH, "<", $fn)) { print "ERROR: couldn't open datafile '$fn' for reading. Reason: $!\n"; exit (1); } my (@contents) = ; if (!close (FH)) { print "ERROR: couldn't close datafile '$fn'. Reason: $!\n"; exit (1); } # Invoke YAML::XS 'Load' method to take the YAML input and turn it into a # hash of hashes. return (Load join ("", @contents)); } # End loadYAMLFile(). # Given a directory, this subroutine will parse out all of the regular # files, and return a list of files that appear to be videos. sub extractFilesFromDir { my ($dir) = $_[0]; my (@matchingFiles) = (); if (!opendir (DH, $dir)) { print "WARN: Unable to open specified directory '$dir' for reading. Reason: $!\n"; return (@matchingFiles); } # If the directory includes a trailing slash, strip it. if ($dir =~ /\/$/) { chop ($dir); } my ($fn) = ""; while ($fn = readdir (DH)) { if ($fn =~ /[.]mp4|[.]m4v$/) { # FIXME: Just check file extension to make sure it's a video. Should # probably use some MIME/'file' solution, but this is easy on me. push @matchingFiles, "$dir/$fn"; } } if (!closedir (DH)) { print "WARN: Unable to close directory '$dir'. Reason: $!\n"; } return (@matchingFiles); } # End extractFilesFromDir(). # Given a string, this will extract a Simpson-esque Episode ID, and return # it. Simpson Episode IDs are in the form of: # # - #F## # - #G## # - AABF## # - BABF## # - CABF## # - etc... # sub extractEpisodeID { my ($f) = $_[0]; if ($f =~ /(\d[fFgG]\d\d)/) { return (uc($1)); } elsif ($f =~ /([ABCDEFGHJK]ABF\d\d)/i) { return (uc($1)); } # If we get here, then we couldn't find a Simpson-esque Episode ID in the # string. return; } # End extractEpisodeID(). # Print debugging messages to STDERR, if enabled. sub DBG { my ($msg) = $_[0]; if ($DEBUG > 0) { chomp ($msg); print STDERR "DBG: $msg\n"; } } # End DBG(). # Print verbose messages to STDOUT, only if enabled. sub v { my ($msg) = $_[0]; if ($optVerbose > 0) { chomp ($msg); print "$msg\n"; } } # End v(). sub usage { print < [parameters] This script will take in the YAML file of metadata for Simpsons episodes, and tag the specified files using AtomicParsley. Parameters: =========== --datafile Specify a YAML db file of metadata (required). --dir Specify a directory of files to work on. --file Specify a single episode to work on. --atomicParsley Specify the location of the 'AtomicParsley' binary, if it isn't in your \$PATH. --overwrite Enable "overwrite" mode in AtomicParsley. NOT RECOMMENDED --verbose Enable progress report messages from this script. --help Display this help text. --debug Turn on debugging output. Examples: ========= 1. Add metadata (in a new file), for every file in the 'Simpsons_Season_8' directory (should be all on one line): ./add_metadata_to_simpsons_episodes.pl --verbose \ --datafile=simpsons_metadb_seasons_1-11.yml \ --dir=/Volumes/Excelsior/Simpsons_Season_8 \ --atomicParsley=/Users/andyr/Downloads/AtomicParsley-MacOSX-0.9.0/AtomicParsley EOF } # End usage().