#!/usr/bin/perl ############################################################################## ## Perl script which takes in a YAML file which contains metadata for ## episodes of "Angel", and uses AtomicParsley to add said metadata to ## selected episodes of "Angel", 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 "Angel" from DVD, you must ## ensure that the Episode ID (i.e. "2ADH22") appears somewhere in the ## filename. This script understands the format of the episode IDs used on ## "Angel", 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_angel_episode_db.pl'. ## ## By Andy Reitz , September 7th, 2009. ## URL: http://redefine.dyndns.org/~andyr/blog/archives/2009/09/angel-in-my-itunes.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) = ""; my ($optDOIT); # Static variables. Would be nice to harvest this from the YAML file, so # that this script can become more generic. my ($tvNetwork) = "The WB"; my ($showName) = "Angel"; my ($genre) = "Drama"; # 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, "DOIT" => \$optDOIT, # 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); } if (!$optDOIT) { print "WARN: --doit flag unspecified, operating in read-only mode.\n"; } my ($f); my ($epid) = ""; foreach $f (@files) { $epid = &extractEpisodeID ($f); if (!$epid) { print "WARN: Filename '$f' doesn't contain a valid Angel 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 '$tvNetwork' --TVShowName '$showName' --TVEpisode $epid --stik 'TV Show' --genre '$genre'"; $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 \"$showName\""; $cmdArgs .= " --composer \"" . $allEpisodesHash->{$epid}->{"Directed by"} . "\""; my ($cmd) = "$optAtomicParsley '$f' $optOverwrite $cmdArgs"; if ($optDOIT) { &DBG ("** CMD to execute: $cmd **"); &v("About to execute AtomicParsley..."); system ($cmd); &v("... AtomicParsley finished.\n\n"); } else { print "WOULD EXEC: $cmd\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]; &DBG ("pYFD(): airdate string: '$s'"); 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 the Angel episode ID, which is in the # form of: # # - #ADH## # sub extractEpisodeID { my ($f) = $_[0]; if ($f =~ /(\dadh\d\d)/i) { return (uc($1)); } # If we get here, then we couldn't find a Angel-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 < [--DOIT] [parameters] This script will take in the YAML file of metadata for $showName episodes, and tag the specified files using AtomicParsley. **NOTE:** by default, this program operates in "read-only" mode. It will read in the metadata file, and the source movie files, and simply print out the commands that it would execute. You must add the '--DOIT' flag in order to make this script execute the commands, which will add the metadata to your movie files. Parameters: =========== --DOIT Actually execute AtomicParsley and change your video files. --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. Print out the AtomicParsley commands that would add metadata to every file in the 'Angel_Season_1' directory (should all be on one line): ./add_metadata_to_angel_episodes.pl --verbose \ --datafile=angel_metadb_seasons_1-5.yml \ --atomicParsley=/Users/andyr/Downloads/AtomicParsley-MacOSX-0.9.0/AtomicParsley \ --dir=/Volumes/The_Trio/Angel_Season_1/ 2. Actually modify the files: ./add_metadata_to_angel_episodes.pl --verbose \ --datafile=angel_metadb_seasons_1-5.yml \ --atomicParsley=/Users/andyr/Downloads/AtomicParsley-MacOSX-0.9.0/AtomicParsley \ --dir=/Volumes/The_Trio/Angel_Season_1/ \ --DOIT 3. Update the metadata in just one file: ./add_metadata_to_angel_episodes.pl \ --datafile=angel_metadb_seasons_1-5.yml \ --atomicParsley=/Users/andyr/Downloads/AtomicParsley-MacOSX-0.9.0/AtomicParsley \ --verbose --file=/Volumes/The_Trio/Angel\ Season\ 3/3adh01.m4v --DOIT EOF } # End usage().