Scanning Plex Library for Missing TV Episodes

UPDATE: This is old and I’m guessing it doesn’t work anymore? I’m now all about PowerShell so I re-wrote it in PowerShell…. https://github.com/MysticRyuujin/PlexMissingEpisodes

The following Perl script will scan for every episode of all TV in your Plex server library (you have to specify what one) and then tell you what episodes you are missing for the shows in that library. It ignores Specials (S00Exx) and it ignores un-aired episodes.

You need to fill in the variables for your TheTVDB API Key, your Plex Server Name and Plex port (usually 32400). You also need to open your browser and go to:
http://$plexserver:$plexport/library/sections/

Then located the section number of the TV library you want to scan. Mine looks like this, and the $sectionid would be ‘2’:

<Directory allowSync="0" art="/:/resources/show-fanart.jpg" composite="/library/sections/2/composite/1439778056" filters="1" refreshing="1" thumb="/:/resources/show.png" key="2" type="show" title="TV Shows" agent="com.plexapp.agents.thetvdb" scanner="Plex Series Scanner" language="en" uuid="18a9bbb7-0a81-4b23-874b-1c433771efbd" updatedAt="1439778056" createdAt="1434676205">
<Location id="2" path="/media/FS01/TV"/>
use strict;
use warnings;
use XML::Simple;
use LWP::Simple;
use Archive::Zip;
use Archive::Zip::MemberRead;
use Encode qw( encode );
use DateTime;
use DateTime::Format::Strptime qw( );
use Sort::Naturally;

$|=1;
my $dt = DateTime->now;

my $apiKey = '';
my $plexserver = 'plex'; # IP works too
my $plexport = '32400';
my $sectionid = '2';

my ($xmlmirror, $bannermirror, $zipmirror) = &TVDB_GetMirrors();

my @plexseries = &PLEX_GetAllSeries();
my @plexguids;
foreach my $series (@plexseries)
{
	push(@plexguids, &PLEX_GetSeriesGUID($series));
}

my %myepisodes;

for (my $i = 0; $i < @plexguids; $i++)
{
	$myepisodes{$plexguids[$i]} = &PLEX_MyEpisodes($plexseries[$i]);
}

my %allepisodes;

for (my $i = 0; $i < @plexguids; $i++) { $allepisodes{$plexguids[$i]} = &TVDB_GetAllEpisodes($plexguids[$i]); } my %missing; foreach my $pkey (keys %allepisodes) { foreach my $key (keys $allepisodes{$pkey}) { if (!defined($myepisodes{$pkey}{$key})) { if ($key !~ /S00E/) { $missing{"$myepisodes{$pkey}{'title'} - $key"} = 1; } } } } foreach my $missing (nsort keys %missing) { print "$missingn"; } sub PLEX_MyEpisodes() { my $seriesid = $_[0]; my $dom = &getDom('http://'.$plexserver.':'.$plexport.'/library/metadata/'.$_[0].'/allLeaves'); my %episodes; $episodes{'title'} = $dom->{'parentTitle'};
	foreach my $key (keys $dom->{'Video'})
	{
		$episodes{sprintf ("S%02dE%02d", $dom->{'Video'}->{$key}->{'parentIndex'}, $dom->{'Video'}->{$key}->{'index'})} = 1;
	}
	return %episodes;
}

sub PLEX_GetSeriesGUID()
{
	my $dom = &getDom('http://'.$plexserver.':'.$plexport.'/library/metadata/'.$_[0].'/');
	my $guid = $1 if ($dom->{'Directory'}->{'/library/metadata/'.$_[0].'/children'}->{'guid'} =~ m#//(d+)?#);
	return $guid;
}

sub PLEX_GetAllSeries()
{
	my $dom = &getDom('http://'.$plexserver.':'.$plexport.'/library/sections/'.$sectionid.'/all/');
	my @series;
	foreach my $key (keys $dom->{'Directory'})
	{
		push(@series, $dom->{'Directory'}->{$key}->{'ratingKey'});
	}
	return @series;
}

sub getDom()
{
	my $url = $_[0];
	my $data = get($url);
	my $parser = new XML::Simple;
	return $parser->XMLin(encode("UTF-8", $data), ForceArray => 1);
}

sub TVDB_GetMirrors()
{
	my $mirrors_url = 'http://thetvdb.com/api/' . $apiKey . '/mirrors.xml';
	my $dom = &getDom($mirrors_url);
	
	my @xmlmirrors;
	my @bannermirrors;
	my @zipmirrors;

	foreach my $mirror (@{$dom->{'Mirror'}})
	{
		if ($mirror->{'typemask'}[0] & (1<<0)) { push(@xmlmirrors, $mirror->{'mirrorpath'}[0]);
		}
		if ($mirror->{'typemask'}[0] & (1<<1)) { push(@bannermirrors, $mirror->{'mirrorpath'}[0]);
		}
		if ($mirror->{'typemask'}[0] & (1<<2)) { push(@zipmirrors, $mirror->{'mirrorpath'}[0]);
		}
	}
	return ($xmlmirrors[rand(@xmlmirrors)], $bannermirrors[rand(@bannermirrors)], $zipmirrors[rand(@zipmirrors)]);
}

sub TVDB_GetAllEpisodes()
{
	my $seriesid = $_[0];
	my $url = $zipmirror . '/api/' . $apiKey . '/series/' . $seriesid . '/all/en.zip';
	my $zipname = "$seriesid" . '_en.zip';
	getstore($url, $seriesid . '_en.zip') unless ((-e $zipname) or (defined($_[1]) and ($_[1] eq 1)));
	my $zip = Archive::Zip->new();
	my $status = $zip->read($zipname);
	my $file = Archive::Zip::MemberRead->new($zip, "en.xml");
	my $xml;
	while (defined(my $line = $file->getline()))
	{
		$xml .= $line;
	}
	my $parser = new XML::Simple;
	my $dom = $parser->XMLin(encode("UTF-8", $xml), ForceArray => 1);
	
	my %episodes;
	my $format = DateTime::Format::Strptime->new(
		pattern   => '%Y-%m-%d',
		time_zone => 'local',
		on_error  => 'croak',
	);
	foreach my $episode (@{$dom->{'Episode'}})
	{
		$episode->{'FirstAired'}[0] = "3000-01-01" if (ref($episode->{'FirstAired'}[0]) eq "HASH");
		my $airdate = $format->parse_datetime($episode->{'FirstAired'}[0]);
		if (DateTime->compare($dt, $airdate) == 1)
		{
			$episodes{sprintf("S%02dE%02d", $episode->{'SeasonNumber'}[0], $episode->{'EpisodeNumber'}[0])} = 1;
		}
		
	}
	return %episodes;
}