Wrapto: Wrapper for Capto for Tablo

I truly appreciate all the work you’ve put into wrapto! some notes…

I have capto in my path since it’s hard coded my $capto = "./capto"; to work specifically from current directory… you say to put it in the same as capto, not necessarily you have to work from there, suggest to verify, create link or edit wrapto.

Warning to others, I just copied and pasted your examples from above for trial (removing the [ | ] ) Probably due to the forum formatting, seems some of the – turned into one big dash and the goofy opening quote, you know, isn’t the same thing.

Finally got a command formatted to work…
1 wrapto --verbose --test --all --filename "%n - s%se%e.mp4"
2 wrapto --test --all --filename "%n - s%se%e.mp4"

1 seems to go through 2 loops starting with:

wrapto: Starting up with verbose='1', name='', range='', get='1', test='1', all='1',
Retrieving all content...

$VAR1 = ‘67830’;
$VAR2 = {
‘date’ => ‘2019-04-02’,
‘season’ => ‘06’,
‘showname’ => ‘Criminal Minds’,
‘epname’ => ‘The Longest Night’,
‘time’ => ‘20:00’,
‘id’ => ‘67830’,
‘episode’ => ‘01’
};

through each record. Then looping through lines appearing to be capto’s output

Retrieving item '48871' via command 'capto -e 48871 -f Rizzoli\ \&\ Isles - s06e10.mp4'

Subsequent runs I’ve notices the first loop is in a different random order, the second iteration is sequential.

The second command, without --verbose just went through the first loop, always in a different order.

Not sure if something got missed while coping the text. What am I missing? My perl,
~$ perl --version

This is perl 5, version 28, subversion 1 (v5.28.1) built for x86_64-linux-gnu-thread-multi
(with 61 registered patches, see perl -V for more detail)

Pardon my ignorance; but where exactly does this Perl script run from? Do you load it into the Tablo somehow, or does it run on your computer, or what?

Hi there,

First, thanks for the note about the double dash. I’ll try to make sure that future release notes show up properly on the board.

Second, with regard to the output of the first list that you see, I’ve been using a perl module to display an internal data structure for debugging. I think by default it doesn’t sort the output, which is why it appeared differently during each of your runs. I’m going to take it out in the next release since it doesn’t really serve a purpose anymore. Regardless, though, that second and final list should always come out in sorted order, which as you suggest it does.

Just so I’m clear, have you gotten it to work the way you wanted yet (the issue of not having it in the same directory as capto itself aside), and have you had any trouble getting it to actually fetch the files from your Tablo?

1 Like

Hi ACC,

This wrapper, called wrapto, works alongside another utility, called capto, to retrieve files from your Tablo DVR. You’d need to download both capto and wrapto to a Mac or Linux machine on the same LAN as your Tablo and execute the commands from there. People use utilities like these to batch-offload files from the DVR onto a home server for further processing (e.g. for commercial removal), or just to move them onto another storage medium so the Tablo has more space to record.

Ok. I am a Mac guy; but I have never touched Perl (LOL). That will work GREAT for me.

I assume I can just copy this Script into a Text Editor, then save it as [something].pl, right?

Where can I find Capto for Mac? Is it available as a MacOS Application.

Are you talking about THIS?

https://www.globaldelight.com/captoformac/

Sorry for the stupidity…

BTW, That’s a GREAT utility! I’m a moderate data-hoarder; so that will be perfect, once I get my Tablo Quad… :wink:

Capto can be downloaded from http://www.twg.org/capto/

The original forum thread about it is here: Capto for Tablo (CLI Grabber)

Capto can be used without wrapto. Wrapto just adds more functionality. You shouldn’t need to know any perl to use the utility. Just copy the source blurb into a text editor, save, and change the executable bit so you can run it like a program - from a terminal just do chmod 755 /wherever/you/put/wrapto

Thanks!

Since I don’t have a Tablo (or Capto) yet, is there any way I can “bookmark” your post with the pl script so I can find it again? :wink:

There should be a bookmark button on the bottom of this forum page. Just click that and it will be saved under your profile.

The order was benign, I just wondered what the code was

As for the directory path, I just deleted ./ from line 43 - non issue. The way I wanted things to work :stuck_out_tongue_winking_eye::roll_eyes: of course it’s never that easy…
I tried using wrapto and capto “out-of-the-box” to see what I can get. Seems to get close except spaces get escaped with backspaces when it passes %n to capto

Retrieving item '77925' via command 'capto -e 77925 -f The\ Orville - s02e12.mp4'

Here, in the debugging thing, showname is single quoted

$VAR35 = '77925';
$VAR36 = {
           'date' => '2019-04-11',
           'episode' => '12',
           'time' => '21:01',
           'showname' => 'The Orville',
           'id' => '77925',
           'epname' => 'Sanctuary',
           'season' => '02'
         };

and this command gives me a file with a \ in it

$ wrapto --verbose --get --filename "%n - s%se%e.mp4" 77925
$ stat The*
File: The\ Orville - s02e12.mp4

I would suspect that may come from the shell… but I don’t think it comes into play directly. Other than these TV shows, I generally avoid spaces in filenames, but never had issues with them otherwise. I get why the apostate and ampersand are escaped, and to some degree spaces, but why even periods. I recall @tgwaste did a bunch to get rid of illicit characters in filenames for capto.

Ok… trying to work out what I can do with wrapto, help with what’s wrong with this line:

$ wrapto --verbose --get --name "The Orville" --filename "%n - s%se%e.mp4"
wrapto: Starting up with verbose='1', name='The Orville', range='', get='1', test='0', all='0',
Finished 0 jobs in 0 sec: 0 files transferred OK, 0 files failed.

Just to confirm: as noted: --name | The canonical name of the series to be recorded, as provided from the output of a capto -s "some series name" command. (I think line 208 referencing capto -i may be a typo)

$ capto -s "The Orville"
[capto] [2019-04-11 21:01] (77925) The Orville - s02e12 - Sanctuary

So that name exsist and yet I get nothing that way :man_shrugging:
But please to understand, if wrapto works for you and others that’s awesome!! Don’t beat yourself up over this up if it’s not an issue moving froward collectively. My backstory isn’t this straight forward and I’ve gone on enough for now. :tired_face::sleeping:

You should quote file names with spaces.

Retrieving item ‘77925’ via command ‘capto -e 77925 -f “The Orville - s02e12.mp4”’

or if within quotes:

“This is a \“File Name.mp4\””

Absolutely! This is the output I get using wrapto’s variable for showname %n

When I use the --test command to list all the content with the --filename template the list has all the spaces escaped wrapto --verbose --test --all --filename "%n - s%se%e.mp4"

So, not really have much more than barely a clue I’m trying to see if I can get wrapto to quote the output. Fiddling with line 175 $output_file, building the @capto_cmd. Maybe it’s not even the right place, failed.

Then I realized wrapto should accept everything including " for --filename, but since they are necessary for the command… well I added them--filename "\"%n - s%se%e.mp4\"" escaping them with backslashes. Can you believe I got output with quoted filename - still with backslashes!

Retrieving item '77925' via command 'capto -e 77925 -f "The\ Orville - s02e12.mp4"'

So I’m SOL that’s just messed up… and then some.

ok, so being as stoopid as I am, I was smart enough not to edit my primary wrapto in my path… but not smart enough to specify ~/ when I trialed the edits from the test file… which explains why I never generated any errors! So who knows, maybe I had stumbled on something, but wasted my time and won’t know about it.
I did try
push(@capto_cmd, "-f", "\"", $output_file, "\""); not ideal, of cours it’s not the answer I get spaces, and still backslashes!? (but then again, I dont know what I’m doing)
'capto -e 77925 -f " The\ Orville - s02e12.mp4 "'

Hi djk,

I think I can reproduce what you’re seeing - my shell is set up a bit differently which is probably why I didn’t notice it before. Try this new release, which also adds --capto to allow you to specify a path to capto, as well as --capto-args that lets you pass specific arguments to capto itself:

#!/usr/bin/perl -w
use strict; use warnings;
use Getopt::Long;
# use Data::Dumper;

my $VERSION      = "1.2";

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
$range      =~ s/^['"]//g;
$range      =~ s/['"]$//g;
$name       =~ s/^['"]//g;
$name       =~ s/['"]$//g;
$filename   =~ s/^['"]//g;
$filename   =~ s/['"]$//g;
$capto      =~ s/^['"]//g;
$capto      =~ s/['"]$//g;
$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, 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;
				
				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);
			warn("Retrieving item '$id' via command '@capto_cmd' "
				. "([" . 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/special_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";
}

As always, wrapto --help gives all of the available options:

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 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/special_ffmpeg" would allow you to run capto with a specific 
             ffmpeg version per its own internal capabilities. 
--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 a `capto -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.

When I run it now in a totally stock bash shell, I get unescaped output file names, even when they contain strange characters:

usr@w550s:~/capto.v0.6.linux$ ./wrapto.pl --test --verbose --name "The Simpsons" --range "s30e18-s30e20" --filename "%n - s%se%e - %t.mp4" --capto ~/capto.v0.6.linux/capto --capto-args "-debug" 
wrapto: Starting up with verbose='1', name='The Simpsons', range='s30e18-s30e20', get='0', test='1', all='0',
Looking at episode range 's30e18-s30e20' for program 'The Simpsons'
Looking for episodes of 'The Simpsons' between s30e18 and s30e20
Found all possible episodes of 'The Simpsons':
[capto] [2019-03-24 20:00] (1792741) The Simpsons - s30e18 - Bart vs. Itchy & Scratchy
 [capto] [2019-03-31 20:00] (1800558) The Simpsons - s30e19 - Girl's in the Band
 [capto] [2019-04-07 20:00] (1808877) The Simpsons - s30e20 - I'm Just a Girl Who Can't Say D'oh

Adding episode #1792741 ([capto] [2019-03-24 20:00] (1792741) The Simpsons - s30e18 - Bart vs. Itchy & Scratchy)
Adding episode #1800558 ([capto] [2019-03-31 20:00] (1800558) The Simpsons - s30e19 - Girl's in the Band)
Adding episode #1808877 ([capto] [2019-04-07 20:00] (1808877) The Simpsons - s30e20 - I'm Just a Girl Who Can't Say D'oh)
Retrieving item '1792741' via command '/home/bill/capto.v0.6.linux/capto -e 1792741 -debug -f "The Simpsons - s30e18 - Bart vs. Itchy & Scratchy.mp4"' ([/home/bill/capto.v0.6.linux/capto],[-e],[1792741],[-debug],[-f],["The Simpsons - s30e18 - Bart vs. Itchy & Scratchy.mp4"]
Retrieving item '1800558' via command '/home/bill/capto.v0.6.linux/capto -e 1800558 -debug -f "The Simpsons - s30e19 - Girl's in the Band.mp4"' ([/home/bill/capto.v0.6.linux/capto],[-e],[1800558],[-debug],[-f],["The Simpsons - s30e19 - Girl's in the Band.mp4"]
Retrieving item '1808877' via command '/home/bill/capto.v0.6.linux/capto -e 1808877 -debug -f "The Simpsons - s30e20 - I'm Just a Girl Who Can't Say D'oh.mp4"' ([/home/bill/capto.v0.6.linux/capto],[-e],[1808877],[-debug],[-f],["The Simpsons - s30e20 - I'm Just a Girl Who Can't Say D'oh.mp4"]
Finished 3 jobs in 0 sec: 0 files transferred OK, 0 files failed.

Let me know if that solves the problem for you!

time constraints… Broke the major hurdle,

$ wrapto --verbose --get --filename "%n - s%se%e.mp4" 77925

yields a filename as expected (as far as I’m concerned)
I did get some messages

Use of uninitialized value $capto in substitution (s///) at /usr/local/sbin/wrapto line 46.
Use of uninitialized value $capto in substitution (s///) at /usr/local/sbin/wrapto line 47.

but again I deleted the ./ at line 59 since capto and wrapto are both in /usr/local/sbin

If I use wrapto --test --all without verbose should I see nothing? (never did before well, Finished message) note: capto -i = “get tablo info”

Beyond recognizing a perl regex when I see it, and the most basic rudimentary, match this, function… I always knew it was something to understand. Thanks for everything you and tgwast (and other 3rd party apps) put into this!

oh yeah, time constraints…

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

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!

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

1 Like

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
1 Like

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.

: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: