Wrapto: Wrapper for Capto for Tablo


#21

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";
}

#22

Looks like you’ve got something great! I like --capto-args to pass on the delete when done option.

It’s not really not easy for me. Before tablo I used my pc with tv cards to ‘dvr’ OTA programs, and had control over everything. The software became outdated. With streaming services, DVB wasn’t getting much support.

I wasn’t real happy with the way tablo worked, but it did what I needed and the only problem was… well it didn’t work the way I wanted it to.

Then I discovered this create_mp4_from_tablo_directory.sh and had a way to get shows off the device, not just to off load them but to use a decent media player. (and watching network traffic in the browser)

capto cleaned things up giving me an easy way to get names to match the id’s. So I just -s ALL > text.file and quick edits and sent it to my script. Yea, it was a bit tedious, but I only have a handful of shows here and there. But it just bothered me having the long filenames with the meaningless episode titles. OCD is a battle.

I just ran wrapto --capto-args "-a" --filename "%n - s%se%e.mp4" --name "Criminal Minds" --all --get and gave me exactly what I need to the universe from imploding.

  • THANKS GREATLY!

#23

Just keeps getting better - thanks! A small batch of shows may not be sequential id’s. Perhaps crude, I have a quick and dirty shell scrip to loop via for in through some random id’s passed to it.

#!/bin/bash
set -x
for id in $1
do
	wrapto --capto-args "-a" --filename "%n - s%se%e.mp4" --get $id
done

Your need to wrap the id’s in quotes to pass it to a single variable $1.
~$ wraptoscript "1234 5678 9012 3456"
and, of course, even it capto has a quoted filename passed to it with an ’ in it… it’s logic removes it.
I’m sure it was never in doubt, of course you can specify a complete path for --filename (because I can’t use %n for the directory… spaces, The or extraneous characters in show names will cause problems I can’t explain.

Thanks

@wbill3d and @tgwaste for all the over-the-top work


#24

Example of sorting and no need to quote the numbers:

#!/bin/bash

for id in `(for i in $@; do echo $i; done) | sort -n`
do
        echo $id
done

./t.sh 9 2761 23 4 1
1
4
9
23
2761

#25

Out of curiosity why not just use --name and --range together and skip the tablo file IDs altogether, like wrapto --name "The Showname" --range "s02e03-s02e05" ?

wrapto won’t care whether the file IDs are sequential or not, it’s matching the text in the string that capto sends back with the season and episode info in it.


#26

:boom: COOL :boom:

they don’t really need sorted, I mentioned sequential because wrapto can get a range of id’s [10101-10115] (or season/episode) - I don’t always have.
Now, this is just makes it that much less hassle. :smiley:


#27

They are different show names. I have 4 different show I want. Different seasons from each other, who-knows what episode and no relational order to id’s At least I don’t want shows sequential between them to specify a range, that’s why I mentioned show “sequential”.

# Fetch a list of episode IDs found with a previous capto -i command:
  wrapto [--verbose] [--get | --test] 555555 666666 777777

This help line suggest wrapto can get multiple single episodes, although capto -i still gives me tablo info.
but with the script, I have the options --capto-args "-a" --filename "/path/%n - s%se%e.mp4" in place, so I just pass the id’s to it. For specific shows, I use --name, this just for a handful of “random” shows.

sorry for the misunderstanding.


#28

Ah, I see.

Yes, if there’s more than one show involved you’d need to pass individual file IDs like you’re doing now, or else issue multiple wrapto --name "The Showname" --all commands to fetch everything for the given show name (assuming that’s what you want).


#29

In the event someday somebody comes back through these, and my little script means something… To just specify some id’s on the command line unquoted use $@ instead of $1

#!/bin/bash
# getshow
for id in $@
 do wrapto --capto-args "-a" --filename "%n - s%se%e.mp4" --get $id
done

~ $ getshow 1234 2345 3456 4567


#30

Hi there,
I guess I’m still a little fuzzy on why that script is necessary. I just did a quick capto -s ALL to my dvr and then picked three random show IDs from three different shows, and ran this wrapto command:

wrapto --capto-args "-a" --filename "%n - s%se%e.mp4" --test 1796172 1802881 1800558

Retrieving item '1796172' via command './capto -e 1796172 -a -f "Superstore - s04e13.mp4"'
Retrieving item '1802881' via command './capto -e 1802881 -a -f "Abby's - s01e01.mp4"'
Retrieving item '1800558' via command './capto -e 1800558 -a -f "The Simpsons - s30e19.mp4"'
Preformatted text Finished 3 jobs in 0 sec: 0 files transferred OK, 0 files failed.

So it would seem that if you knew the IDs in advance, wrapto should still do what you want… unless I’m still missing something.


#31

well, if nothing else it’s a lot shorter and this way I have all the parameters I know I’m going to use already there.

The loop, well, now I guess it’s unnecessary.

The line from the help

# Fetch a list of episode IDs found with a previous capto -i command:
  wrapto [--verbose] [--get | --test] 555555 666666 777777

I almost suggest it’s one line, yet

[capto] -i - get tablo info

It’s a bit ambivalent since capto -i doesn’t provid IDs never got to it, was getting