diff --git a/app/Console/Commands/NntmuxPopulateSteamApps.php b/app/Console/Commands/NntmuxPopulateSteamApps.php index ba1928631..03ce93594 100644 --- a/app/Console/Commands/NntmuxPopulateSteamApps.php +++ b/app/Console/Commands/NntmuxPopulateSteamApps.php @@ -40,9 +40,9 @@ class NntmuxPopulateSteamApps extends Command $this->info(sprintf( 'Added %d new steam app(s), %d skipped, %d errors', - $stats['inserted'], // @phpstan-ignore offsetAccess.notFound - $stats['skipped'], // @phpstan-ignore offsetAccess.notFound - $stats['errors'] // @phpstan-ignore offsetAccess.notFound + $stats['inserted'], + $stats['skipped'], + $stats['errors'] )); } catch (\Exception $e) { $this->error($e->getMessage()); diff --git a/app/Console/Commands/NntmuxSearchDiag.php b/app/Console/Commands/NntmuxSearchDiag.php index e3e49a4d2..356a9151a 100644 --- a/app/Console/Commands/NntmuxSearchDiag.php +++ b/app/Console/Commands/NntmuxSearchDiag.php @@ -125,7 +125,7 @@ class NntmuxSearchDiag extends Command // ['data' => [...]] shape is retained as a fallback. $rows = []; foreach ($response as $key => $value) { - if (\is_int($key) || (\is_string($key) && ctype_digit($key))) { + if (\is_int($key) || ctype_digit($key)) { if (\is_array($value)) { $row = $value; if (! isset($row['id'])) { diff --git a/app/Console/Commands/NntmuxSearchReconcile.php b/app/Console/Commands/NntmuxSearchReconcile.php index a4b2493b0..ba42eadbd 100644 --- a/app/Console/Commands/NntmuxSearchReconcile.php +++ b/app/Console/Commands/NntmuxSearchReconcile.php @@ -172,7 +172,7 @@ class NntmuxSearchReconcile extends Command // The old `$response['data']` shape only exists for non-raw mode or pre-4.x clients. foreach ($response as $key => $value) { // Top-level numeric keys mirror row ids (single-column id projection). - if (\is_int($key) || (\is_string($key) && ctype_digit($key))) { + if (\is_int($key) || ctype_digit($key)) { $id = (int) $key; if (isset($requested[$id])) { $out[$id] = $id; diff --git a/app/Http/Controllers/Admin/AdminPaymentController.php b/app/Http/Controllers/Admin/AdminPaymentController.php index 8bdb2eadb..1651a7c79 100644 --- a/app/Http/Controllers/Admin/AdminPaymentController.php +++ b/app/Http/Controllers/Admin/AdminPaymentController.php @@ -68,8 +68,8 @@ class AdminPaymentController extends BasePageController $summaryTotals = [ 'tx_count' => (int) $summary->sum('tx_count'), - 'invoice_total' => (float) $summary->sum(fn ($r) => (float) $r->invoice_total), - 'value_total' => (float) $summary->sum(fn ($r) => (float) $r->value_total), + 'invoice_total' => (float) $summary->sum(fn (Payment $payment) => (float) $payment->getAttribute('invoice_total')), + 'value_total' => (float) $summary->sum(fn (Payment $payment) => (float) $payment->getAttribute('value_total')), ]; $paymentStatuses = BtcPaymentController::paymentStatusesForAdminFilter(); diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index f36e70b7b..54c96d62b 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -101,7 +101,7 @@ class RegisterController extends Controller 'email', 'max:255', Rule::unique('users', 'email')->where(fn ($query) => $query->whereNull('deleted_at')), - new ValidEmailDomain, + app(ValidEmailDomain::class), ], 'password' => ['required', 'confirmed', Password::min(8)->letters()->mixedCase()->numbers()->symbols()->uncompromised()], ], [ diff --git a/app/Http/Controllers/BasePageController.php b/app/Http/Controllers/BasePageController.php index ed9ea09e9..c1655ea0f 100644 --- a/app/Http/Controllers/BasePageController.php +++ b/app/Http/Controllers/BasePageController.php @@ -20,7 +20,7 @@ class BasePageController extends Controller /** * @var Collection */ - public Collection $settings; // @phpstan-ignore property.phpDocType, class.notFound, missingType.generics + public Collection $settings; public string $title = ''; diff --git a/app/Http/Controllers/ContentController.php b/app/Http/Controllers/ContentController.php index a5bb65df9..610cf1750 100644 --- a/app/Http/Controllers/ContentController.php +++ b/app/Http/Controllers/ContentController.php @@ -87,9 +87,9 @@ class ContentController extends BasePageController * * @return Collection */ - protected function getActiveContent(): Collection // @phpstan-ignore class.notFound, missingType.generics, return.phpDocType + protected function getActiveContent(): Collection { - return Content::active()->ordered()->get(); // @phpstan-ignore method.notFound + return Content::active()->ordered()->get(); } /** @@ -97,11 +97,11 @@ class ContentController extends BasePageController * * @return Collection */ - protected function getAllButFront(): Collection // @phpstan-ignore class.notFound, missingType.generics, return.phpDocType + protected function getAllButFront(): Collection { return Content::query() ->where('id', '<>', 1) - ->ordered() // @phpstan-ignore method.notFound + ->ordered() ->get(); } @@ -121,7 +121,7 @@ class ContentController extends BasePageController * * @return Collection */ - protected function getFrontPageContent(): Collection // @phpstan-ignore class.notFound, missingType.generics, return.phpDocType + protected function getFrontPageContent(): Collection { return Content::frontPage()->get(); } @@ -133,7 +133,7 @@ class ContentController extends BasePageController { return Content::active() ->ofType(Content::TYPE_INDEX) - ->ordered() // @phpstan-ignore method.notFound + ->ordered() ->first(); } } diff --git a/app/Models/AnidbInfo.php b/app/Models/AnidbInfo.php index 4f226448e..321cfc712 100644 --- a/app/Models/AnidbInfo.php +++ b/app/Models/AnidbInfo.php @@ -142,6 +142,18 @@ class AnidbInfo extends Model return null; } + /** + * Get AniDB URL if anidbid exists. + */ + public function getAnidbUrl(): ?string + { + if (empty($this->anidbid)) { + return null; + } + + return 'https://anidb.net/anime/'.$this->anidbid; + } + /** * Get AniList URL if anilist_id exists. */ @@ -165,4 +177,34 @@ class AnidbInfo extends Model return 'https://myanimelist.net/anime/'.$this->mal_id; } + + /** + * Get MyAnimeList URL if mal_id exists. + */ + public function getMyAnimeListUrl(): ?string + { + return $this->getMalUrl(); + } + + /** + * Get available external links keyed by provider. + * + * @return array + */ + public function getExternalLinks(): array + { + return array_filter([ + 'anidb' => $this->getAnidbUrl(), + 'anilist' => $this->getAnilistUrl(), + 'myanimelist' => $this->getMyAnimeListUrl(), + ]); + } + + /** + * Check whether at least one external link exists. + */ + public function hasExternalLinks(): bool + { + return $this->getExternalLinks() !== []; + } } diff --git a/app/Models/Content.php b/app/Models/Content.php index ae59f5afc..a3cb0260c 100644 --- a/app/Models/Content.php +++ b/app/Models/Content.php @@ -143,7 +143,7 @@ class Content extends Model { return $query->active() // @phpstan-ignore method.notFound ->ofType(self::TYPE_INDEX) - ->ordered(); // @phpstan-ignore method.notFound + ->ordered(); } /** diff --git a/app/Models/ReleaseFile.php b/app/Models/ReleaseFile.php index 4f4f51a38..271038075 100644 --- a/app/Models/ReleaseFile.php +++ b/app/Models/ReleaseFile.php @@ -65,9 +65,9 @@ class ReleaseFile extends Model * @param Builder $query * @return Builder */ - public function scopeNfoFiles(Builder $query): Builder // @phpstan-ignore missingType.generics, return.phpDocType + public function scopeNfoFiles(Builder $query): Builder { - return $query->where(function (Builder $q) { // @phpstan-ignore missingType.generics + return $query->where(function (Builder $q) { $q->where('name', 'like', '%.nfo') ->orWhere('name', 'like', '%.diz') ->orWhere('name', 'like', '%.inf') @@ -82,7 +82,7 @@ class ReleaseFile extends Model * @param Builder $query * @return Builder */ - public function scopeNfoFilesWithContent(Builder $query): Builder // @phpstan-ignore missingType.generics, return.phpDocType + public function scopeNfoFilesWithContent(Builder $query): Builder { return $query->nfoFiles()->where('size', '>', 0); } diff --git a/app/Rules/ValidEmailDomain.php b/app/Rules/ValidEmailDomain.php index 04fd39abe..f4b9203d5 100644 --- a/app/Rules/ValidEmailDomain.php +++ b/app/Rules/ValidEmailDomain.php @@ -182,7 +182,7 @@ class ValidEmailDomain implements ValidationRule /** * Validate that the domain has proper DNS records */ - private function validateDnsRecords(string $domain): bool + protected function validateDnsRecords(string $domain): bool { // Check for MX records (primary email validation) if (@checkdnsrr($domain, 'MX')) { diff --git a/app/Services/Binaries/BinariesService.php b/app/Services/Binaries/BinariesService.php index 03c856aa6..da8326e15 100644 --- a/app/Services/Binaries/BinariesService.php +++ b/app/Services/Binaries/BinariesService.php @@ -63,7 +63,7 @@ class BinariesService private int $headersBlackListed = 0; /** - * @var array + * @var array */ private array $headersReceived = []; @@ -257,7 +257,7 @@ class BinariesService * @param int $first The oldest wanted header. * @param int $last The newest wanted header. * @param string $type Is this part repair or update or backfill? - * @param array|null $missingParts If we are running in part repair, the list of missing article numbers. + * @param array|null $missingParts If we are running in part repair, the list of missing article numbers. * @return array Empty on failure. * * @throws \Exception @@ -381,7 +381,7 @@ class BinariesService } // Calculate parts repaired - $lastPartNumber = $missingParts[$missingCount - 1]->numberid; // @phpstan-ignore offsetAccess.notFound + $lastPartNumber = $missingParts[$missingCount - 1]->numberid; $remainingCount = $this->missedPartHandler->getCount($groupArr['id'], $lastPartNumber); $partsRepaired = $missingCount - $remainingCount; @@ -693,7 +693,7 @@ class BinariesService } /** - * @return array + * @return array> */ private function downloadHeaders(bool $partRepair): ?array { @@ -736,8 +736,8 @@ class BinariesService } /** - * @param array $headersNotInserted - * @param array $parsedHeaders + * @param array $headersNotInserted + * @param array> $parsedHeaders */ private function handlePartRepairTracking(array $headersNotInserted, array $parsedHeaders): void { @@ -750,11 +750,11 @@ class BinariesService // Check for missing headers in range $expectedCount = $this->last - $this->first - $this->notYEnc - $this->headersBlackListed + 1; if ($expectedCount > \count($this->headersReceived)) { - $rangeNotReceived = array_diff(range($this->first, $this->last), $this->headersReceived); + $rangeNotReceived = array_values(array_diff(range($this->first, $this->last), $this->headersReceived)); $notReceivedCount = \count($rangeNotReceived); if ($notReceivedCount > 0) { - $this->missedPartHandler->addMissingParts($rangeNotReceived, $this->groupMySQL['id']); // @phpstan-ignore argument.type + $this->missedPartHandler->addMissingParts($rangeNotReceived, $this->groupMySQL['id']); if ($this->config->echoCli) { cli()->alternate( @@ -766,14 +766,14 @@ class BinariesService } /** - * @param array $missingParts - * @return array + * @param array $missingParts + * @return list}> */ private function groupMissingPartsIntoRanges(array $missingParts): array { $ranges = []; $partList = []; - $firstPart = $lastNum = $missingParts[0]->numberid; // @phpstan-ignore offsetAccess.notFound + $firstPart = $lastNum = $missingParts[0]->numberid; foreach ($missingParts as $part) { if (($part->numberid - $firstPart) > ($this->config->messageBuffer / 4)) { diff --git a/app/Services/Binaries/CollectionHandler.php b/app/Services/Binaries/CollectionHandler.php index 7551509e0..42d68188a 100644 --- a/app/Services/Binaries/CollectionHandler.php +++ b/app/Services/Binaries/CollectionHandler.php @@ -140,6 +140,7 @@ final class CollectionHandler * Resolve collections for a chunk of headers with one bulk insert and one id lookup. * * @param array> $headers + * @param array $totalFilesByIndex * @return array Collection ids keyed by header index */ public function getOrCreateCollections( diff --git a/app/Services/Binaries/HeaderParser.php b/app/Services/Binaries/HeaderParser.php index 4a038b013..34d57cab7 100644 --- a/app/Services/Binaries/HeaderParser.php +++ b/app/Services/Binaries/HeaderParser.php @@ -34,10 +34,10 @@ final class HeaderParser /** * Parse and filter raw headers from NNTP. * - * @param array $headers Raw headers from NNTP + * @param array> $headers Raw headers from NNTP * @param string $groupName The newsgroup name * @param bool $partRepair Whether this is a part repair scan - * @param array|null $missingParts Missing part numbers if part repair + * @param array|null $missingParts Missing part numbers if part repair * @return array Filtered and parsed headers with article info */ public function parse( @@ -137,7 +137,7 @@ final class HeaderParser /** * Extract highest and lowest article info from headers. * - * @param array $headers + * @param array> $headers * @return array */ public function getArticleRange(array $headers): array diff --git a/app/Services/Binaries/HeaderStorageService.php b/app/Services/Binaries/HeaderStorageService.php index a68857870..b3c3c14aa 100644 --- a/app/Services/Binaries/HeaderStorageService.php +++ b/app/Services/Binaries/HeaderStorageService.php @@ -20,7 +20,7 @@ final class HeaderStorageService private BinariesConfig $config; - /** @var array Article numbers that failed to insert */ + /** @var array Article numbers that failed to insert */ private array $failedInserts = []; public function __construct( @@ -41,10 +41,10 @@ final class HeaderStorageService /** * Store parsed headers to the database. * - * @param array $headers Parsed headers with 'matches' already populated + * @param array> $headers Parsed headers with 'matches' already populated * @param array $groupMySQL Group info from database * @param bool $addToPartRepair Whether to track failed inserts - * @return array Article numbers that failed to insert + * @return array Article numbers that failed to insert */ public function store(array $headers, array $groupMySQL, bool $addToPartRepair = true): array { @@ -77,7 +77,7 @@ final class HeaderStorageService /** * Store one bounded header chunk inside its own transaction. * - * @param array $headers + * @param array> $headers * @param array $groupMySQL */ private function storeChunk(array $headers, array $groupMySQL, bool $addToPartRepair): void @@ -87,16 +87,17 @@ final class HeaderStorageService $this->partHandler->reset(); $this->partHandler->setAddToPartRepair($addToPartRepair); - $chunkNumbers = array_values(array_filter(array_map( - static fn (array $header): mixed => $header['Number'] ?? null, - $headers - ))); + $chunkNumbers = []; + foreach ($headers as $header) { + if (isset($header['Number']) && (\is_int($header['Number']) || \is_string($header['Number']))) { + $chunkNumbers[] = $header['Number']; + } + } // Create transaction $transaction = new HeaderStorageTransaction( $this->collectionHandler, - $this->binaryHandler, - $this->partHandler + $this->binaryHandler ); $transaction->begin(); @@ -208,65 +209,11 @@ final class HeaderStorageService private function markHeaderFailed(array $header, HeaderStorageTransaction $transaction, bool $addToPartRepair): void { $transaction->markError(); - if ($addToPartRepair && isset($header['Number'])) { + if ($addToPartRepair && isset($header['Number']) && (\is_int($header['Number']) || \is_string($header['Number']))) { $this->failedInserts[] = $header['Number']; } } - /** - * @param array $groupMySQL - * @param array $header - */ - private function processHeader(array $header, array $groupMySQL, HeaderStorageTransaction $transaction): bool - { - // Get file count from subject - $fileCount = $this->getFileCount($header['matches'][1]); - if ($fileCount[1] === 0 && $fileCount[3] === 0) { - $fileCount = $this->getFileCount($header['matches'][0]); - } - - $totalFiles = (int) $fileCount[3]; - $fileNumber = (int) $fileCount[1]; - - // Get or create collection - $collectionId = $this->collectionHandler->getOrCreateCollection( - $header, - $groupMySQL['id'], - $groupMySQL['name'], - $totalFiles, - $transaction->getBatchNoise() - ); - - if ($collectionId === null) { - $transaction->markError(); - - return false; - } - - // Get or create binary - $binaryId = $this->binaryHandler->getOrCreateBinary( - $header, - $collectionId, - $groupMySQL['id'], - $fileNumber - ); - - if ($binaryId === null) { - $transaction->markError(); - - return false; - } - - // Add part - if (! $this->partHandler->addPart($binaryId, $header)) { - $transaction->markError(); - - return false; - } - - return true; - } - /** * @return array */ diff --git a/app/Services/Binaries/HeaderStorageTransaction.php b/app/Services/Binaries/HeaderStorageTransaction.php index 30a9c0b4a..a791c3e1c 100644 --- a/app/Services/Binaries/HeaderStorageTransaction.php +++ b/app/Services/Binaries/HeaderStorageTransaction.php @@ -24,8 +24,7 @@ final class HeaderStorageTransaction public function __construct( CollectionHandler $collectionHandler, - BinaryHandler $binaryHandler, - PartHandler $partHandler + BinaryHandler $binaryHandler ) { $this->collectionHandler = $collectionHandler; $this->binaryHandler = $binaryHandler; diff --git a/app/Services/Binaries/MissedPartHandler.php b/app/Services/Binaries/MissedPartHandler.php index d58c48685..eff298b71 100644 --- a/app/Services/Binaries/MissedPartHandler.php +++ b/app/Services/Binaries/MissedPartHandler.php @@ -29,7 +29,7 @@ final class MissedPartHandler /** * Add missing article numbers to the repair queue. * - * @param array $numbers + * @param array $numbers */ public function addMissingParts(array $numbers, int $groupId): void { @@ -49,7 +49,7 @@ final class MissedPartHandler } /** - * @param array $numbers + * @param array $numbers */ private function addMissingPartsSqlite(array $numbers, int $groupId): void { @@ -71,7 +71,7 @@ final class MissedPartHandler } /** - * @param array $numbers + * @param array $numbers */ private function addMissingPartsMysql(array $numbers, int $groupId): void { @@ -95,7 +95,7 @@ final class MissedPartHandler /** * Remove successfully repaired parts from the queue. * - * @param array $numbers + * @param array $numbers */ public function removeRepairedParts(array $numbers, int $groupId): void { @@ -120,7 +120,7 @@ final class MissedPartHandler /** * Get parts that need repair for a group. * - * @return array Array of missed parts + * @return array Array of missed parts */ public function getMissingParts(int $groupId): array { diff --git a/app/Services/Categorization/CategorizationPipeline.php b/app/Services/Categorization/CategorizationPipeline.php index 40aa133e3..2b0876502 100644 --- a/app/Services/Categorization/CategorizationPipeline.php +++ b/app/Services/Categorization/CategorizationPipeline.php @@ -45,7 +45,6 @@ class CategorizationPipeline ?NzbSplitUnwrapper $nzbSplitUnwrapper = null, ?ObfuscatedSubjectExtractor $obfuscatedSubjectExtractor = null ) { - /** @phpstan-ignore argument.templateType */ $this->pipes = collect($pipes) ->sortBy(fn (AbstractCategorizationPipe $p) => $p->getPriority()); diff --git a/app/Services/IGDBService.php b/app/Services/IGDBService.php index b37e013ce..7439ea32e 100644 --- a/app/Services/IGDBService.php +++ b/app/Services/IGDBService.php @@ -442,13 +442,13 @@ class IGDBService if ($isPublisher === true && $companyId) { $companyData = Company::find($companyId); if ($companyData) { - $publishers[] = $companyData->name; // @phpstan-ignore property.notFound + $publishers[] = $companyData->name; } } if ($isDeveloper === true && $companyId) { $companyData = Company::find($companyId); if ($companyData) { - $developers[] = $companyData->name; // @phpstan-ignore property.notFound + $developers[] = $companyData->name; } } } diff --git a/app/Services/Releases/ReleaseBrowseService.php b/app/Services/Releases/ReleaseBrowseService.php index 9f8e19a38..50769212b 100644 --- a/app/Services/Releases/ReleaseBrowseService.php +++ b/app/Services/Releases/ReleaseBrowseService.php @@ -119,7 +119,7 @@ class ReleaseBrowseService 'groups_id' => $groupId, 'password_allow_rar' => $this->passwordAllowRar(), 'sort_field' => $indexSort, - 'sort_dir' => $orderBy[1] ?? 'desc', // @phpstan-ignore offsetAccess.notFound + 'sort_dir' => $orderBy[1] ?? 'desc', 'try_fuzzy' => true, ], (int) $num, (int) $start); $searchIndexIds = $filtered['ids']; diff --git a/app/Services/SteamService.php b/app/Services/SteamService.php index aab188bb2..58dd4e0a8 100644 --- a/app/Services/SteamService.php +++ b/app/Services/SteamService.php @@ -312,7 +312,7 @@ class SteamService /** * Populate the steam_apps table with the full app list from Steam. * - * @return list> + * @return array{inserted: int<0, max>, updated: int<0, max>, skipped: int<0, max>, errors: int<0, max>} */ public function populateSteamAppsTable(?callable $progressCallback = null): array { @@ -397,13 +397,13 @@ class SteamService /** * Get the full list of Steam apps. * - * @return array{inserted: int<0, max>, updated: 0, skipped: int<0, max>, errors: int<0, max>} + * @return list> */ public function getFullAppList(): array { $cacheKey = 'steam_full_app_list'; $cached = Cache::get($cacheKey); - if ($cached !== null) { + if (is_array($cached)) { return $cached; } diff --git a/app/Services/TmdbClient.php b/app/Services/TmdbClient.php index 090a99964..b8090b8c3 100644 --- a/app/Services/TmdbClient.php +++ b/app/Services/TmdbClient.php @@ -275,7 +275,7 @@ class TmdbClient * Get TV show details by ID * * @param int|string $id The TMDB TV show ID - * @param array $appendToResponse Additional data to append + * @param list $appendToResponse Additional data to append * @return array|null TV show data or null on failure */ public function getTvShow(int|string $id, array $appendToResponse = []): ?array diff --git a/app/Services/TvProcessing/Pipes/TmdbPipe.php b/app/Services/TvProcessing/Pipes/TmdbPipe.php index 6ef9bf0d8..64814d292 100644 --- a/app/Services/TvProcessing/Pipes/TmdbPipe.php +++ b/app/Services/TvProcessing/Pipes/TmdbPipe.php @@ -77,7 +77,7 @@ class TmdbPipe extends AbstractTvProviderPipe if ($videoId !== 0) { $siteId = $tmdb->getSiteByID('tmdb', (int) $videoId); // If show exists in local DB with a TMDB ID, use it directly - if ($siteId !== false && $siteId !== 0) { // @phpstan-ignore notIdentical.alwaysTrue + if ($siteId !== false && $siteId !== 0) { $this->outputFoundInDb($cleanName); } else { // Show exists in local DB but without TMDB ID (from another source) diff --git a/app/Services/TvProcessing/Pipes/TvMazePipe.php b/app/Services/TvProcessing/Pipes/TvMazePipe.php index 8b24c2012..6419b65de 100644 --- a/app/Services/TvProcessing/Pipes/TvMazePipe.php +++ b/app/Services/TvProcessing/Pipes/TvMazePipe.php @@ -78,7 +78,7 @@ class TvMazePipe extends AbstractTvProviderPipe $siteId = $tvmaze->getSiteByID('tvmaze', (int) $videoId); // If show exists in local DB but doesn't have a TVMaze ID, use the existing video // and process episode matching without trying to search TVMaze API - if ($siteId === false || $siteId === 0) { // @phpstan-ignore identical.alwaysFalse + if ($siteId === false || $siteId === 0) { // Show exists in our DB (likely from another source like TMDB) // Skip TVMaze API search and proceed to episode matching $this->outputFoundInDb($cleanName); diff --git a/app/Services/TvProcessing/Pipes/TvdbPipe.php b/app/Services/TvProcessing/Pipes/TvdbPipe.php index 3f82aa4fb..aa0918fec 100644 --- a/app/Services/TvProcessing/Pipes/TvdbPipe.php +++ b/app/Services/TvProcessing/Pipes/TvdbPipe.php @@ -80,7 +80,7 @@ class TvdbPipe extends AbstractTvProviderPipe $siteId = $tvdb->getSiteByID('tvdb', (int) $videoId); // If show exists in local DB but doesn't have a TVDB ID, use the existing video // and process episode matching without trying to search TVDB API - if ($siteId === false || $siteId === 0) { // @phpstan-ignore identical.alwaysFalse + if ($siteId === false || $siteId === 0) { // Show exists in our DB (likely from another source like TMDB) // Skip TVDB API search and proceed to episode matching $this->outputFoundInDb($cleanName); diff --git a/app/Services/TvProcessing/TvProcessingPipeline.php b/app/Services/TvProcessing/TvProcessingPipeline.php index 121e23a42..59f9f236e 100644 --- a/app/Services/TvProcessing/TvProcessingPipeline.php +++ b/app/Services/TvProcessing/TvProcessingPipeline.php @@ -51,7 +51,6 @@ class TvProcessingPipeline */ public function __construct(iterable $pipes = [], bool $echoOutput = true) { - /** @phpstan-ignore argument.templateType */ $this->pipes = collect($pipes) ->sortBy(fn (AbstractTvProviderPipe $p) => $p->getPriority()); diff --git a/app/Services/TvProcessing/TvShowAdder.php b/app/Services/TvProcessing/TvShowAdder.php index ee035023b..b1b234dde 100644 --- a/app/Services/TvProcessing/TvShowAdder.php +++ b/app/Services/TvProcessing/TvShowAdder.php @@ -7,6 +7,7 @@ namespace App\Services\TvProcessing; use App\Models\Video; use App\Services\TmdbClient; use App\Services\TraktService; +use App\Services\TvProcessing\Providers\AbstractTvProvider; use App\Services\TvProcessing\Providers\TmdbProvider; use App\Services\TvProcessing\Providers\TraktProvider; use App\Services\TvProcessing\Providers\TvdbProvider; @@ -139,10 +140,6 @@ class TvShowAdder return null; } - if (! is_object($extended)) { - return null; - } - // TvdbProvider::formatShowInfo() expects fields shaped like SearchResult // (tvdb_id, name, overview, first_air_time, aliases). The extended // endpoint returns SeriesExtendedRecord (camelCase). Adapt it. @@ -166,6 +163,9 @@ class TvShowAdder ]; } + /** + * @return array{videoId: int, existed: bool, source: string, externalId: string, title: ?string} + */ private function addViaTvdb(int $tvdbId, int $type): array { $provider = new TvdbProvider; @@ -206,6 +206,9 @@ class TvShowAdder return is_array($show) ? $show : null; } + /** + * @return array{videoId: int, existed: bool, source: string, externalId: string, title: ?string} + */ private function addViaTvMaze(int $tvmazeId, int $type): array { $provider = new TvMazeProvider; @@ -251,12 +254,16 @@ class TvShowAdder } $show['alternative_titles'] = $alternativeTitles; - $networks = TmdbClient::getArray($show, 'networks'); - $show['network'] = ! empty($networks[0]['name']) ? (string) $networks[0]['name'] : ''; + $networks = array_values(TmdbClient::getArray($show, 'networks')); + $firstNetwork = $networks[0] ?? null; + $show['network'] = is_array($firstNetwork) && ! empty($firstNetwork['name']) ? (string) $firstNetwork['name'] : ''; return $show; } + /** + * @return array{videoId: int, existed: bool, source: string, externalId: string, title: ?string} + */ private function addViaTmdb(int $tmdbId, int $type): array { $provider = new TmdbProvider; @@ -290,6 +297,9 @@ class TvShowAdder return is_array($show) && ! empty($show['ids']) ? $show : null; } + /** + * @return array{videoId: int, existed: bool, source: string, externalId: string, title: ?string} + */ private function addViaTrakt(int|string $traktId, int $type): array { $provider = new TraktProvider; @@ -338,8 +348,10 @@ class TvShowAdder $trakt = app(TraktService::class); if ($trakt->isConfigured()) { $results = $trakt->searchById($imdbId, 'imdb', 'show'); - if (is_array($results) && ! empty($results[0]['show']['ids']['trakt'])) { - $traktId = (int) $results[0]['show']['ids']['trakt']; + $resultRows = is_array($results) ? array_values($results) : []; + $firstResult = $resultRows[0] ?? null; + if (is_array($firstResult) && ! empty($firstResult['show']['ids']['trakt'])) { + $traktId = (int) $firstResult['show']['ids']['trakt']; $provider = new TraktProvider; $show = $this->fetchTrakt($traktId); if ($show !== null) { @@ -365,6 +377,9 @@ class TvShowAdder return null; } + /** + * @return array{videoId: int, existed: bool, source: string, externalId: string, title: ?string} + */ private function addViaImdb(string $imdbId, int $type): array { // TMDB @@ -388,8 +403,10 @@ class TvShowAdder $trakt = app(TraktService::class); if ($trakt->isConfigured()) { $results = $trakt->searchById($imdbId, 'imdb', 'show'); - if (is_array($results) && ! empty($results[0]['show']['ids']['trakt'])) { - return $this->addViaTrakt((int) $results[0]['show']['ids']['trakt'], $type); + $resultRows = is_array($results) ? array_values($results) : []; + $firstResult = $resultRows[0] ?? null; + if (is_array($firstResult) && ! empty($firstResult['show']['ids']['trakt'])) { + return $this->addViaTrakt((int) $firstResult['show']['ids']['trakt'], $type); } } } catch (\Throwable $e) { @@ -417,7 +434,7 @@ class TvShowAdder * @param array $data * @return array{videoId: int, existed: bool, source: string, externalId: string, title: ?string} */ - private function persist(object $provider, array $data, string $source, string $externalId): array + private function persist(AbstractTvProvider $provider, array $data, string $source, string $externalId): array { // Ensure required keys formatShowInfo callers might omit $data += [ @@ -442,10 +459,9 @@ class TvShowAdder throw new RuntimeException('Provider returned a show without a title.'); } - /** @var int $videoId */ - $videoId = $provider->add($data); // @phpstan-ignore-line - AbstractTvProvider::add() + $videoId = $provider->add($data); - if ($videoId > 0 && method_exists($provider, 'getPoster')) { + if ($videoId > 0) { try { $provider->getPoster($videoId); } catch (\Throwable $e) { diff --git a/app/View/Composers/GlobalDataComposer.php b/app/View/Composers/GlobalDataComposer.php index b5e912072..c5789c8c5 100644 --- a/app/View/Composers/GlobalDataComposer.php +++ b/app/View/Composers/GlobalDataComposer.php @@ -65,7 +65,7 @@ class GlobalDataComposer $viewData['usefulLinks'] = $this->rememberWithCacheFallback('content_useful_links', self::CACHE_TTL, function () { return Content::active() ->ofType(Content::TYPE_USEFUL) - ->ordered() // @phpstan-ignore method.notFound + ->ordered() ->get(); }); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3f64862bd..d10c93526 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -756,12 +756,6 @@ parameters: count: 1 path: app/Services/AdditionalProcessing/ReleaseProcessor.php - - - message: '#^Method App\\Services\\Binaries\\BinariesService\:\:groupMissingPartsIntoRanges\(\) should return array\ but returns list\\>\.$#' - identifier: return.type - count: 1 - path: app/Services/Binaries/BinariesService.php - - message: '#^PHPDoc tag @return with type list\\> is incompatible with native type int\|null\.$#' identifier: return.phpDocType @@ -1196,24 +1190,6 @@ parameters: count: 1 path: app/Services/SteamService.php - - - message: '#^Method App\\Services\\SteamService\:\:getFullAppList\(\) should return array\{inserted\: int\<0, max\>, updated\: 0, skipped\: int\<0, max\>, errors\: int\<0, max\>\} but returns array\{\}\.$#' - identifier: return.type - count: 1 - path: app/Services/SteamService.php - - - - message: '#^Method App\\Services\\SteamService\:\:populateSteamAppsTable\(\) should return list\\> but returns array\{inserted\: 0, updated\: 0, skipped\: 0, errors\: 0\}\.$#' - identifier: return.type - count: 1 - path: app/Services/SteamService.php - - - - message: '#^Method App\\Services\\SteamService\:\:populateSteamAppsTable\(\) should return list\\> but returns array\{inserted\: int\<0, max\>, updated\: 0, skipped\: int\<0, max\>, errors\: int\<0, max\>\}\.$#' - identifier: return.type - count: 1 - path: app/Services/SteamService.php - - message: '#^Method App\\Services\\SteamService\:\:tokenize\(\) should return array\ but returns list\\.$#' identifier: return.type @@ -1250,12 +1226,6 @@ parameters: count: 1 path: app/Services/UserStatsService.php - - - message: '#^Method App\\Services\\UserStatsService\:\:getDownloadsPerDay\(\) should return array\ but returns list\\>\.$#' - identifier: return.type - count: 1 - path: app/Services/UserStatsService.php - - message: '#^Method App\\Services\\UserStatsService\:\:getDownloadsPerHour\(\) should return list\\> but returns array\\.$#' identifier: return.type @@ -1268,12 +1238,6 @@ parameters: count: 1 path: app/Services/UserStatsService.php - - - message: '#^Method App\\Services\\UserStatsService\:\:getSummaryStats\(\) should return list\\> but returns array\{total_users\: int\<0, max\>, downloads_today\: int, downloads_week\: int, api_hits_today\: int, api_hits_week\: int\}\.$#' - identifier: return.type - count: 1 - path: app/Services/UserStatsService.php - - message: '#^Method App\\Services\\XrefService\:\:extractTokens\(\) should return array\ but returns list\\.$#' identifier: return.type @@ -1352,12 +1316,6 @@ parameters: count: 1 path: app/Support/SecondaryIndexDocuments.php - - - message: '#^Method App\\Support\\UpdatePerformanceHelper\:\:checkSystemResources\(\) should return array\ but returns list\\.$#' - identifier: return.type - count: 1 - path: app/Support/UpdatePerformanceHelper.php - - message: '#^Method App\\Support\\UpdatePerformanceHelper\:\:getSystemMemoryInfo\(\) should return list\\|null but returns array\{total\: int, available\: int, used\: int, usage_percent\: \(float\|int\)\}\.$#' identifier: return.type diff --git a/tests/Feature/RegisterControllerTest.php b/tests/Feature/RegisterControllerTest.php index f98327ce7..defaee068 100644 --- a/tests/Feature/RegisterControllerTest.php +++ b/tests/Feature/RegisterControllerTest.php @@ -6,6 +6,7 @@ namespace Tests\Feature; use App\Http\Controllers\Auth\RegisterController; use App\Models\User; +use App\Rules\ValidEmailDomain; use App\Services\RegistrationStatusService; use Illuminate\Contracts\Validation\UncompromisedVerifier; use Illuminate\Database\Schema\Blueprint; @@ -38,6 +39,13 @@ class RegisterControllerTest extends TestCase return true; } }); + $this->app->bind(ValidEmailDomain::class, fn () => new class extends ValidEmailDomain + { + protected function validateDnsRecords(string $domain): bool + { + return true; + } + }); Schema::dropIfExists('users'); Schema::dropIfExists('roles'); diff --git a/tests/Unit/AdditionalProcessing/ReleaseProcessorTest.php b/tests/Unit/AdditionalProcessing/ReleaseProcessorTest.php index e8b9710dd..fee75dc34 100644 --- a/tests/Unit/AdditionalProcessing/ReleaseProcessorTest.php +++ b/tests/Unit/AdditionalProcessing/ReleaseProcessorTest.php @@ -69,6 +69,7 @@ class ReleaseProcessorTest extends TestCase ]); $releaseManager = Mockery::mock(ReleaseFileManager::class); + $releaseManager->shouldReceive('processReleaseNameFromNzbContents')->once()->andReturnFalse(); $releaseManager->shouldReceive('finalizeRelease')->once()->andReturnNull(); $tempWorkspace = Mockery::mock(TempWorkspaceService::class); diff --git a/tests/Unit/Admin/DeletedUsersByColumnTest.php b/tests/Unit/Admin/DeletedUsersByColumnTest.php index daf851e91..a7f21af50 100644 --- a/tests/Unit/Admin/DeletedUsersByColumnTest.php +++ b/tests/Unit/Admin/DeletedUsersByColumnTest.php @@ -62,16 +62,16 @@ class DeletedUsersByColumnTest extends TestCase */ public function test_dashboard_activity_log_shows_deleted_by(): void { - $bladePath = __DIR__.'/../../../resources/views/admin/dashboard.blade.php'; + $scriptPath = __DIR__.'/../../../resources/js/alpine/components/admin/dashboard.js'; - $this->assertFileExists($bladePath); + $this->assertFileExists($scriptPath); - $content = file_get_contents($bladePath); + $content = file_get_contents($scriptPath); // Check that the dashboard shows deleted_by for deleted activity - $this->assertStringContainsString("activity->type === 'deleted'", $content); - $this->assertStringContainsString("metadata['deleted_by']", $content); - $this->assertStringContainsString("metadata['permanent']", $content); + $this->assertStringContainsString("activity.type === 'deleted'", $content); + $this->assertStringContainsString('activity.metadata.deleted_by', $content); + $this->assertStringContainsString('activity.metadata.permanent', $content); } /** diff --git a/tests/Unit/ElasticSearchQueryTest.php b/tests/Unit/ElasticSearchQueryTest.php index a65df4b77..6902b0567 100644 --- a/tests/Unit/ElasticSearchQueryTest.php +++ b/tests/Unit/ElasticSearchQueryTest.php @@ -27,14 +27,19 @@ final class ElasticSearchQueryTest extends TestCase $driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ElasticSearchDriver.php'); $this->assertIsString($driverSource); - $this->assertStringContainsString("'fields' => self::RELEASE_TEXT_FIELDS", $driverSource); - $this->assertStringContainsString("'type' => 'cross_fields'", $driverSource); - $this->assertStringContainsString("'operator' => 'and'", $driverSource); + $methodSource = strstr($driverSource, 'public function searchReleasesFiltered'); + $this->assertIsString($methodSource); + $methodSource = strstr($methodSource, 'private function buildReleaseFieldSpecificMustClauses', true); + $this->assertIsString($methodSource); + + $this->assertStringContainsString("'fields' => self::RELEASE_TEXT_FIELDS", $methodSource); + $this->assertStringContainsString("'type' => 'cross_fields'", $methodSource); + $this->assertStringContainsString("'operator' => 'and'", $methodSource); $this->assertStringNotContainsString( "'fields' => ['searchname^3', 'name^2', 'filename', 'plainsearchname']", - $driverSource + $methodSource ); - $this->assertStringNotContainsString("'type' => 'best_fields'", $driverSource); + $this->assertStringNotContainsString("'type' => 'best_fields'", $methodSource); } #[Test] diff --git a/tests/Unit/Models/AnidbInfoTest.php b/tests/Unit/Models/AnidbInfoTest.php index 614589a18..880c93d39 100644 --- a/tests/Unit/Models/AnidbInfoTest.php +++ b/tests/Unit/Models/AnidbInfoTest.php @@ -3,17 +3,15 @@ namespace Tests\Unit\Models; use App\Models\AnidbInfo; -use Illuminate\Foundation\Testing\RefreshDatabase; +use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; class AnidbInfoTest extends TestCase { - use RefreshDatabase; - - /** @test */ + #[Test] public function it_returns_anidb_url_when_anidbid_exists(): void { - $animeInfo = AnidbInfo::create([ + $animeInfo = new AnidbInfo([ 'anidbid' => 12345, 'type' => 'TV Series', ]); @@ -23,10 +21,10 @@ class AnidbInfoTest extends TestCase $this->assertEquals('https://anidb.net/anime/12345', $url); } - /** @test */ + #[Test] public function it_returns_anilist_url_when_anilist_id_exists(): void { - $animeInfo = AnidbInfo::create([ + $animeInfo = new AnidbInfo([ 'anidbid' => 1, 'anilist_id' => 9253, 'type' => 'TV Series', @@ -37,12 +35,12 @@ class AnidbInfoTest extends TestCase $this->assertEquals('https://anilist.co/anime/9253', $url); } - /** @test */ + #[Test] public function it_returns_myanimelist_url_when_myanimelist_id_exists(): void { - $animeInfo = AnidbInfo::create([ + $animeInfo = new AnidbInfo([ 'anidbid' => 1, - 'myanimelist_id' => 9253, + 'mal_id' => 9253, 'type' => 'TV Series', ]); @@ -51,13 +49,13 @@ class AnidbInfoTest extends TestCase $this->assertEquals('https://myanimelist.net/anime/9253', $url); } - /** @test */ + #[Test] public function it_returns_all_external_links(): void { - $animeInfo = AnidbInfo::create([ + $animeInfo = new AnidbInfo([ 'anidbid' => 12345, 'anilist_id' => 9253, - 'myanimelist_id' => 9253, + 'mal_id' => 9253, 'type' => 'TV Series', ]); @@ -69,10 +67,10 @@ class AnidbInfoTest extends TestCase $this->assertArrayHasKey('myanimelist', $links); } - /** @test */ + #[Test] public function it_checks_if_external_links_exist(): void { - $animeInfo = AnidbInfo::create([ + $animeInfo = new AnidbInfo([ 'anidbid' => 1, 'anilist_id' => 123, 'type' => 'TV Series', diff --git a/tests/Unit/NzbServicePathResolutionTest.php b/tests/Unit/NzbServicePathResolutionTest.php index f4db2464b..8c1ce9a42 100644 --- a/tests/Unit/NzbServicePathResolutionTest.php +++ b/tests/Unit/NzbServicePathResolutionTest.php @@ -137,7 +137,9 @@ final class NzbServicePathResolutionTest extends TestCase ); foreach ($iterator as $item) { - if ($item->isDir()) { + if ($item->isLink()) { + unlink($item->getPathname()); + } elseif ($item->isDir()) { rmdir($item->getPathname()); } else { unlink($item->getPathname()); diff --git a/tests/Unit/Rules/ValidEmailDomainTest.php b/tests/Unit/Rules/ValidEmailDomainTest.php index 44a2f1f42..660a39e45 100644 --- a/tests/Unit/Rules/ValidEmailDomainTest.php +++ b/tests/Unit/Rules/ValidEmailDomainTest.php @@ -12,7 +12,7 @@ class ValidEmailDomainTest extends TestCase */ public function test_rejects_disposable_email_domains(): void { - $rule = new ValidEmailDomain; + $rule = $this->makeRule(); $disposableEmails = [ 'test@guerrillamail.com', @@ -38,7 +38,7 @@ class ValidEmailDomainTest extends TestCase */ public function test_accepts_legitimate_email_domains(): void { - $rule = new ValidEmailDomain; + $rule = $this->makeRule(); $legitimateEmails = [ 'test@gmail.com', @@ -62,7 +62,7 @@ class ValidEmailDomainTest extends TestCase */ public function test_rejects_emails_with_suspicious_patterns(): void { - $rule = new ValidEmailDomain; + $rule = $this->makeRule(); $suspiciousEmails = [ 'test@tempdomainexample.com', @@ -85,7 +85,7 @@ class ValidEmailDomainTest extends TestCase */ public function test_rejects_emails_with_invalid_domains(): void { - $rule = new ValidEmailDomain; + $rule = $this->makeRule(); $invalidEmails = [ 'test@nonexistentdomain12345xyz.com', @@ -109,7 +109,7 @@ class ValidEmailDomainTest extends TestCase */ public function test_rejects_malformed_emails(): void { - $rule = new ValidEmailDomain; + $rule = $this->makeRule(); $malformedEmails = [ 'notanemail', @@ -126,4 +126,20 @@ class ValidEmailDomainTest extends TestCase $this->assertTrue($failed, "Expected {$email} to be rejected as malformed"); } } + + private function makeRule(): ValidEmailDomain + { + return new class extends ValidEmailDomain + { + protected function validateDnsRecords(string $domain): bool + { + return in_array($domain, [ + 'gmail.com', + 'yahoo.com', + 'outlook.com', + 'protonmail.com', + ], true); + } + }; + } } diff --git a/tests/Unit/XxxCategorizationTest.php b/tests/Unit/XxxCategorizationTest.php index f04b432d7..bf4363a10 100644 --- a/tests/Unit/XxxCategorizationTest.php +++ b/tests/Unit/XxxCategorizationTest.php @@ -116,11 +116,12 @@ class XxxCategorizationTest extends TestCase } #[DataProvider('vrReleasesProvider')] - public function test_vr_releases_through_full_pipeline(string $releaseName, int $expectedCategoryId): void + public function test_vr_releases_through_full_pipeline(string $releaseName, int $expectedCategoryId, string $expectedMatchedBy): void { $passable = $this->runPipeline($releaseName); $this->assertSame($expectedCategoryId, $passable->bestResult->categoryId, "Pipeline wrong category for: {$releaseName}"); + $this->assertSame($expectedMatchedBy, $passable->bestResult->matchedBy, "Pipeline wrong matched_by for: {$releaseName}"); } #[DataProvider('nonVrReleasesProvider')]