I wrote a small wrapper script that invokes capto (Capto for Tablo (CLI Grabber) - #30 by tgwaste) to retrieve content from a connected Tablo device. wrapto adds some useful functionality, like being able to download a range of episodes (even across multiple seasons), specify a file naming format, download all episodes of a given show, or even download all content from the Tablo at once.
wrapto version 1.1
Usage:
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
Options:
–all - Fetch all episodes of a given show, or all content on the Tablo. Used by itself or with --name
–filename - Format filenames using template variables like:
%D - datetime (YYYY-MM-DD HH:MM)
%d - date (YYYY-MM-DD)
%i - Tablo file ID
%s - Season number (zero-padding intact)
%e - Episode number (zero-padding intact)
%n - Show name
%t - Episode title
For example: --filename “%n - S%sE%e - %t.mp4” would produce output like ‘The Simpsons - S30E19 - Girl’s in the Band.mp4’
–get - Tell wrapto to fetch episodes matched with --name, --range, and/or --all. Cannot be combined with --test. Defaults to false.
–help - Show this help text and exit
–name - The canonical name of the series to be recorded, as provided from the output of acapto -s "some series name"
command.
Used in conjunction with --range or --all
–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
–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.
–update - Tell wrapto to do a capto -u to update the cache before running the query.
–verbose - Toggle verbose messaging. Defaults to false.
Here’s the source code. As wrapto is written in Perl, you’ll need to drop it into the same directory as your capto installation, save the file and mark it as executable. It doesn’t require any Perl modules that don’t already come with the default installation. Generally speaking, if capto works for you, then wrapto should too!
#!/usr/bin/perl -w
use strict; use warnings;
use Getopt::Long;
use Data::Dumper;
my $VERSION = "1.1";
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 = ();
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,
);
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;
my $capto = "./capto";
my @default_options = ("-f", "plex");
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 = `$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, time => $2, id => $3, showname => $4,
season => $5, episode => $6, 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");
}
}
}
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) {
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;
$output_file =~ s/\%t/\Q$all_list{$id}->{epname}\E/g;
push(@capto_cmd, "-f", $output_file);
}
else { push(@capto_cmd, @default_options); }
warn("Retrieving item '$id' via command '@capto_cmd'\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.\n"
. "Cannot be combined with --get. Defaults to false.",
"range" => "Specify the range of episodes to download in the format sAAeXX-sBBeYY.\n"
. "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.\n"
. "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.\nUsed 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.\n"
. "Used by itself or with --name",
);
my $help_str = "";
my $maxlen = 0;
foreach my $opt(keys %options) { $maxlen = length($opt) if length($opt) > $maxlen; }
my $spacestr = " "x($maxlen+3) . "| ";
foreach my $opt (sort keys %options) {
$options{$opt} =~ s/\r?\n$//;
$options{$opt} =~ s/\r?\n/\n$spacestr/g;
$help_str .= "--$opt" . " "x(($maxlen - length($opt))+1) . "| " . $options{$opt} . "\n";
}
return usage() . "\n" . '#'x80 . "\n# Options:\n" . '#'x80 . "\n\n" . $help_str . "\n";
}