Files
tvmover/trunk/tvmover.pl
Ben Roberts 9202d5f127 Added better log handling
Added support for moving files in subdirectories of the torrent-dir
Added support for altering the --series option for the call to tvrenamer
Added filters to ignore trailing country codes, or year numbers from series names
Added support for tv shows without conventional season numbers, and episode numbers using Pt# or Part#
2009-11-22 14:47:53 +00:00

296 lines
8.8 KiB
Perl
Executable File

#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
use Getopt::Long;
use File::Basename;
use Log::Handler;
use Pod::Usage;
use Switch;
use Sys::Pushd;
use File::Copy;
use FindBin;
use lib $FindBin::Bin;
# Globals
our %options = (
verbose => 0,
quiet => 0,
log_file => '',
silent => 0,
help => 0,
pretend => 0,
interactive => 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+' => \$options{verbose},
'debug|d' => \$options{debug},
'quiet|q' => \$options{quiet},
'silent|s' => \$options{silent},
'log|l=s' => \$options{log_file},
'help|h' => \$options{help},
'pretend|n' => \$options{pretend},
'interactive|i' => \$options{interactive},
'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});
# Override the series name for calls to tvrenamer for select shows
my %series_overrides = (
csinewyork => 'CSINY',
);
#setup logging
my $log = Log::Handler->new();
my $maxLogLevel = ($options{debug} ? 'debug' : ($options{quiet} ? 3 : $options{verbose} + 4)); # default to logging warning+, unless quiet in which case log error+, or debug in which case log everything
my $maxFileLogLevel = ($options{debug} ? 'debug' : 'info');
if ( ! $options{silent}) {
$log->add(
screen => {
log_to => 'STDOUT',
minlevel => 'emergency',
maxlevel => $maxLogLevel,
},
);
}
if ( $options{log_file}) {
$log->add(
file => {
filename => $options{log_file},
minlevel => 'emergency',
maxlevel => $maxFileLogLevel,
},
);
}
# Get a list of tv series directories, and their normalised names
my %series_dir = read_series_directories($options{media_dir});
# Scan the torrent directory for completed files
process_completed_directory($options{torrent_dir});
# done!
sub process_completed_directory {
my $base_directory = shift or die;
my $dh;
$log->notice("Reading contents of $base_directory");
opendir $dh, $base_directory;
while (my $completed_torrent = readdir $dh) {
next if $completed_torrent =~ m/^\..*/;
my $completed_filename = $base_directory . '/' . $completed_torrent;
next if -l $completed_filename; # ignore symlinks
# If the entry is a directory and is named after an episode, attempt to scan it
if ( -d $completed_filename) {
# Attempt to grab the series name from the dir name
my ($series) = decode_filename($completed_torrent);
next unless $series;
$log->info("Moving into subdirectory $completed_filename");
process_completed_directory($completed_filename);
} else {
$log->notice("Dealing with $completed_torrent ($completed_filename)");
# Only move video files
next unless ($completed_torrent =~ m/\.(avi|mkv)$/i);
my $output_dir = get_destination_dir_from_filename($completed_torrent);
if ( ! $output_dir) {
$log->warning("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;
# Strip out trailing year numbers
$input =~ s/(19|20)\d\d$//;
# Strip out trailing country codes
$input =~ s/(us|uk)$//;
# Handle a couple of series specific haxes
$input =~ s/csiny/csinewyork/;
$input =~ s/ncisla/ncislosangeles/;
$input =~ s/theclevelandshow/clevelandshow/;
return $input;
}
sub copy_to_media_share {
my $torrent_dir_filename = shift or die;
my $output_filename = shift or die;
$log->info("Copying $torrent_dir_filename to $output_filename");
if ($options{interactive}) {
return unless confirm("Copy $torrent_dir_filename to $output_filename? [y/N]");
}
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;
if ($season) {
return $options{media_dir} . '/' . $series . '/Season ' . $season;
} else {
return $options{media_dir} . '/' . $series;
}
}
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+)|P(?:ar)?t(\d+))/;
my $series = $1;
my $season = $2 || $4;
my $episode = $3 || $5 || $6;
return unless $series;
$log->debug("Series: $series, Season: $season, Episode: $episode");
# 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/ if $season;
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';
$log->notice("Replacing $torrent_dir_filename with symlink to $media_share_filename");
if ($options{interactive}) {
return unless confirm("Replace $torrent_dir_filename with symlink to $media_share_filename? [y/N]");
}
if ( ! $options{pretend}) {
symlink $media_share_filename, $temp_filename or return;
move $temp_filename, $torrent_dir_filename;
}
}
sub rename_media_share_file {
my $input_filename = shift or die;
my $output_filename = $input_filename;
$log->notice("Attempting to rename $input_filename using tvrenamer");
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;
my $canonical_series = canonicalise($series);
# Change into the output directory temporarily, to run the renamer
my $changed_dir = new Sys::Pushd $input_dir;
# Certain shows require a custom call to tvrenamer in order to work, eg CSI:NY
my $override_series = '';
if ($series_overrides{$canonical_series}) {
$log->debug("Overriding series name for $series with $series_overrides{$canonical_series}");
$override_series = "--series='$series_overrides{$canonical_series}'";
}
my $tvrenamer_cmd = "$options{tvrenamer} --unattended --noANSI $override_series --rangemin=$episode --rangemax=$episode --postproc='s/-img---a- -a-//;'";
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;
$log->notice("File has been renamed from $input_filename to $output_filename");
return $input_dir . '/' . $output_filename;
}
}
return $output_filename;
}
sub confirm {
my $message = shift or die;
print $message, ": ";
my $response = <STDIN>;
return $response =~ m/^[yY]$/;
}
__END__;
=head1 NAME
=head1 SYNOPSIS
=head1 DESCRIPTION
=head1 OPTIONS
=cut