From 74ebc9d7f10ba3f320ee4d84685f381612fdb5fb Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Mon, 16 Nov 2009 00:17:58 +0000 Subject: [PATCH] First draft of tvmover script * Copies files into their correct /export/videos directory if it exists * Attempts to rename the file using tvrenamer * Creates a symlink to the moved file, overwriting the existing torrent-dir copy, permitting seeding without duplicating the file across filesystems. May be a few special cases which fail, and failures may cause data loss; needs testing. --- trunk/tvmover.pl | 198 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100755 trunk/tvmover.pl diff --git a/trunk/tvmover.pl b/trunk/tvmover.pl new file mode 100755 index 0000000..bd2900b --- /dev/null +++ b/trunk/tvmover.pl @@ -0,0 +1,198 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Data::Dumper; +use Getopt::Long; +use File::Basename; +use Pod::Usage; +use Sihnon::Common qw/verbose debug/; +use Switch; +use Sys::Pushd; +use File::Copy; +use FindBin; +use lib $FindBin::Bin; + + +# Globals +our %options = ( + verbose => 0, + quiet => 0, + silent => 0, + help => 0, + pretend => 0, + media_dir => '/export/videos/', + torrent_dir => '/export/torrents/completed/', + tvrenamer => '/usr/bin/tvrenamer', +); + +Getopt::Long::Configure( qw(bundling no_getopt_compat) ); +GetOptions( + 'verbose|v+' => \$Sihnon::Common::VERBOSE, + 'debug|d+' => \$Sihnon::Common::DEBUG, + 'quiet|q' => \$options{quiet}, + 'silent|s' => \$options{silent}, + 'help|h' => \$options{help}, + 'pretend|n' => \$options{pretend}, + 'media-dir|m=s' => \$options{media_dir}, + 'torrent-dir|t=s' => \$options{torrent_dir}, + 'tvrenamer=s' => \$options{tvrenamer}, +) or pod2usage(-verbose => 0); +pod2usage(-verbose => 1) if ($options{help}); + +my @args = @ARGV; + +# Get a list of tv series directories, and their normalised names +my %series_dir = read_series_directories($options{media_dir}); + +my $dh; +verbose("Reading contents of $options{torrent_dir}"); +opendir $dh, $options{torrent_dir}; +while (my $completed_torrent = readdir $dh) { + next if $completed_torrent =~ m/^\..*/; + + my $completed_filename = $options{torrent_dir} . '/' . $completed_torrent; + next if ! -f $completed_filename; + + verbose("Dealing with $completed_torrent ($completed_filename)"); + + my $output_dir = get_destination_dir_from_filename($completed_torrent); + if ( ! $output_dir) { + warn "Failed to find directory for $completed_torrent"; + next; + } + my $output_filename = $output_dir . '/' . $completed_torrent; + + # Copy the file over to the media share + copy_to_media_share($completed_filename, $output_filename) or next; + + # Attempt to rename the file + my $final_filename = $output_filename; + $final_filename = rename_media_share_file($output_filename); + + # Symlink the file back into the torrent directory for seeding + replace_with_symlink($final_filename, $completed_filename); +} + +sub read_series_directories { + my $media_share_dir = shift or die; + + my %series_dir = (); + my $dh; + opendir $dh, $media_share_dir; + while (my $file = readdir $dh) { + next if $file =~ m/^\..*/; + + my $canonical_file = canonicalise($file); + $series_dir{$canonical_file} = $file; + } + + return %series_dir; +} + +sub canonicalise { + my $input = shift or die; + $input =~ tr/A-Z/a-z/; + $input =~ s/[^a-z0-9]//g; + return $input; +} + +sub copy_to_media_share { + my $torrent_dir_filename = shift or die; + my $output_filename = shift or die; + + verbose("Copying $torrent_dir_filename to $output_filename"); + if ( ! $options{pretend}) { + return copy $torrent_dir_filename, $output_filename; + } else { + return 1; # Return true if only pretending + } +} + +sub get_destination_dir_from_filename { + my $source_filename = shift or die; + + my ($series, $season, $episode) = decode_filename($source_filename); + return unless $series; + + verbose("Series: $series, Season: $season, Episode: $episode"); + + return $options{media_dir} . '/' . $series . '/Season ' . $season; +} + +sub decode_filename { + my $input_filename = shift or die; + + # grab the series, and season number from the torrented filename + $input_filename =~ m/(.*)[-_\.]*(?:(\d+)x(\d+)|[sS](\d+)[eE](\d+))/; + my $series = $1; + my $season = $4; + my $episode = $5; + + # Get the real series name if it exists + my $canonical_series = canonicalise($series) or return; + # Sanitise the season and episode numbers + $season =~ s/0*(\d+)/$1/; + + return ($series_dir{$canonical_series}, $season, $episode); +} + +sub replace_with_symlink { + my $media_share_filename = shift or die; + my $torrent_dir_filename = shift or die; + + my $temp_filename = $torrent_dir_filename . '.tmp'; + verbose("Replacing $torrent_dir_filename with symlink to $media_share_filename"); + if ( ! $options{pretend}) { + symlink $media_share_filename, $temp_filename; + move $temp_filename, $torrent_dir_filename; + } +} + +sub rename_media_share_file { + my $input_filename = shift or die; + my $output_filename = $input_filename; + + return $output_filename if $options{pretend}; + + my $input_dir = dirname($input_filename); + + # Find out the season and episode number for the file we are renaming + my ($series, $season, $episode) = decode_filename(basename($input_filename)); + return $output_filename unless $series; + + # Change into the output directory temporarily, to run the renamer + my $changed_dir = new Sys::Pushd $input_dir; + + my $tvrenamer_cmd = "$options{tvrenamer} --unattended --noANSI --rangemin=$episode --rangemax=$episode"; + open my $tvrenamer_fh, "$tvrenamer_cmd|"; + my @tvrenamer_output = <$tvrenamer_fh>; + close $tvrenamer_fh; + + # Scan through the output and look for the new filename for the episode with this season and episode number + foreach my $line (@tvrenamer_output) { + my $identifier = $season . 'x' . $episode; + if ($line =~ m/$identifier/) { + $output_filename = $line; + chomp $output_filename; + + verbose("File has been renamed from $input_filename to $output_filename"); + + return $input_dir . '/' . $output_filename; + } + } + + return $output_filename; +} + +__END__; +=head1 NAME + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +=head1 OPTIONS + +=cut