From 0430dba6a93d8ed514f65c2d87d37c9f9592d3fd Mon Sep 17 00:00:00 2001 From: Brock H Caldwell Date: Fri, 1 Aug 2025 21:34:03 -0500 Subject: [PATCH] fix: bad PTN parsing causing monitors of episodes with 3 digit episodes to download multiple times --- .../Framework/Controller/IndexController.php | 10 + src/Base/Service/MediaFiles.php | 2 +- src/Base/Util/PTN.php | 246 ++++++++++++++++++ .../Action/Handler/DownloadSeasonHandler.php | 2 +- src/Download/Framework/Entity/Download.php | 2 +- .../Repository/DownloadRepository.php | 2 +- .../Action/Handler/LibrarySearchHandler.php | 2 +- .../Action/Handler/MonitorTvSeasonHandler.php | 2 +- .../Action/Handler/MonitorTvShowHandler.php | 2 +- src/Torrentio/Result/ResultFactory.php | 2 +- 10 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 src/Base/Util/PTN.php diff --git a/src/Base/Framework/Controller/IndexController.php b/src/Base/Framework/Controller/IndexController.php index 16b9292..1fe8fcf 100644 --- a/src/Base/Framework/Controller/IndexController.php +++ b/src/Base/Framework/Controller/IndexController.php @@ -2,6 +2,7 @@ namespace App\Base\Framework\Controller; +use App\Monitor\Action\Command\MonitorTvShowCommand; use App\Monitor\Action\Handler\MonitorTvShowHandler; use App\Tmdb\Tmdb; use App\User\Framework\Entity\User; @@ -48,4 +49,13 @@ final class IndexController extends AbstractController 'message' => 'Email sent!' ]); } + + #[Route('/test')] + public function monitorTvShow(): Response + { + $this->monitorTvShowHandler->handle(new MonitorTvShowCommand(96)); + return $this->json([ + 'Success' => 'Monitor added' + ]); + } } diff --git a/src/Base/Service/MediaFiles.php b/src/Base/Service/MediaFiles.php index 7a81e2a..bdb16d4 100644 --- a/src/Base/Service/MediaFiles.php +++ b/src/Base/Service/MediaFiles.php @@ -4,7 +4,7 @@ namespace App\Base\Service; use Aimeos\Map; use App\Download\Framework\Entity\Download; -use Nihilarr\PTN; +use App\Base\Util\PTN; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Filesystem\Filesystem; diff --git a/src/Base/Util/PTN.php b/src/Base/Util/PTN.php new file mode 100644 index 0000000..ad62aac --- /dev/null +++ b/src/Base/Util/PTN.php @@ -0,0 +1,246 @@ + '(s?([0-9]{1,3}))[ex]'), + array('episode' => '([ex]([0-9]{1,3})(?:[^0-9]|$))'), + array('year' => '([\[\(]?((?:19[0-9]|20[01])[0-9])[\]\)]?)'), + array('resolution' => '([0-9]{3,4}p)'), + array('quality' => '((?:PPV\.)?[HP]DTV|(?:HD)?CAM|B[DR]Rip|(?:HD-?)?TS|(?:PPV )?WEB-?DL(?: DVDRip)?|HDRip|DVDRip|DVDRIP|CamRip|W[EB]BRip|BluRay|DvDScr|hdtv|telesync)'), + array('codec' => '(xvid|[hx]\.?26[45])'), + array('audio' => '(MP3|DD5\.?1|Dual[\- ]Audio|LiNE|DTS|AAC[.-]LC|AAC(?:\.?2\.0)?|AC3(?:\.5\.1)?)'), + array('group' => '(- ?([^-]+(?:-={[^-]+-?$)?))$'), + array('region' => 'R[0-9]'), + array('extended' => '(EXTENDED(:?.CUT)?)'), + array('hardcoded' => 'HC'), + array('proper' => 'PROPER'), + array('repack' => 'REPACK'), + array('container' => '(MKV|AVI|MP4)'), + array('widescreen' => 'WS'), + array('website' => '^(\[ ?([^\]]+?) ?\])'), + array('language' => '(rus\.eng|ita\.eng)'), + array('sbs' => '(?:Half-)?SBS'), + array('unrated' => 'UNRATED'), + array('size' => '(\d+(?:\.\d+)?(?:GB|MB))'), + array('3d' => '3D') + ); + + public $types = array( + 'season' => 'integer', + 'episode' => 'integer', + 'year' => 'integer', + 'extended' => 'boolean', + 'hardcoded' => 'boolean', + 'proper' => 'boolean', + 'repack' => 'boolean', + 'widescreen' => 'boolean', + 'unrated' => 'boolean', + '3d' => 'boolean' + ); + + public function __construct() {} + + public function parse($name) { + $this->parts = array(); + $this->torrent = array('name' => $name); + $this->excess_raw = $name; + $this->group_raw = ''; + $this->start = 0; + $this->end = null; + $this->title_raw = null; + + foreach($this->patterns as $patterns_single) { + foreach($patterns_single as $key => $pattern) { + if(!in_array($key, array('season', 'episode', 'website'))) { + $pattern = "\b{$pattern}\b"; + } + + $clean_name = str_replace('_', ' ', $this->torrent['name']); + if(preg_match("/{$pattern}/i", $clean_name, $match) == 0) break; + + $index = array(); + if(is_array($match)) { + array_shift($match); + } + if(sizeof($match) == 0) break; + if(sizeof($match) > 1) { + $index['raw'] = 0; + $index['clean'] = 1; + } + else { + $index['raw'] = 0; + $index['clean'] = 0; + } + + if(isset($this->types[$key]) && $this->types[$key] == 'boolean') { + $clean = true; + } + else { + $clean = $match[$index['clean']]; + if(isset($this->types[$key]) && $this->types[$key] == 'integer') { + $clean = (int)$clean; + } + } + + if($key == 'group') { + if((isset($this->patterns[5][1]) && preg_match_all("/{$this->patterns[5][1]}/i", $clean)) || + (isset($this->patterns[4][1]) && preg_match_all("/{$this->patterns[4][1]}/", $clean))) { + break; + } + if(preg_match('/[^ ]+ [^ ]+ .+/', $clean)) { + $key = 'episodeName'; + } + } + if($key == 'episode') { + $sub_pattern = $this->escape_regex($match[$index['raw']]); + $this->torrent['map'] = preg_replace("/{$sub_pattern}/", '{episode}', $this->torrent['name']); + } + + $this->part($key, $match, $match[$index['raw']], $clean); + } + } + + $raw = $this->torrent['name']; + if(!is_null($this->end)) { + $raw = explode('(', substr($raw, $this->start, $this->end - $this->start)); + $raw = $raw[0]; + } + + $clean = preg_replace("/^ -/", '', $raw); + if(strpos($clean, ' ') === false && strpos($clean, '.') !== false) { + $clean = str_replace('.', ' ', $clean); + } + $clean = str_replace('_', ' ', $clean); + $clean = trim(preg_replace("/([\[\(_]|- )$/", '', $clean)); + + $this->part('title', array(), $raw, $clean); + + $clean = preg_replace("/(^[-\. ()]+)|([-\. ]+$)/", '', $this->excess_raw); + $clean = preg_replace("/[\(\)\/]/", ' ', $clean); + $match = preg_split("/\.\.+| +/", $clean); + if(sizeof($match) > 0 && is_array($match[0])) { + $match = $match[0]; + } + + $clean = $match; + $clean = array_filter($clean, function($var) { + return $var != '-' ? true : false; + }); + $clean = array_filter($clean, function($var) { + return trim($var, '-'); + }); + $clean = array_values($clean); + + if(sizeof($clean) > 0) { + $group_pattern = $clean[sizeof($clean) - 1] . $this->group_raw; + if(strpos($this->torrent['name'], $group_pattern) == strlen($this->torrent['name']) - strlen($group_pattern)) { + $this->late('group', array_pop($clean) . $this->group_raw); + } + + if(isset($this->torrent['map']) && sizeof($clean) > 0) { + $episode_name_pattern = '{episode}' . preg_replace("/_+$/", '', $clean[0]); + + if(strpos($this->torrent['map'], $episode_name_pattern) != -1) { + $this->late('episodeName', array_shift($clean)); + } + } + } + + if(sizeof($clean) != 0) { + if(sizeof($clean) == 1) { + $clean = $clean[0]; + } + $this->part('excess', array(), $this->excess_raw, $clean); + } + return $this->parts; + } + + private function escape_regex($subject) { + return preg_replace("/[\-\[\]{}()*+?.,\\\^$|#\s]/", "\\\\$&", $subject); + } + + private function part($name, $match, $raw, $clean) { + # The main core instructuions + $this->parts[$name] = $clean; + + if(sizeof($match) > 0) { + # The instructions for extracting title + $index = strpos($this->torrent['name'], $match[0]); + if($index == 0) { + $this->start = strlen($match[0]); + } + elseif(is_null($this->end) || $index < $this->end) { + $this->end = $index; + } + } + if($name != 'excess') { + if($name == 'group') { + $this->group_raw = $raw; + } + + if(!is_null($raw)) { + $this->excess_raw = str_replace($raw, '', $this->excess_raw); + } + } + } + + private function late($name, $clean) { + if($name == 'group') { + $this->part($name, array(), null, $clean); + } + elseif($name == 'episodeName') { + $clean = preg_replace("/[\._]/", ' ', $clean); + $clean = preg_replace("/_+$/", '', $clean); + $this->part($name, array(), null, trim($clean)); + } + } +} diff --git a/src/Download/Action/Handler/DownloadSeasonHandler.php b/src/Download/Action/Handler/DownloadSeasonHandler.php index cb04da9..63f6552 100644 --- a/src/Download/Action/Handler/DownloadSeasonHandler.php +++ b/src/Download/Action/Handler/DownloadSeasonHandler.php @@ -14,7 +14,7 @@ use App\Torrentio\Action\Command\GetTvShowOptionsCommand; use App\Torrentio\Action\Handler\GetTvShowOptionsHandler; use App\User\Dto\UserPreferencesFactory; use App\User\Framework\Repository\UserRepository; -use Nihilarr\PTN; +use App\Base\Util\PTN; use OneToMany\RichBundle\Contract\CommandInterface; use OneToMany\RichBundle\Contract\HandlerInterface; use OneToMany\RichBundle\Contract\ResultInterface; diff --git a/src/Download/Framework/Entity/Download.php b/src/Download/Framework/Entity/Download.php index cb3231c..c939e7f 100644 --- a/src/Download/Framework/Entity/Download.php +++ b/src/Download/Framework/Entity/Download.php @@ -6,7 +6,7 @@ use App\Download\Framework\Repository\DownloadRepository; use App\User\Framework\Entity\User; use Doctrine\ORM\Mapping as ORM; use Gedmo\Timestampable\Traits\TimestampableEntity; -use Nihilarr\PTN; +use App\Base\Util\PTN; use Symfony\Component\Serializer\Attribute\Ignore; use Symfony\UX\Turbo\Attribute\Broadcast; diff --git a/src/Download/Framework/Repository/DownloadRepository.php b/src/Download/Framework/Repository/DownloadRepository.php index b0a4610..b796624 100644 --- a/src/Download/Framework/Repository/DownloadRepository.php +++ b/src/Download/Framework/Repository/DownloadRepository.php @@ -7,7 +7,7 @@ use App\Download\Framework\Entity\Download; use App\User\Framework\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; -use Nihilarr\PTN; +use App\Base\Util\PTN; use Symfony\Component\Security\Core\User\UserInterface; /** diff --git a/src/Library/Action/Handler/LibrarySearchHandler.php b/src/Library/Action/Handler/LibrarySearchHandler.php index 0c918e3..9048683 100644 --- a/src/Library/Action/Handler/LibrarySearchHandler.php +++ b/src/Library/Action/Handler/LibrarySearchHandler.php @@ -6,7 +6,7 @@ use App\Base\Service\MediaFiles; use App\Library\Action\Command\LibrarySearchCommand; use App\Library\Action\Result\LibrarySearchResult; use App\Library\Dto\MediaFileDto; -use Nihilarr\PTN; +use App\Base\Util\PTN; use OneToMany\RichBundle\Contract\CommandInterface; use OneToMany\RichBundle\Contract\HandlerInterface; use OneToMany\RichBundle\Contract\ResultInterface; diff --git a/src/Monitor/Action/Handler/MonitorTvSeasonHandler.php b/src/Monitor/Action/Handler/MonitorTvSeasonHandler.php index c7b698a..f0369e4 100644 --- a/src/Monitor/Action/Handler/MonitorTvSeasonHandler.php +++ b/src/Monitor/Action/Handler/MonitorTvSeasonHandler.php @@ -12,7 +12,7 @@ use App\Monitor\Framework\Repository\MonitorRepository; use App\Tmdb\Tmdb; use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; -use Nihilarr\PTN; +use App\Base\Util\PTN; use OneToMany\RichBundle\Contract\CommandInterface; use OneToMany\RichBundle\Contract\HandlerInterface; use OneToMany\RichBundle\Contract\ResultInterface; diff --git a/src/Monitor/Action/Handler/MonitorTvShowHandler.php b/src/Monitor/Action/Handler/MonitorTvShowHandler.php index cec13ec..62c0d6b 100644 --- a/src/Monitor/Action/Handler/MonitorTvShowHandler.php +++ b/src/Monitor/Action/Handler/MonitorTvShowHandler.php @@ -13,7 +13,7 @@ use App\Tmdb\Tmdb; use Carbon\Carbon; use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; -use Nihilarr\PTN; +use App\Base\Util\PTN; use OneToMany\RichBundle\Contract\CommandInterface; use OneToMany\RichBundle\Contract\HandlerInterface; use OneToMany\RichBundle\Contract\ResultInterface; diff --git a/src/Torrentio/Result/ResultFactory.php b/src/Torrentio/Result/ResultFactory.php index 44bd8ab..d877219 100644 --- a/src/Torrentio/Result/ResultFactory.php +++ b/src/Torrentio/Result/ResultFactory.php @@ -3,7 +3,7 @@ namespace App\Torrentio\Result; use App\User\Database\CountryLanguages; -use Nihilarr\PTN; +use App\Base\Util\PTN; class ResultFactory {