Here is wrapto v1.3, which offers a few small changes and bugfixes:
- Per djk44883’s comment, without --verbose wrapto provided no output, and only displayed capto output. This version modifies the behavior slightly to provide minimal but useful output without --verbose
- Removed too-verbose Data::Dumper output when --verbose is passed
- When passing numeric file IDs, check each one to make sure it actually exists before attempting to fetch
- Removed logspam from attempting to use uninitialized values
- Fixed help message formatting
The source is below. The forum is giving me a hard time about formatting (preformatted isn’t really preformatted, apparently), so here’s a link as well: wrapto 1.3
#!/usr/bin/perl -w
use strict; use warnings;
use Getopt::Long;
use Data::Dumper;
my $VERSION = "1.3";
my $verbose = 0;
my $quiet = 0;
my $get = 0;
my $test = 0;
my $all = 0;
my $do_update = 0;
my $show_help = 0;
my $name = '';
my $range = '';
my $filename = '';
my @episode_list = ();
my $capto_args = "";
my $capto;
die(usage()) unless @ARGV;
GetOptions (
'quiet' => \$quiet,
'verbose' => \$verbose,
'range=s' => \$range,
'name=s' => \$name,
'get' => \$get,
'test' => \$test,
'all' => \$all,
'update' => \$do_update,
'help' => \$show_help,
'filename=s' => \$filename,
'capto=s' => \$capto,
'capto-args=s' => \$capto_args,
);
# Remove leading/trailing quotes if Getopt doesn't
if (defined $range) {
$range =~ s/^['"]//g;
$range =~ s/['"]$//g;
}
if (defined $name) {
$name =~ s/^['"]//g;
$name =~ s/['"]$//g;
}
if (defined $filename) {
$filename =~ s/^['"]//g;
$filename =~ s/['"]$//g;
}
if (defined $capto) {
$capto =~ s/^['"]//g;
$capto =~ s/['"]$//g;
}
if (defined $capto_args) {
$capto_args =~ s/^['"]//g;
$capto_args =~ s/['"]$//g;
}
if ($show_help) {
print show_help();
exit;
}
warn("wrapto: Starting up with verbose='$verbose', name='$name', range='$range', "
. "get='$get', test='$test', all='$all',\n") if $verbose;
$capto ||= "./capto";
my @filename_args = ("-f", "plex");
my @capto_args = ();
if ($capto_args) {
@capto_args = split(/\s+/, $capto_args);
}
my @search = ("-s");
my $success = 0;
my $fail = 0;
my $start = time();
my %all_list = ();
if ($get || $test) {
# Fetch a list of all files first
my @list = ();
@list = `$capto -s ALL`;
foreach my $line (@list) {
chomp $line;
if ($line =~ /\[capto\] \[(\d\d\d\d-\d\d-\d\d) (\d\d:\d\d)\] \((\d+)\) (.*?) - s(\d\d)e(\d\d) - ?(.*)?/) {
my $id = $3;
my $item = { date => ($1 || "unknown_date"), time => ($2 || "unknown_time"),
id => ($3 || "unknown_id"), showname => ($4 || "unknown_showname"),
season => ($5 || "unknown_season"), episode => ($6 || "unknown_ep"),
epname => ($7 || "") };
if ($id && $item->{showname}) {
$item->{showname} =~ s/^\s*//g;
$item->{showname} =~ s/\s*$//g;
$item->{epname} =~ s/^\s*//g;
$item->{epname} =~ s/\s*$//g;
$all_list{$id} = $item;
}
else {
warn("Warning: skipping invalid entry for item '$id' ($line)\n");
}
# warn Dumper $item;
}
}
if ($all && ! $name) {
warn("Retrieving all content...\n") if $verbose;
@episode_list = sort { $a <=> $b } keys %all_list;
# print Dumper %all_list;
}
elsif (($name && $range) || ($name && $all)) {
my ($start_season, $start_ep, $end_season, $end_ep);
if ($range) {
warn("Looking at episode range '$range' for program '$name'\n") if $verbose;
($start_season, $start_ep, $end_season, $end_ep) =
$range =~ /s(\d+)e(\d+)-s(\d+)e(\d+)/;
die("Invalid range '$range'\n") unless defined $start_season
&& defined $start_ep && defined $end_season && defined $end_ep;
$start_season *= 1;
$end_season *= 1;
$start_ep *= 1;
$end_ep *= 1;
warn("Looking for episodes of '$name' between s${start_season}e${start_ep} "
. "and s${end_season}e${end_ep}\n") if $verbose;
}
else {
warn("Looking for all episodes for program '$name'\n") if $verbose;
}
my @episodes = `$capto @search "$name"`;
warn("Found all possible episodes of '$name':\n@episodes\n") if $verbose;
foreach my $ep (sort @episodes) {
chomp $ep;
my ($id, $sno, $eno) = $ep =~ /\((\d+)\) $name - s(\d+)e(\d+)/;
if (defined $sno && defined $eno && (
($all)
||
# range case 1: when start season == end season
($start_season == $end_season && $sno == $start_season
&& $eno >= $start_ep && $eno <= $end_ep)
||
# range case 2: when episode is in start season
($start_season != $end_season && $sno == $start_season && $eno >= $start_ep)
||
# range case 3: when episode is in end season
($start_season != $end_season && $sno == $end_season && $eno <= $end_ep)
||
# range case 4: when episode is in middle-of-range season
($start_season != $end_season && $sno > $start_season && $sno < $end_season)
))
{
push(@episode_list, $id);
warn("Adding episode #$id ($ep)\n") if $verbose;
}
}
}
else {
while (my $id = shift @ARGV) {
# xxxx-yyyy range of IDs
if ($id =~ /(\d+)-(\d+)/) {
my $start = $1 * 1;
my $end = $2 * 1;
warn("Found ID range '$start' to '$end'\n") if $verbose;
if ($end > $start) {
while ($end >= $start) {
warn("Adding range ID '$start' to fetch list\n") if $verbose;
push(@episode_list, $start);
$start++;
}
}
elsif ($start > $end) {
while ($start >= $end) {
warn("Adding range ID '$end' to fetch list\n") if $verbose;
push(@episode_list, $end);
$end++;
}
}
else {
warn("Adding single ID '$start' to fetch list\n") if $verbose;
push(@episode_list, $start);
}
}
# Single ID
else {
warn("Adding episode id '$id' to fetch list\n") if $verbose;
push(@episode_list, $id);
}
}
}
if (@episode_list) {
foreach my $id (@episode_list) {
unless (defined $all_list{$id}) {
print("Item '$id' not found in list. Skipping.\n");
next;
}
my @capto_cmd = ($capto, "-e", $id);
if ($filename) {
my $output_file = $filename;
#warn("id='$id', showname='$all_list{$id}->{showname}', epname='$all_list{$id}->{epname}'\n");
$output_file =~ s/\%D/\Q$all_list{$id}->{date} $all_list{$id}->{time}\E/g;
$output_file =~ s/\%d/\Q$all_list{$id}->{date}\E/g;
$output_file =~ s/\%i/\Q$all_list{$id}->{id}\E/g;
$output_file =~ s/\%s/\Q$all_list{$id}->{season}\E/g;
$output_file =~ s/\%e/\Q$all_list{$id}->{episode}\E/g;
$output_file =~ s/\%n/\Q$all_list{$id}->{showname}\E/g;
if ($filename =~ /\%t/) {
if ($all_list{$id}->{epname}) {
$output_file =~ s/\%t/\Q$all_list{$id}->{epname}\E/g;
}
else {
warn("Retrieving file with no episode name, but filename template calls for it. Using file ID instead.\n");
$output_file =~ s/\%t/$id/g;
}
}
$output_file =~ s@\\@@g;
$output_file =~ s@"@@g;
@filename_args = ("-f", '"' . $output_file . '"');
}
else {
@filename_args = ("-f", "plex");
}
push(@capto_cmd, @capto_args, @filename_args);
print("Retrieving item '$id' via command '@capto_cmd'\n");
warn("([" . join("],[", @capto_cmd) . "]" # comment out for production
. "\n") if $verbose;
if ($get && ! $test) {
my $rv = system(@capto_cmd);
if ($rv) {
warn("Failed to retrieve $id: $!") unless $quiet;
$fail++;
}
else {
warn("Retrieved '$id' OK\n") unless $quiet;
$success++;
}
}
}
}
}
print("Finished " . @episode_list
. " job" . (@episode_list == 1 ? "" : "s")
. " in " . (time-$start) . " sec: "
. $success . " file" . ($success == 1 ? "" : "s")
. " transferred OK, $fail "
. "file" . ($fail == 1 ? "" : "s")
. " failed.\n") unless $quiet;
exit;
sub usage {
return qq~wrapto version $VERSION\n\nUsage:
# Fetch a list of episode IDs found with a previous capto -i command:
wrapto [--verbose] [--get | --test] 555555 666666 777777
# Fetch a numerical range of episodes
wrapto [--verbose] [--get | --test] 555555-555566
# Fetch a range of episodes of a specific show name:
wrapto [--verbose] [--get | --test] --name "Program Name" --range "s01e01-s04e16"
# Fetch all recorded episodes of a specific show name:
wrapto [--verbose] [--get | --test] --name "Program Name" --all
# Fetch everything on the Tablo and save using a custom filename template
wrapto [--verbose] [--get | --test] --all --filename "\%n - S\%sE\%e - \%t.mp4"
# List all options:
wrapto --help
~; #
}
sub show_help {
my %options = (
"verbose" => "Toggle verbose messaging. Defaults to false.",
"get" => "Tell wrapto to fetch episodes matched with --name, --range "
. "and/or --all. Cannot be combined with --test. Defaults to false.",
"test" => "Tell wrapto to go through the motions of searching for episodes "
. "and preparing a download list, but don't actually download any files. "
. "Cannot be combined with --get. Defaults to false.",
"range" => "Specify the range of episodes to download in the format sAAeXX-sBBeYY. "
. "For example, to download all of season 1 might be s01e01-s01e22, and to "
. "download from the middle of season 2 through season 3 might be s02e13-s03e22. "
. "Used in conjunction with --name",
"name" => "The canonical name of the series to be recorded, as provided from the "
. "output of a `capto -s \"some series name\"` command. Used in conjunction with "
. "--range or --all",
"help" => "Show this help text and exit",
"update" => "Tell wrapto to do a capto -u to update the cache before running the query.",
"filename" => "Format filenames using template variables like:\n"
. "\%D - datetime (YYYY-MM-DD HH:MM)\n"
. "\%d - date (YYYY-MM-DD)\n"
. "\%i - Tablo file ID\n"
. "\%s - Season number (zero-padding intact)\n"
. "\%e - Episode number (zero-padding intact)\n"
. "\%n - Show name\n"
. "\%t - Episode title\n"
. "For example: --filename \"\%n - S\%sE\%e - \%t.mp4\" would produce output like '"
. "The Simpsons - S30E19 - Girl's in the Band.mp4'\n",
"all" => "Fetch all episodes of a given show, or all content on the Tablo. "
. "Used by itself or with something like --name",
"capto" => "If supplied wrapto will use this path to capto. Default is ./ "
. "(current directory). Note: don't enclose the path in quotes, for some reason.",
"capto-args" => "If supplied, this string will be passed to capto as arguments "
. "on the commandline. For example --capto_args \"-ffmpeg /path/to/ffmpeg\" "
. "would allow you to run capto with a specific ffmpeg version per its own "
. "internal capabilities.",
);
my $help_str = "";
my $maxlen = 0;
foreach my $opt(keys %options) { $maxlen = length($opt) if length($opt) > $maxlen; }
my $spacestr = " "x($maxlen+3);
my $maxtext = 80 - $maxlen;
foreach my $opt (sort keys %options) {
my $optlines = "";
my @lines = split(/\r?\n/, $options{$opt});
foreach my $line (@lines) {
my $currline = "";
my @words = split(/\s+/, $line);
foreach my $word (@words) {
if (length($currline) + length($word) > $maxtext) {
$optlines .= "\n" . $spacestr . $currline;
$currline = "";
}
$currline .= $word . " ";
}
$optlines .= "\n" . $spacestr . $currline;
}
$help_str .= "--$opt" . " "x(($maxlen - length($opt))+1) . $optlines . "\n";
}
return usage() . "\n" . '#'x80 . "\n# Options:\n" . '#'x80 . "\n\n" . $help_str . "\n";
}