From fe50781a8ebcd47a3395eb0edefd380e5157e181 Mon Sep 17 00:00:00 2001 From: DariusIII Date: Tue, 28 Apr 2026 12:17:24 +0200 Subject: [PATCH] Update DTOs and add spatie/laravel-data package to manage them --- Makefile | 14 + app/Data/Api/CategoryData.php | 39 + app/Data/Api/DetailsData.php | 85 ++ app/Data/Api/ReleaseData.php | 111 +++ app/Http/Controllers/Api/ApiV2Controller.php | 90 +- .../AdditionalProcessingOrchestrator.php | 2 +- .../ArchiveExtractionService.php | 2 +- .../MediaExtractionService.php | 2 +- .../ReleaseFileManager.php | 2 +- .../ReleaseFilesArchiveFallback.php | 2 +- .../AdditionalProcessing/ReleaseProcessor.php | 2 +- .../ReleaseProcessingContext.php | 7 +- app/Services/BookService.php | 2 +- .../Checkers/AbstractNameChecker.php | 2 +- .../NameFixing/Checkers/AppNameChecker.php | 2 +- .../NameFixing/Checkers/GameNameChecker.php | 2 +- .../NameFixing/Checkers/MovieNameChecker.php | 2 +- .../NameFixing/Checkers/TvNameChecker.php | 2 +- .../Contracts/NameCheckerInterface.php | 2 +- .../{DTO => Data}/NameFixResult.php | 20 +- .../Extractors/FileNameExtractor.php | 2 +- .../Extractors/NfoNameExtractor.php | 2 +- .../NameFixing/NameCheckerService.php | 2 +- app/Services/ReleaseProcessingService.php | 8 +- app/Support/BookMatchScorer.php | 2 +- app/Support/DTOs/ProcessReleasesSettings.php | 109 --- app/Support/DTOs/ReleaseCreationResult.php | 58 -- app/Support/DTOs/ReleaseDeleteStats.php | 105 --- app/Support/DTOs/SteamGameData.php | 313 ------- .../{DTOs => Data}/BookParseResult.php | 8 +- .../Data/Factories/SteamGameDataFactory.php | 236 +++++ app/Support/Data/ProcessReleasesSettings.php | 92 ++ app/Support/Data/ReleaseCreationResult.php | 30 + app/Support/Data/ReleaseDeleteStats.php | 62 ++ app/Support/Data/SteamGameData.php | 155 ++++ app/Support/{DTOs => Data}/SteamPriceData.php | 45 +- app/Transformers/ApiTransformer.php | 146 --- app/Transformers/CategoryTransformer.php | 25 - app/Transformers/DetailsTransformer.php | 66 -- composer.json | 3 +- composer.lock | 620 ++++++++----- config/typescript-transformer.php | 94 ++ docker-entrypoint.sh | 3 + docs/nntmux_api_v2.md | 40 +- package.json | 3 +- phpstan-baseline.neon | 845 +++++++++++++++++- ...AdditionalProcessingNzbSplitRenameTest.php | 2 +- .../ReleaseFilesArchiveFallbackTest.php | 2 +- .../ReleaseProcessorTest.php | 2 +- tests/Unit/SteamDTOsTest.php | 6 +- tests/Unit/Support/BookMatchScorerTest.php | 2 +- 51 files changed, 2248 insertions(+), 1232 deletions(-) create mode 100644 app/Data/Api/CategoryData.php create mode 100644 app/Data/Api/DetailsData.php create mode 100644 app/Data/Api/ReleaseData.php rename app/Services/AdditionalProcessing/{DTO => State}/ReleaseProcessingContext.php (93%) rename app/Services/NameFixing/{DTO => Data}/NameFixResult.php (80%) delete mode 100644 app/Support/DTOs/ProcessReleasesSettings.php delete mode 100644 app/Support/DTOs/ReleaseCreationResult.php delete mode 100644 app/Support/DTOs/ReleaseDeleteStats.php delete mode 100644 app/Support/DTOs/SteamGameData.php rename app/Support/{DTOs => Data}/BookParseResult.php (77%) create mode 100644 app/Support/Data/Factories/SteamGameDataFactory.php create mode 100644 app/Support/Data/ProcessReleasesSettings.php create mode 100644 app/Support/Data/ReleaseCreationResult.php create mode 100644 app/Support/Data/ReleaseDeleteStats.php create mode 100644 app/Support/Data/SteamGameData.php rename app/Support/{DTOs => Data}/SteamPriceData.php (67%) delete mode 100644 app/Transformers/ApiTransformer.php delete mode 100644 app/Transformers/CategoryTransformer.php delete mode 100644 app/Transformers/DetailsTransformer.php create mode 100644 config/typescript-transformer.php diff --git a/Makefile b/Makefile index fcd062d81..09a166ae1 100644 --- a/Makefile +++ b/Makefile @@ -125,6 +125,20 @@ npm-build: ## Run npm install and build inside the container npm-dev: ## Start Vite dev server inside the container @$(SAIL) npm run dev +.PHONY: ts-types +ts-types: ## Regenerate TypeScript types from PHP DTOs/Enums + @$(SAIL) artisan typescript:transform + +.PHONY: ts-types-check +ts-types-check: ## CI: regenerate TS types and fail if working tree drifts + @$(SAIL) artisan typescript:transform --quiet + @git diff --exit-code resources/js/types/generated.d.ts \ + || (echo "❌ resources/js/types/generated.d.ts is out of date — run 'make ts-types' and commit." && exit 1) + +.PHONY: data-cache +data-cache: ## Cache spatie/laravel-data structures (run on deploy) + @$(SAIL) artisan data:cache-structures + # ── Dependencies ───────────────────────────────────────────── .PHONY: composer-install diff --git a/app/Data/Api/CategoryData.php b/app/Data/Api/CategoryData.php new file mode 100644 index 000000000..786b8cbf5 --- /dev/null +++ b/app/Data/Api/CategoryData.php @@ -0,0 +1,39 @@ + $subcategories Map of sub-category ID → title. + */ + public function __construct( + public int $id, + public string $name, + public array $subcategories, + ) {} + + public static function fromCategory(RootCategory $category): self + { + /** @var array $subcategories */ + $subcategories = $category->categories()->pluck('title', 'id')->all(); + + return new self( + id: (int) $category->id, + name: (string) $category->title, + subcategories: $subcategories, + ); + } +} diff --git a/app/Data/Api/DetailsData.php b/app/Data/Api/DetailsData.php new file mode 100644 index 000000000..1f183f1a1 --- /dev/null +++ b/app/Data/Api/DetailsData.php @@ -0,0 +1,85 @@ +categories_id; + $base = [ + 'title' => (string) $release->searchname, + 'details' => url('/').'/details/'.$release->guid, + 'link' => url('/').'/getnzb?id='.$release->guid.'.nzb&r='.$user->api_token, + 'category' => $categoriesId, + 'category_name' => $release->category_name ?? null, + 'added' => Carbon::parse($release->adddate)->toRssString(), + 'size' => $release->size, + 'files' => $release->totalpart, + 'grabs' => $release->grabs, + 'comments' => $release->comments, + 'password' => $release->passwordstatus, + 'usenetdate' => Carbon::parse($release->postdate)->toRssString(), + ]; + + if (in_array($categoriesId, Category::MOVIES_GROUP, true)) { + return new self( + ...$base, + imdbid: $release->imdbid, + ); + } + + if (in_array($categoriesId, Category::TV_GROUP, true)) { + return new self( + ...$base, + imdbid: $release->imdb, // @phpstan-ignore property.notFound + tmdbid: $release->tmdb, + traktid: $release->trakt, + tvairdate: $release->firstaired, + tvdbid: $release->tvdb, + tvrageid: $release->tvrage, + tvmazeid: $release->tvmaze, + ); + } + + return new self(...$base); + } +} diff --git a/app/Data/Api/ReleaseData.php b/app/Data/Api/ReleaseData.php new file mode 100644 index 000000000..9dacb4caf --- /dev/null +++ b/app/Data/Api/ReleaseData.php @@ -0,0 +1,111 @@ + $release->{$key} ?? $default; + + $categoriesId = (int) $get('categories_id', 0); + $guid = (string) $get('guid', ''); + + $base = [ + 'title' => (string) $get('searchname', ''), + 'details' => url('/details/'.$guid), + 'url' => url('/getnzb').'?id='.$guid.'.nzb&r='.$user->api_token, + 'category' => $categoriesId, + 'category_name' => $get('category_name'), + 'added' => Carbon::parse($get('adddate'))->toRssString(), + 'size' => $get('size'), + 'files' => $get('totalpart'), + 'grabs' => self::nullIfZero($get('grabs')), + 'comments' => self::nullIfZero($get('comments')), + 'password' => $get('passwordstatus'), + 'usenetdate' => Carbon::parse($get('postdate'))->toRssString(), + ]; + + if (in_array($categoriesId, Category::MOVIES_GROUP, true)) { + return new self( + ...$base, + imdbid: self::nullIfZero($get('imdbid')), + tmdbid: self::nullIfZero($get('tmdbid')), + traktid: self::nullIfZero($get('traktid')), + ); + } + + if (in_array($categoriesId, Category::TV_GROUP, true)) { + return new self( + ...$base, + imdbid: self::nullIfZero($get('imdb')), + tmdbid: self::nullIfZero($get('tmdb')), + traktid: self::nullIfZero($get('trakt')), + episode_title: $get('title'), + season: $get('series'), + episode: $get('episode'), + tvairdate: $get('firstaired'), + tvdbid: self::nullIfZero($get('tvdb')), + tvrageid: self::nullIfZero($get('tvrage')), + tvmazeid: self::nullIfZero($get('tvmaze')), + ); + } + + return new self(...$base); + } + + private static function nullIfZero(mixed $value): mixed + { + return ($value !== null && $value !== 0 && $value !== '' && $value !== '0') ? $value : null; + } +} diff --git a/app/Http/Controllers/Api/ApiV2Controller.php b/app/Http/Controllers/Api/ApiV2Controller.php index f79360004..f4c821f1a 100644 --- a/app/Http/Controllers/Api/ApiV2Controller.php +++ b/app/Http/Controllers/Api/ApiV2Controller.php @@ -4,6 +4,9 @@ declare(strict_types=1); namespace App\Http\Controllers\Api; +use App\Data\Api\CategoryData; +use App\Data\Api\DetailsData; +use App\Data\Api\ReleaseData; use App\Events\UserAccessedApi; use App\Http\Controllers\BasePageController; use App\Models\Category; @@ -16,9 +19,6 @@ use App\Models\UserRequest; use App\Services\RegistrationStatusService; use App\Services\Releases\ReleaseBrowseService; use App\Services\Releases\ReleaseSearchService; -use App\Transformers\ApiTransformer; -use App\Transformers\CategoryTransformer; -use App\Transformers\DetailsTransformer; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Routing\ResponseFactory; use Illuminate\Http\JsonResponse; @@ -101,6 +101,34 @@ class ApiV2Controller extends BasePageController ]; } + /** + * Build the standard search-results JSON response. + * + * Replaces the legacy Fractal `['Results' => fractal(...)]` envelope with a + * lower-case `results` array of {@see ReleaseData} payloads. Pagination + * total and per-user API/grab quotas are kept inline alongside the array + * because a JSON object cannot carry both top-level metadata fields and a + * bare array body. + * + * @param iterable $rows + */ + private function buildSearchResponse(iterable $rows, User $user): JsonResponse + { + $rowsArray = is_array($rows) ? $rows : iterator_to_array($rows, false); + $total = (int) ($rowsArray[0]->_totalrows ?? 0); + + $results = array_map( + static fn ($row): array => ReleaseData::fromRelease($row, $user)->toArray(), + $rowsArray, + ); + + return response()->json(array_merge( + ['Total' => $total], + $this->buildUserStatsResponse($user), + ['results' => $results], + )); + } + private function parseMaxAge(Request $request): int|JsonResponse { if (! $request->has('maxage')) { @@ -158,7 +186,7 @@ class ApiV2Controller extends BasePageController 'book-search' => ['available' => 'yes', 'supportedParams' => 'id,cat,minsize,maxsize,maxage,group,limit,offset,sort'], 'anime-search' => ['available' => 'yes', 'supportedParams' => 'id,anidbid,anilistid,cat,minsize,maxsize,maxage,limit,offset,sort'], ], - 'categories' => fractal($category, new CategoryTransformer), + 'categories' => $category->map(static fn ($c) => CategoryData::fromCategory($c))->values(), 'groups' => Schema::hasTable('usenet_groups') ? UsenetGroup::query() ->where('active', 1) @@ -256,13 +284,7 @@ class ApiV2Controller extends BasePageController ); }); - $response = array_merge( - ['Total' => $relData[0]->_totalrows ?? 0], - $this->buildUserStatsResponse($user), - ['Results' => fractal($relData, new ApiTransformer($user))] - ); - - return response()->json($response); + return $this->buildSearchResponse($relData, $user); } public function audio(Request $request): JsonResponse|Response @@ -308,13 +330,7 @@ class ApiV2Controller extends BasePageController $sort ); - $response = array_merge( - ['Total' => $relData[0]->_totalrows ?? 0], - $this->buildUserStatsResponse($user), - ['Results' => fractal($relData, new ApiTransformer($user))] - ); - - return response()->json($response); + return $this->buildSearchResponse($relData, $user); } public function books(Request $request): JsonResponse|Response @@ -360,13 +376,7 @@ class ApiV2Controller extends BasePageController $sort ); - $response = array_merge( - ['Total' => $relData[0]->_totalrows ?? 0], - $this->buildUserStatsResponse($user), - ['Results' => fractal($relData, new ApiTransformer($user))] - ); - - return response()->json($response); + return $this->buildSearchResponse($relData, $user); } public function anime(Request $request): JsonResponse|Response @@ -412,13 +422,7 @@ class ApiV2Controller extends BasePageController $sort ); - $response = array_merge( - ['Total' => $relData[0]->_totalrows ?? 0], - $this->buildUserStatsResponse($user), - ['Results' => fractal($relData, new ApiTransformer($user))] - ); - - return response()->json($response); + return $this->buildSearchResponse($relData, $user); } /** @@ -479,13 +483,7 @@ class ApiV2Controller extends BasePageController ); } - $response = array_merge( - ['Total' => $relData[0]->_totalrows ?? 0], - $this->buildUserStatsResponse($user), - ['Results' => fractal($relData, new ApiTransformer($user))] - ); - - return response()->json($response); + return $this->buildSearchResponse($relData, $user); } /** @@ -559,13 +557,7 @@ class ApiV2Controller extends BasePageController $sort ); - $response = array_merge( - ['Total' => $relData[0]->_totalrows ?? 0], - $this->buildUserStatsResponse($user), - ['Results' => fractal($relData, new ApiTransformer($user))] - ); - - return response()->json($response); + return $this->buildSearchResponse($relData, $user); } public function getNzb(Request $request): Application|ResponseFactory|JsonResponse|Redirector|RedirectResponse @@ -599,9 +591,11 @@ class ApiV2Controller extends BasePageController event(new UserAccessedApi($user, $request->ip())); $relData = Release::getByGuidForApi($request->input('id')); - $relData = fractal($relData, new DetailsTransformer($user)); + if ($relData === null) { + return response()->json(['error' => 'No such item'], 404); + } - return response()->json($relData); + return response()->json(DetailsData::fromRelease($relData, $user)->toArray()); } private function hasTvSearchParameters(Request $request): bool diff --git a/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php b/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php index 495aea384..07c96cdcd 100644 --- a/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php +++ b/app/Services/AdditionalProcessing/AdditionalProcessingOrchestrator.php @@ -6,7 +6,7 @@ namespace App\Services\AdditionalProcessing; use App\Models\Release; use App\Services\AdditionalProcessing\Config\ProcessingConfiguration; -use App\Services\AdditionalProcessing\DTO\ReleaseProcessingContext; +use App\Services\AdditionalProcessing\State\ReleaseProcessingContext; use App\Services\TempWorkspaceService; use Exception; use Illuminate\Support\Collection; diff --git a/app/Services/AdditionalProcessing/ArchiveExtractionService.php b/app/Services/AdditionalProcessing/ArchiveExtractionService.php index 6d46cc128..18d257543 100644 --- a/app/Services/AdditionalProcessing/ArchiveExtractionService.php +++ b/app/Services/AdditionalProcessing/ArchiveExtractionService.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Services\AdditionalProcessing; use App\Services\AdditionalProcessing\Config\ProcessingConfiguration; -use App\Services\AdditionalProcessing\DTO\ReleaseProcessingContext; +use App\Services\AdditionalProcessing\State\ReleaseProcessingContext; use App\Services\Releases\ReleaseBrowseService; use dariusiii\rarinfo\ArchiveInfo; use dariusiii\rarinfo\Par2Info; diff --git a/app/Services/AdditionalProcessing/MediaExtractionService.php b/app/Services/AdditionalProcessing/MediaExtractionService.php index 072627443..9428d328f 100644 --- a/app/Services/AdditionalProcessing/MediaExtractionService.php +++ b/app/Services/AdditionalProcessing/MediaExtractionService.php @@ -8,7 +8,7 @@ use App\Facades\Search; use App\Models\Category; use App\Models\Release; use App\Services\AdditionalProcessing\Config\ProcessingConfiguration; -use App\Services\AdditionalProcessing\DTO\ReleaseProcessingContext; +use App\Services\AdditionalProcessing\State\ReleaseProcessingContext; use App\Services\Categorization\CategorizationService; use App\Services\NameFixing\ReleaseUpdateService; use App\Services\ReleaseExtraService; diff --git a/app/Services/AdditionalProcessing/ReleaseFileManager.php b/app/Services/AdditionalProcessing/ReleaseFileManager.php index 7f49f9e7d..af919eb6d 100644 --- a/app/Services/AdditionalProcessing/ReleaseFileManager.php +++ b/app/Services/AdditionalProcessing/ReleaseFileManager.php @@ -11,7 +11,7 @@ use App\Models\Predb; use App\Models\Release; use App\Models\ReleaseFile; use App\Services\AdditionalProcessing\Config\ProcessingConfiguration; -use App\Services\AdditionalProcessing\DTO\ReleaseProcessingContext; +use App\Services\AdditionalProcessing\State\ReleaseProcessingContext; use App\Services\NameFixing\FileNameCleaner; use App\Services\NameFixing\NameFixingService; use App\Services\NameFixing\ReleaseUpdateService; diff --git a/app/Services/AdditionalProcessing/ReleaseFilesArchiveFallback.php b/app/Services/AdditionalProcessing/ReleaseFilesArchiveFallback.php index 9b3523c46..a1c155711 100644 --- a/app/Services/AdditionalProcessing/ReleaseFilesArchiveFallback.php +++ b/app/Services/AdditionalProcessing/ReleaseFilesArchiveFallback.php @@ -6,7 +6,7 @@ namespace App\Services\AdditionalProcessing; use App\Models\ReleaseFile; use App\Services\AdditionalProcessing\Config\ProcessingConfiguration; -use App\Services\AdditionalProcessing\DTO\ReleaseProcessingContext; +use App\Services\AdditionalProcessing\State\ReleaseProcessingContext; use App\Services\AdditionalProcessing\Enums\DownloadKind; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Support\Facades\File; diff --git a/app/Services/AdditionalProcessing/ReleaseProcessor.php b/app/Services/AdditionalProcessing/ReleaseProcessor.php index 30ae5e5b4..a1e78654d 100644 --- a/app/Services/AdditionalProcessing/ReleaseProcessor.php +++ b/app/Services/AdditionalProcessing/ReleaseProcessor.php @@ -6,7 +6,7 @@ namespace App\Services\AdditionalProcessing; use App\Models\UsenetGroup; use App\Services\AdditionalProcessing\Config\ProcessingConfiguration; -use App\Services\AdditionalProcessing\DTO\ReleaseProcessingContext; +use App\Services\AdditionalProcessing\State\ReleaseProcessingContext; use App\Services\AdditionalProcessing\Enums\DownloadKind; use App\Services\Releases\ReleaseBrowseService; use App\Services\TempWorkspaceService; diff --git a/app/Services/AdditionalProcessing/DTO/ReleaseProcessingContext.php b/app/Services/AdditionalProcessing/State/ReleaseProcessingContext.php similarity index 93% rename from app/Services/AdditionalProcessing/DTO/ReleaseProcessingContext.php rename to app/Services/AdditionalProcessing/State/ReleaseProcessingContext.php index 06524d7e3..d35b348d6 100644 --- a/app/Services/AdditionalProcessing/DTO/ReleaseProcessingContext.php +++ b/app/Services/AdditionalProcessing/State/ReleaseProcessingContext.php @@ -2,13 +2,18 @@ declare(strict_types=1); -namespace App\Services\AdditionalProcessing\DTO; +namespace App\Services\AdditionalProcessing\State; use App\Models\Release; /** * Mutable context object that holds the processing state for a single release. * Passed between services during processing to share state. + * + * NOTE: Intentionally NOT a `spatie/laravel-data` Data object. This class is + * mutated heavily during the additional-processing pipeline (counters, found + * flags, accumulating message IDs) and holds an Eloquent {@see Release} model, + * neither of which fits the immutable, serialisable design of `Data` DTOs. */ class ReleaseProcessingContext { diff --git a/app/Services/BookService.php b/app/Services/BookService.php index 0a0b04954..31d84cf9e 100644 --- a/app/Services/BookService.php +++ b/app/Services/BookService.php @@ -13,7 +13,7 @@ use App\Models\Settings; use App\Services\NameFixing\Extractors\ObfuscatedSubjectExtractor; use App\Services\Releases\ReleaseBrowseService; use App\Support\BookMatchScorer; -use App\Support\DTOs\BookParseResult; +use App\Support\Data\BookParseResult; use App\Support\MetadataSearchLookup; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Cache; diff --git a/app/Services/NameFixing/Checkers/AbstractNameChecker.php b/app/Services/NameFixing/Checkers/AbstractNameChecker.php index 92cc2e1d9..68be32aa6 100644 --- a/app/Services/NameFixing/Checkers/AbstractNameChecker.php +++ b/app/Services/NameFixing/Checkers/AbstractNameChecker.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Services\NameFixing\Checkers; use App\Services\NameFixing\Contracts\NameCheckerInterface; -use App\Services\NameFixing\DTO\NameFixResult; +use App\Services\NameFixing\Data\NameFixResult; /** * Abstract base class for name checkers. diff --git a/app/Services/NameFixing/Checkers/AppNameChecker.php b/app/Services/NameFixing/Checkers/AppNameChecker.php index d237a7d69..81b1b5ca4 100644 --- a/app/Services/NameFixing/Checkers/AppNameChecker.php +++ b/app/Services/NameFixing/Checkers/AppNameChecker.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Services\NameFixing\Checkers; -use App\Services\NameFixing\DTO\NameFixResult; +use App\Services\NameFixing\Data\NameFixResult; use App\Services\NameFixing\Patterns\AppPatterns; /** diff --git a/app/Services/NameFixing/Checkers/GameNameChecker.php b/app/Services/NameFixing/Checkers/GameNameChecker.php index 365074d70..db28e8a90 100644 --- a/app/Services/NameFixing/Checkers/GameNameChecker.php +++ b/app/Services/NameFixing/Checkers/GameNameChecker.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Services\NameFixing\Checkers; -use App\Services\NameFixing\DTO\NameFixResult; +use App\Services\NameFixing\Data\NameFixResult; use App\Services\NameFixing\Patterns\GamePatterns; /** diff --git a/app/Services/NameFixing/Checkers/MovieNameChecker.php b/app/Services/NameFixing/Checkers/MovieNameChecker.php index 575d13529..64c3869cf 100644 --- a/app/Services/NameFixing/Checkers/MovieNameChecker.php +++ b/app/Services/NameFixing/Checkers/MovieNameChecker.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Services\NameFixing\Checkers; -use App\Services\NameFixing\DTO\NameFixResult; +use App\Services\NameFixing\Data\NameFixResult; use App\Services\NameFixing\Patterns\MoviePatterns; /** diff --git a/app/Services/NameFixing/Checkers/TvNameChecker.php b/app/Services/NameFixing/Checkers/TvNameChecker.php index d44a77faf..fd7440709 100644 --- a/app/Services/NameFixing/Checkers/TvNameChecker.php +++ b/app/Services/NameFixing/Checkers/TvNameChecker.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Services\NameFixing\Checkers; -use App\Services\NameFixing\DTO\NameFixResult; +use App\Services\NameFixing\Data\NameFixResult; use App\Services\NameFixing\Patterns\TvPatterns; /** diff --git a/app/Services/NameFixing/Contracts/NameCheckerInterface.php b/app/Services/NameFixing/Contracts/NameCheckerInterface.php index 543dc2a47..9b3ea1407 100644 --- a/app/Services/NameFixing/Contracts/NameCheckerInterface.php +++ b/app/Services/NameFixing/Contracts/NameCheckerInterface.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Services\NameFixing\Contracts; -use App\Services\NameFixing\DTO\NameFixResult; +use App\Services\NameFixing\Data\NameFixResult; /** * Interface for name checker strategies. diff --git a/app/Services/NameFixing/DTO/NameFixResult.php b/app/Services/NameFixing/Data/NameFixResult.php similarity index 80% rename from app/Services/NameFixing/DTO/NameFixResult.php rename to app/Services/NameFixing/Data/NameFixResult.php index 6225719cf..967dc1fa6 100644 --- a/app/Services/NameFixing/DTO/NameFixResult.php +++ b/app/Services/NameFixing/Data/NameFixResult.php @@ -2,7 +2,10 @@ declare(strict_types=1); -namespace App\Services\NameFixing\DTO; +namespace App\Services\NameFixing\Data; + +use Spatie\LaravelData\Data; +use Spatie\TypeScriptTransformer\Attributes\TypeScript; /** * Data Transfer Object for name fix results. @@ -10,18 +13,19 @@ namespace App\Services\NameFixing\DTO; * Encapsulates the result of a name fixing operation, including the new name, * method used, and optional metadata. */ -final class NameFixResult +#[TypeScript] +final class NameFixResult extends Data { /** * @param array $metadata */ public function __construct( - public readonly string $newName, - public readonly string $method, - public readonly string $checkerName, - public readonly int $preDbId = 0, - public readonly float $confidence = 1.0, - public readonly array $metadata = [], + public string $newName, + public string $method, + public string $checkerName, + public int $preDbId = 0, + public float $confidence = 1.0, + public array $metadata = [], ) {} /** diff --git a/app/Services/NameFixing/Extractors/FileNameExtractor.php b/app/Services/NameFixing/Extractors/FileNameExtractor.php index 756d4c1b7..032541ff4 100644 --- a/app/Services/NameFixing/Extractors/FileNameExtractor.php +++ b/app/Services/NameFixing/Extractors/FileNameExtractor.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Services\NameFixing\Extractors; -use App\Services\NameFixing\DTO\NameFixResult; +use App\Services\NameFixing\Data\NameFixResult; use App\Services\NameFixing\FileNameCleaner; /** diff --git a/app/Services/NameFixing/Extractors/NfoNameExtractor.php b/app/Services/NameFixing/Extractors/NfoNameExtractor.php index 3493ceb84..e97b9c7f9 100644 --- a/app/Services/NameFixing/Extractors/NfoNameExtractor.php +++ b/app/Services/NameFixing/Extractors/NfoNameExtractor.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Services\NameFixing\Extractors; -use App\Services\NameFixing\DTO\NameFixResult; +use App\Services\NameFixing\Data\NameFixResult; /** * Extracts release names from NFO content. diff --git a/app/Services/NameFixing/NameCheckerService.php b/app/Services/NameFixing/NameCheckerService.php index 21f3faac6..2d3619dc9 100644 --- a/app/Services/NameFixing/NameCheckerService.php +++ b/app/Services/NameFixing/NameCheckerService.php @@ -9,7 +9,7 @@ use App\Services\NameFixing\Checkers\GameNameChecker; use App\Services\NameFixing\Checkers\MovieNameChecker; use App\Services\NameFixing\Checkers\TvNameChecker; use App\Services\NameFixing\Contracts\NameCheckerInterface; -use App\Services\NameFixing\DTO\NameFixResult; +use App\Services\NameFixing\Data\NameFixResult; use Illuminate\Support\Collection; /** diff --git a/app/Services/ReleaseProcessingService.php b/app/Services/ReleaseProcessingService.php index 87e024459..f0e1117f1 100644 --- a/app/Services/ReleaseProcessingService.php +++ b/app/Services/ReleaseProcessingService.php @@ -17,9 +17,9 @@ use App\Services\NNTP\NNTPService; use App\Services\Nzb\NzbService; use App\Services\Releases\ReleaseBrowseService; use App\Services\Releases\ReleaseManagementService; -use App\Support\DTOs\ProcessReleasesSettings; -use App\Support\DTOs\ReleaseCreationResult; -use App\Support\DTOs\ReleaseDeleteStats; +use App\Support\Data\ProcessReleasesSettings; +use App\Support\Data\ReleaseCreationResult; +use App\Support\Data\ReleaseDeleteStats; use App\Support\ReleaseSearchIndexSync; use DateTimeInterface; use Illuminate\Database\QueryException; @@ -114,7 +114,7 @@ final class ReleaseProcessingService $dbSettings[$key] = Settings::settingValue($key); } - return ProcessReleasesSettings::fromDatabase($dbSettings); + return ProcessReleasesSettings::forDatabase($dbSettings); } /** diff --git a/app/Support/BookMatchScorer.php b/app/Support/BookMatchScorer.php index 08ecef68c..f57f45d34 100644 --- a/app/Support/BookMatchScorer.php +++ b/app/Support/BookMatchScorer.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Support; use App\Models\BookInfo; -use App\Support\DTOs\BookParseResult; +use App\Support\Data\BookParseResult; class BookMatchScorer { diff --git a/app/Support/DTOs/ProcessReleasesSettings.php b/app/Support/DTOs/ProcessReleasesSettings.php deleted file mode 100644 index 33e6ba7ab..000000000 --- a/app/Support/DTOs/ProcessReleasesSettings.php +++ /dev/null @@ -1,109 +0,0 @@ - 2, - 'crossPostTime' => 2, - 'releaseCreationLimit' => 1000, - 'completion' => 0, - 'collectionTimeout' => 48, - 'maxSizeToFormRelease' => 0, - 'minSizeToFormRelease' => 0, - 'minFilesToFormRelease' => 0, - 'releaseRetentionDays' => 0, - 'deletePasswordedRelease' => false, - 'miscOtherRetentionHours' => 0, - 'miscHashedRetentionHours' => 0, - 'partRetentionHours' => 24, - 'lastRunTime' => null, - ]; - - /** - * Create settings from database values. - * - * @param array $dbSettings - */ - public static function fromDatabase(array $dbSettings): self - { - $getInt = static fn (string $key, int $default): int => ($dbSettings[$key] ?? '') !== '' ? (int) $dbSettings[$key] : $default; - - $completion = min(100, $getInt('completionpercent', self::DEFAULTS['completion'])); - - return new self( - collectionDelayTime: $getInt('delaytime', self::DEFAULTS['collectionDelayTime']), - crossPostTime: $getInt('crossposttime', self::DEFAULTS['crossPostTime']), - releaseCreationLimit: $getInt('maxnzbsprocessed', self::DEFAULTS['releaseCreationLimit']), - completion: $completion, - collectionTimeout: $getInt('collection_timeout', self::DEFAULTS['collectionTimeout']), - maxSizeToFormRelease: $getInt('maxsizetoformrelease', self::DEFAULTS['maxSizeToFormRelease']), - minSizeToFormRelease: $getInt('minsizetoformrelease', self::DEFAULTS['minSizeToFormRelease']), - minFilesToFormRelease: $getInt('minfilestoformrelease', self::DEFAULTS['minFilesToFormRelease']), - releaseRetentionDays: $getInt('releaseretentiondays', self::DEFAULTS['releaseRetentionDays']), - deletePasswordedRelease: ((int) ($dbSettings['deletepasswordedrelease'] ?? 0)) === 1, - miscOtherRetentionHours: $getInt('miscotherretentionhours', self::DEFAULTS['miscOtherRetentionHours']), - miscHashedRetentionHours: $getInt('mischashedretentionhours', self::DEFAULTS['miscHashedRetentionHours']), - partRetentionHours: $getInt('partretentionhours', self::DEFAULTS['partRetentionHours']), - lastRunTime: ! empty($dbSettings['last_run_time']) ? (string) $dbSettings['last_run_time'] : null, - ); - } - - /** - * Check if completion percentage is valid. - */ - public function hasValidCompletion(): bool - { - return $this->completion >= 0 && $this->completion <= 100; - } - - /** - * Check if retention cleanup is enabled. - */ - public function hasRetentionCleanup(): bool - { - return $this->releaseRetentionDays > 0; - } - - /** - * Check if cross-post detection is enabled. - */ - public function hasCrossPostDetection(): bool - { - return $this->crossPostTime > 0; - } - - /** - * Check if completion-based cleanup is enabled. - */ - public function hasCompletionCleanup(): bool - { - return $this->completion > 0; - } -} diff --git a/app/Support/DTOs/ReleaseCreationResult.php b/app/Support/DTOs/ReleaseCreationResult.php deleted file mode 100644 index aa13b09b6..000000000 --- a/app/Support/DTOs/ReleaseCreationResult.php +++ /dev/null @@ -1,58 +0,0 @@ -added + $this->dupes; - } - - /** - * Check if any releases were added. - */ - public function hasAddedReleases(): bool - { - return $this->added > 0; - } - - /** - * Create from array. - * - * @param array{added?: int, dupes?: int} $data - */ - public static function fromArray(array $data): self - { - return new self( - added: $data['added'] ?? 0, - dupes: $data['dupes'] ?? 0, - ); - } - - /** - * Convert to array. - * - * @return array{added: int, dupes: int} - */ - public function toArray(): array - { - return [ - 'added' => $this->added, - 'dupes' => $this->dupes, - ]; - } -} diff --git a/app/Support/DTOs/ReleaseDeleteStats.php b/app/Support/DTOs/ReleaseDeleteStats.php deleted file mode 100644 index c4458218f..000000000 --- a/app/Support/DTOs/ReleaseDeleteStats.php +++ /dev/null @@ -1,105 +0,0 @@ - $this->retention, - 'password' => $this->password, - 'duplicate' => $this->duplicate, - 'completion' => $this->completion, - 'disabledCategory' => $this->disabledCategory, - 'categoryMinSize' => $this->categoryMinSize, - 'disabledGenre' => $this->disabledGenre, - 'miscOther' => $this->miscOther, - 'miscHashed' => $this->miscHashed, - ]; - - if (isset($values[$field])) { - $values[$field]++; - } - - return new self(...$values); - } - - /** - * Get the total number of deleted releases. - */ - public function total(): int - { - return $this->retention - + $this->password - + $this->duplicate - + $this->completion - + $this->disabledCategory - + $this->categoryMinSize - + $this->disabledGenre - + $this->miscOther - + $this->miscHashed; - } - - /** - * Convert to array representation. - * - * @return array - */ - public function toArray(): array - { - return [ - 'retention' => $this->retention, - 'password' => $this->password, - 'duplicate' => $this->duplicate, - 'completion' => $this->completion, - 'disabledCategory' => $this->disabledCategory, - 'categoryMinSize' => $this->categoryMinSize, - 'disabledGenre' => $this->disabledGenre, - 'miscOther' => $this->miscOther, - 'miscHashed' => $this->miscHashed, - ]; - } - - /** - * Create from an array of values. - * - * @param array $data - */ - public static function fromArray(array $data): self - { - return new self( - retention: $data['retention'] ?? 0, - password: $data['password'] ?? 0, - duplicate: $data['duplicate'] ?? 0, - completion: $data['completion'] ?? 0, - disabledCategory: $data['disabledCategory'] ?? 0, - categoryMinSize: $data['categoryMinSize'] ?? 0, - disabledGenre: $data['disabledGenre'] ?? 0, - miscOther: $data['miscOther'] ?? 0, - miscHashed: $data['miscHashed'] ?? 0, - ); - } -} diff --git a/app/Support/DTOs/SteamGameData.php b/app/Support/DTOs/SteamGameData.php deleted file mode 100644 index f30e7b34b..000000000 --- a/app/Support/DTOs/SteamGameData.php +++ /dev/null @@ -1,313 +0,0 @@ - $screenshots Screenshot URLs - * @param array $movies Movie/trailer data - * @param string|null $trailerUrl Primary trailer URL - * @param string|null $publisher Publisher name(s) - * @param array $developers Developer names - * @param string|null $releaseDate Release date (Y-m-d format) - * @param array $genres Genre names - * @param array $categories Category names (multiplayer, etc.) - * @param int|null $metacriticScore Metacritic score (0-100) - * @param string|null $metacriticUrl Metacritic URL - * @param SteamPriceData|null $price Price information - * @param array $platforms Supported platforms - * @param array $requirements System requirements - * @param array $dlcIds DLC App IDs - * @param int|null $achievementCount Total achievements - * @param int|null $recommendationCount Total recommendations - * @param string|null $website Official website URL - * @param string|null $supportUrl Support URL - * @param string $storeUrl Steam store URL - */ - public function __construct( - public int $steamId, - public string $title, - public string $type = 'game', - public ?string $description = null, - public ?string $detailedDescription = null, - public ?string $about = null, - public ?string $coverUrl = null, - public ?string $backdropUrl = null, - public array $screenshots = [], - public array $movies = [], - public ?string $trailerUrl = null, - public ?string $publisher = null, - public array $developers = [], - public ?string $releaseDate = null, - public array $genres = [], - public array $categories = [], - public ?int $metacriticScore = null, - public ?string $metacriticUrl = null, - public ?SteamPriceData $price = null, - public array $platforms = [], - public array $requirements = [], - public array $dlcIds = [], - public ?int $achievementCount = null, - public ?int $recommendationCount = null, - public ?string $website = null, - public ?string $supportUrl = null, - public string $storeUrl = '', - ) {} - - /** - * Create from Steam API response array. - * - * @param array $data - */ - public static function fromApiResponse(array $data, int $appId): self - { - // Parse screenshots - $screenshots = []; - if (! empty($data['screenshots'])) { - foreach ($data['screenshots'] as $ss) { - $screenshots[] = [ - 'thumbnail' => $ss['path_thumbnail'] ?? null, - 'full' => $ss['path_full'] ?? null, - ]; - } - } - - // Parse movies - $movies = []; - $trailerUrl = null; - if (! empty($data['movies'])) { - foreach ($data['movies'] as $movie) { - $mp4Url = $movie['mp4']['max'] ?? ($movie['mp4']['480'] ?? null); - $movies[] = [ - 'id' => $movie['id'] ?? null, - 'name' => $movie['name'] ?? null, - 'thumbnail' => $movie['thumbnail'] ?? null, - 'webm' => $movie['webm']['max'] ?? ($movie['webm']['480'] ?? null), - 'mp4' => $mp4Url, - ]; - if ($trailerUrl === null && ! empty($mp4Url)) { - $trailerUrl = $mp4Url; - } - } - } - - // Parse genres - $genres = []; - if (! empty($data['genres'])) { - foreach ($data['genres'] as $genre) { - if (! empty($genre['description'])) { - $genres[] = $genre['description']; - } - } - } - - // Parse categories - $categories = []; - if (! empty($data['categories'])) { - foreach ($data['categories'] as $cat) { - if (! empty($cat['description'])) { - $categories[] = $cat['description']; - } - } - } - - // Parse platforms - $platforms = []; - if (! empty($data['platforms'])) { - if ($data['platforms']['windows'] ?? false) { - $platforms[] = 'Windows'; - } - if ($data['platforms']['mac'] ?? false) { - $platforms[] = 'Mac'; - } - if ($data['platforms']['linux'] ?? false) { - $platforms[] = 'Linux'; - } - } - - // Parse requirements - $requirements = []; - if (! empty($data['pc_requirements']) && ! is_array($data['pc_requirements']) === false) { - if (is_array($data['pc_requirements'])) { - $requirements['pc'] = [ - 'minimum' => $data['pc_requirements']['minimum'] ?? null, - 'recommended' => $data['pc_requirements']['recommended'] ?? null, - ]; - } - } - if (! empty($data['mac_requirements']) && is_array($data['mac_requirements'])) { - $requirements['mac'] = [ - 'minimum' => $data['mac_requirements']['minimum'] ?? null, - 'recommended' => $data['mac_requirements']['recommended'] ?? null, - ]; - } - if (! empty($data['linux_requirements']) && is_array($data['linux_requirements'])) { - $requirements['linux'] = [ - 'minimum' => $data['linux_requirements']['minimum'] ?? null, - 'recommended' => $data['linux_requirements']['recommended'] ?? null, - ]; - } - - // Parse price - $price = null; - if (isset($data['price_overview'])) { - $price = SteamPriceData::fromApiResponse($data['price_overview']); - } elseif ($data['is_free'] ?? false) { - $price = SteamPriceData::free(); - } - - // Parse release date - $releaseDate = null; - if (! empty($data['release_date']['date'])) { - try { - $releaseDate = Carbon::parse($data['release_date']['date'])->format('Y-m-d'); - } catch (\Exception $e) { - $releaseDate = $data['release_date']['date']; - } - } - - // Parse publisher - $publisher = null; - if (! empty($data['publishers'])) { - $publisher = implode(', ', array_filter(array_map('strval', $data['publishers']))); - } - - // Parse developers - $developers = []; - if (! empty($data['developers'])) { - $developers = array_values(array_filter(array_map('strval', $data['developers']))); - } - - return new self( - steamId: $appId, - title: $data['name'] ?? '', - type: $data['type'] ?? 'game', - description: $data['short_description'] ?? null, - detailedDescription: $data['detailed_description'] ?? null, - about: $data['about_the_game'] ?? null, - coverUrl: $data['header_image'] ?? null, - backdropUrl: $data['background'] ?? ($data['background_raw'] ?? null), - screenshots: $screenshots, - movies: $movies, - trailerUrl: $trailerUrl, - publisher: $publisher, - developers: $developers, - releaseDate: $releaseDate, - genres: $genres, - categories: $categories, - metacriticScore: $data['metacritic']['score'] ?? null, - metacriticUrl: $data['metacritic']['url'] ?? null, - price: $price, - platforms: $platforms, - requirements: $requirements, - dlcIds: $data['dlc'] ?? [], - achievementCount: $data['achievements']['total'] ?? null, - recommendationCount: $data['recommendations']['total'] ?? null, - website: $data['website'] ?? null, - supportUrl: $data['support_info']['url'] ?? null, - storeUrl: 'https://store.steampowered.com/app/'.$appId, - ); - } - - /** - * Convert to array format compatible with existing GamesInfo model. - * - * @return array - */ - public function toGamesInfoArray(): array - { - return [ - 'title' => $this->title, - 'asin' => (string) $this->steamId, - 'url' => $this->storeUrl, - 'publisher' => $this->publisher ?? 'Unknown', - 'releasedate' => $this->releaseDate, - 'review' => $this->description ?? 'No description available', - 'cover' => ! empty($this->coverUrl) ? 1 : 0, - 'backdrop' => ! empty($this->backdropUrl) ? 1 : 0, - 'trailer' => $this->trailerUrl ?? '', - 'classused' => 'Steam', - 'esrb' => $this->metacriticScore !== null ? (string) $this->metacriticScore : 'Not Rated', - 'coverurl' => $this->coverUrl, - 'backdropurl' => $this->backdropUrl, - 'genres' => implode(',', $this->genres), - ]; - } - - /** - * Check if this is a game (not DLC, video, etc.). - */ - public function isGame(): bool - { - return in_array($this->type, ['game', 'demo'], true); - } - - /** - * Check if game is free. - */ - public function isFree(): bool - { - return $this->price !== null && $this->price->final <= 0; - } - - /** - * Get primary genre. - */ - public function getPrimaryGenre(): ?string - { - return $this->genres[0] ?? null; - } - - /** - * Check if game supports a platform. - */ - public function supportsPlatform(string $platform): bool - { - return in_array($platform, $this->platforms, true); - } - - /** - * Check if game has multiplayer. - */ - public function hasMultiplayer(): bool - { - $multiplayerCategories = ['Multi-player', 'Online Multi-Player', 'Online Co-op', 'Local Multi-Player', 'Local Co-op']; - - return ! empty(array_intersect($multiplayerCategories, $this->categories)); - } - - /** - * Check if game supports Steam Workshop. - */ - public function hasSteamWorkshop(): bool - { - return in_array('Steam Workshop', $this->categories, true); - } - - /** - * Get formatted genres string. - */ - public function getGenresString(): string - { - return implode(', ', $this->genres); - } -} diff --git a/app/Support/DTOs/BookParseResult.php b/app/Support/Data/BookParseResult.php similarity index 77% rename from app/Support/DTOs/BookParseResult.php rename to app/Support/Data/BookParseResult.php index 03024f5ce..b35593704 100644 --- a/app/Support/DTOs/BookParseResult.php +++ b/app/Support/Data/BookParseResult.php @@ -2,9 +2,13 @@ declare(strict_types=1); -namespace App\Support\DTOs; +namespace App\Support\Data; -final readonly class BookParseResult +use Spatie\LaravelData\Data; +use Spatie\TypeScriptTransformer\Attributes\TypeScript; + +#[TypeScript] +final class BookParseResult extends Data { public function __construct( public string $rawName, diff --git a/app/Support/Data/Factories/SteamGameDataFactory.php b/app/Support/Data/Factories/SteamGameDataFactory.php new file mode 100644 index 000000000..e7d60b13d --- /dev/null +++ b/app/Support/Data/Factories/SteamGameDataFactory.php @@ -0,0 +1,236 @@ + $data + */ + public function make(array $data, int $appId): SteamGameData + { + return new SteamGameData( + steamId: $appId, + title: (string) ($data['name'] ?? ''), + type: (string) ($data['type'] ?? 'game'), + description: $data['short_description'] ?? null, + detailedDescription: $data['detailed_description'] ?? null, + about: $data['about_the_game'] ?? null, + coverUrl: $data['header_image'] ?? null, + backdropUrl: $data['background'] ?? ($data['background_raw'] ?? null), + screenshots: $this->parseScreenshots($data), + movies: $this->parseMovies($data, $trailerUrl), + trailerUrl: $trailerUrl, + publisher: $this->parsePublisher($data), + developers: $this->parseDevelopers($data), + releaseDate: $this->parseReleaseDate($data), + genres: $this->parseGenres($data), + categories: $this->parseCategories($data), + metacriticScore: $data['metacritic']['score'] ?? null, + metacriticUrl: $data['metacritic']['url'] ?? null, + price: $this->parsePrice($data), + platforms: $this->parsePlatforms($data), + requirements: $this->parseRequirements($data), + dlcIds: $data['dlc'] ?? [], + achievementCount: $data['achievements']['total'] ?? null, + recommendationCount: $data['recommendations']['total'] ?? null, + website: $data['website'] ?? null, + supportUrl: $data['support_info']['url'] ?? null, + storeUrl: 'https://store.steampowered.com/app/'.$appId, + ); + } + + /** + * @param array $data + * @return array + */ + private function parseScreenshots(array $data): array + { + $screenshots = []; + if (! empty($data['screenshots']) && is_array($data['screenshots'])) { + foreach ($data['screenshots'] as $ss) { + $screenshots[] = [ + 'thumbnail' => $ss['path_thumbnail'] ?? null, + 'full' => $ss['path_full'] ?? null, + ]; + } + } + + return $screenshots; + } + + /** + * @param array $data + * @return array + */ + private function parseMovies(array $data, ?string &$trailerUrl): array + { + $movies = []; + $trailerUrl = null; + if (! empty($data['movies']) && is_array($data['movies'])) { + foreach ($data['movies'] as $movie) { + $mp4Url = $movie['mp4']['max'] ?? ($movie['mp4']['480'] ?? null); + $movies[] = [ + 'id' => $movie['id'] ?? null, + 'name' => $movie['name'] ?? null, + 'thumbnail' => $movie['thumbnail'] ?? null, + 'webm' => $movie['webm']['max'] ?? ($movie['webm']['480'] ?? null), + 'mp4' => $mp4Url, + ]; + if ($trailerUrl === null && ! empty($mp4Url)) { + $trailerUrl = $mp4Url; + } + } + } + + return $movies; + } + + /** + * @param array $data + * @return array + */ + private function parseGenres(array $data): array + { + $genres = []; + if (! empty($data['genres']) && is_array($data['genres'])) { + foreach ($data['genres'] as $genre) { + if (! empty($genre['description'])) { + $genres[] = (string) $genre['description']; + } + } + } + + return $genres; + } + + /** + * @param array $data + * @return array + */ + private function parseCategories(array $data): array + { + $categories = []; + if (! empty($data['categories']) && is_array($data['categories'])) { + foreach ($data['categories'] as $cat) { + if (! empty($cat['description'])) { + $categories[] = (string) $cat['description']; + } + } + } + + return $categories; + } + + /** + * @param array $data + * @return array + */ + private function parsePlatforms(array $data): array + { + $platforms = []; + if (! empty($data['platforms']) && is_array($data['platforms'])) { + if ($data['platforms']['windows'] ?? false) { + $platforms[] = 'Windows'; + } + if ($data['platforms']['mac'] ?? false) { + $platforms[] = 'Mac'; + } + if ($data['platforms']['linux'] ?? false) { + $platforms[] = 'Linux'; + } + } + + return $platforms; + } + + /** + * @param array $data + * @return array + */ + private function parseRequirements(array $data): array + { + $requirements = []; + foreach (['pc' => 'pc_requirements', 'mac' => 'mac_requirements', 'linux' => 'linux_requirements'] as $key => $field) { + if (! empty($data[$field]) && is_array($data[$field])) { + $requirements[$key] = [ + 'minimum' => $data[$field]['minimum'] ?? null, + 'recommended' => $data[$field]['recommended'] ?? null, + ]; + } + } + + return $requirements; + } + + /** + * @param array $data + */ + private function parsePrice(array $data): ?SteamPriceData + { + if (isset($data['price_overview']) && is_array($data['price_overview'])) { + return SteamPriceData::fromApiResponse($data['price_overview']); + } + + if ($data['is_free'] ?? false) { + return SteamPriceData::free(); + } + + return null; + } + + /** + * @param array $data + */ + private function parseReleaseDate(array $data): ?string + { + if (empty($data['release_date']['date'])) { + return null; + } + + try { + return Carbon::parse((string) $data['release_date']['date'])->format('Y-m-d'); + } catch (\Exception) { + return (string) $data['release_date']['date']; + } + } + + /** + * @param array $data + */ + private function parsePublisher(array $data): ?string + { + if (empty($data['publishers']) || ! is_array($data['publishers'])) { + return null; + } + + return implode(', ', array_filter(array_map('strval', $data['publishers']))); + } + + /** + * @param array $data + * @return array + */ + private function parseDevelopers(array $data): array + { + if (empty($data['developers']) || ! is_array($data['developers'])) { + return []; + } + + return array_values(array_filter(array_map('strval', $data['developers']))); + } +} diff --git a/app/Support/Data/ProcessReleasesSettings.php b/app/Support/Data/ProcessReleasesSettings.php new file mode 100644 index 000000000..68dc60cc2 --- /dev/null +++ b/app/Support/Data/ProcessReleasesSettings.php @@ -0,0 +1,92 @@ +completion > 100) { + $this->completion = 100; + } + } + + /** + * Build settings from a raw Settings table array (mixed snake-case keys, + * stringly-typed values, possible nulls/empty strings). + * + * @param array $dbSettings + */ + public static function forDatabase(array $dbSettings): self + { + $getInt = static fn (string $key, int $default): int => (isset($dbSettings[$key]) && $dbSettings[$key] !== '') + ? (int) $dbSettings[$key] + : $default; + + return new self( + collectionDelayTime: $getInt('delaytime', 2), + crossPostTime: $getInt('crossposttime', 2), + releaseCreationLimit: $getInt('maxnzbsprocessed', 1000), + completion: $getInt('completionpercent', 0), + collectionTimeout: $getInt('collection_timeout', 48), + maxSizeToFormRelease: $getInt('maxsizetoformrelease', 0), + minSizeToFormRelease: $getInt('minsizetoformrelease', 0), + minFilesToFormRelease: $getInt('minfilestoformrelease', 0), + releaseRetentionDays: $getInt('releaseretentiondays', 0), + deletePasswordedRelease: ((int) ($dbSettings['deletepasswordedrelease'] ?? 0)) === 1, + miscOtherRetentionHours: $getInt('miscotherretentionhours', 0), + miscHashedRetentionHours: $getInt('mischashedretentionhours', 0), + partRetentionHours: $getInt('partretentionhours', 24), + lastRunTime: ! empty($dbSettings['last_run_time']) ? (string) $dbSettings['last_run_time'] : null, + ); + } + + public function hasValidCompletion(): bool + { + return $this->completion >= 0 && $this->completion <= 100; + } + + public function hasRetentionCleanup(): bool + { + return $this->releaseRetentionDays > 0; + } + + public function hasCrossPostDetection(): bool + { + return $this->crossPostTime > 0; + } + + public function hasCompletionCleanup(): bool + { + return $this->completion > 0; + } +} diff --git a/app/Support/Data/ReleaseCreationResult.php b/app/Support/Data/ReleaseCreationResult.php new file mode 100644 index 000000000..e2f04fd6f --- /dev/null +++ b/app/Support/Data/ReleaseCreationResult.php @@ -0,0 +1,30 @@ +added + $this->dupes; + } + + public function hasAddedReleases(): bool + { + return $this->added > 0; + } +} diff --git a/app/Support/Data/ReleaseDeleteStats.php b/app/Support/Data/ReleaseDeleteStats.php new file mode 100644 index 000000000..d261315ac --- /dev/null +++ b/app/Support/Data/ReleaseDeleteStats.php @@ -0,0 +1,62 @@ + $this->retention, + 'password' => $this->password, + 'duplicate' => $this->duplicate, + 'completion' => $this->completion, + 'disabledCategory' => $this->disabledCategory, + 'categoryMinSize' => $this->categoryMinSize, + 'disabledGenre' => $this->disabledGenre, + 'miscOther' => $this->miscOther, + 'miscHashed' => $this->miscHashed, + ]; + if (array_key_exists($field, $values)) { + $values[$field]++; + } + + return new self(...$values); + } + + public function total(): int + { + return $this->retention + + $this->password + + $this->duplicate + + $this->completion + + $this->disabledCategory + + $this->categoryMinSize + + $this->disabledGenre + + $this->miscOther + + $this->miscHashed; + } +} diff --git a/app/Support/Data/SteamGameData.php b/app/Support/Data/SteamGameData.php new file mode 100644 index 000000000..cf462d24c --- /dev/null +++ b/app/Support/Data/SteamGameData.php @@ -0,0 +1,155 @@ + $screenshots + * @param array $movies + * @param array $developers + * @param array $genres + * @param array $categories + * @param array $platforms + * @param array $requirements + * @param array $dlcIds + */ + public function __construct( + public int $steamId, + public string $title, + public string $type = 'game', + public ?string $description = null, + public ?string $detailedDescription = null, + public ?string $about = null, + public ?string $coverUrl = null, + public ?string $backdropUrl = null, + public array $screenshots = [], + public array $movies = [], + public ?string $trailerUrl = null, + public ?string $publisher = null, + public array $developers = [], + public ?string $releaseDate = null, + public array $genres = [], + public array $categories = [], + public ?int $metacriticScore = null, + public ?string $metacriticUrl = null, + public ?SteamPriceData $price = null, + public array $platforms = [], + public array $requirements = [], + public array $dlcIds = [], + public ?int $achievementCount = null, + public ?int $recommendationCount = null, + public ?string $website = null, + public ?string $supportUrl = null, + public string $storeUrl = '', + ) {} + + /** + * Create from Steam API response array. + * + * Thin facade kept for backwards compatibility; delegates to + * {@see SteamGameDataFactory::make()}. + * + * @param array $data + */ + public static function fromApiResponse(array $data, int $appId): self + { + return (new SteamGameDataFactory)->make($data, $appId); + } + + /** + * Convert to array format compatible with existing GamesInfo model. + * + * @return array + */ + public function toGamesInfoArray(): array + { + return [ + 'title' => $this->title, + 'asin' => (string) $this->steamId, + 'url' => $this->storeUrl, + 'publisher' => $this->publisher ?? 'Unknown', + 'releasedate' => $this->releaseDate, + 'review' => $this->description ?? 'No description available', + 'cover' => ! empty($this->coverUrl) ? 1 : 0, + 'backdrop' => ! empty($this->backdropUrl) ? 1 : 0, + 'trailer' => $this->trailerUrl ?? '', + 'classused' => 'Steam', + 'esrb' => $this->metacriticScore !== null ? (string) $this->metacriticScore : 'Not Rated', + 'coverurl' => $this->coverUrl, + 'backdropurl' => $this->backdropUrl, + 'genres' => implode(',', $this->genres), + ]; + } + + /** + * Check if this is a game (not DLC, video, etc.). + */ + public function isGame(): bool + { + return in_array($this->type, ['game', 'demo'], true); + } + + /** + * Check if game is free. + */ + public function isFree(): bool + { + return $this->price !== null && $this->price->final <= 0; + } + + /** + * Get primary genre. + */ + public function getPrimaryGenre(): ?string + { + return $this->genres[0] ?? null; + } + + /** + * Check if game supports a platform. + */ + public function supportsPlatform(string $platform): bool + { + return in_array($platform, $this->platforms, true); + } + + /** + * Check if game has multiplayer. + */ + public function hasMultiplayer(): bool + { + $multiplayerCategories = ['Multi-player', 'Online Multi-Player', 'Online Co-op', 'Local Multi-Player', 'Local Co-op']; + + return ! empty(array_intersect($multiplayerCategories, $this->categories)); + } + + /** + * Check if game supports Steam Workshop. + */ + public function hasSteamWorkshop(): bool + { + return in_array('Steam Workshop', $this->categories, true); + } + + /** + * Get formatted genres string. + */ + public function getGenresString(): string + { + return implode(', ', $this->genres); + } +} diff --git a/app/Support/DTOs/SteamPriceData.php b/app/Support/Data/SteamPriceData.php similarity index 67% rename from app/Support/DTOs/SteamPriceData.php rename to app/Support/Data/SteamPriceData.php index 7a99d0397..cb45e6a43 100644 --- a/app/Support/DTOs/SteamPriceData.php +++ b/app/Support/Data/SteamPriceData.php @@ -2,24 +2,29 @@ declare(strict_types=1); -namespace App\Support\DTOs; +namespace App\Support\Data; + +use Spatie\LaravelData\Attributes\MapOutputName; +use Spatie\LaravelData\Data; +use Spatie\TypeScriptTransformer\Attributes\TypeScript; /** * Data Transfer Object for Steam Price information. */ -final readonly class SteamPriceData +#[TypeScript] +final class SteamPriceData extends Data { public function __construct( public string $currency, public float $initial, public float $final, + #[MapOutputName('discount_percent')] public int $discountPercent, + #[MapOutputName('formatted')] public ?string $formattedPrice = null, ) {} /** - * Create from Steam API price_overview response. - * * @param array $data */ public static function fromApiResponse(array $data): self @@ -33,9 +38,6 @@ final readonly class SteamPriceData ); } - /** - * Create a free price instance. - */ public static function free(): self { return new self( @@ -47,59 +49,30 @@ final readonly class SteamPriceData ); } - /** - * Check if currently on sale. - */ public function isOnSale(): bool { return $this->discountPercent > 0; } - /** - * Check if free. - */ public function isFree(): bool { return $this->final <= 0; } - /** - * Get savings amount. - */ public function getSavings(): float { return max(0, $this->initial - $this->final); } - /** - * Get formatted display price. - */ public function getDisplayPrice(): string { if ($this->formattedPrice !== null) { return $this->formattedPrice; } - if ($this->isFree()) { return 'Free'; } return sprintf('%s %.2f', $this->currency, $this->final); } - - /** - * Convert to array. - * - * @return array - */ - public function toArray(): array - { - return [ - 'currency' => $this->currency, - 'initial' => $this->initial, - 'final' => $this->final, - 'discount_percent' => $this->discountPercent, - 'formatted' => $this->formattedPrice, - ]; - } } diff --git a/app/Transformers/ApiTransformer.php b/app/Transformers/ApiTransformer.php deleted file mode 100644 index 7ffb5565a..000000000 --- a/app/Transformers/ApiTransformer.php +++ /dev/null @@ -1,146 +0,0 @@ -user = $user; - } - - /** - * Transform a release into an API response array. - * - * @param Release|\stdClass $release The release to transform (can be Eloquent model or stdClass from raw query) - * @return array The transformed release data - */ - public function transform(Release|\stdClass $release): array - { - $data = $this->getBaseData($release); - - $categoriesId = $this->getValue($release, 'categories_id'); - - if (\in_array($categoriesId, Category::MOVIES_GROUP, true)) { - return array_merge($data, $this->getMovieSpecificData($release)); - } - - if (\in_array($categoriesId, Category::TV_GROUP, true)) { - return array_merge($data, $this->getTvSpecificData($release)); - } - - return $data; - } - - /** - * Get a value from the release object, handling both Eloquent models and stdClass objects. - */ - protected function getValue(Release|\stdClass $release, string $key, mixed $default = null): mixed - { - if ($release instanceof Release) { - return $release->{$key} ?? $default; - } - - return $release->{$key} ?? $default; - } - - /** - * Get base data common to all releases. - * - * @return array - */ - protected function getBaseData(Release|\stdClass $release): array - { - return [ - 'title' => $this->getValue($release, 'searchname'), - 'details' => $this->getDetailsUrl($this->getValue($release, 'guid')), - 'url' => $this->getDownloadUrl($this->getValue($release, 'guid')), - 'category' => $this->getValue($release, 'categories_id'), - 'category_name' => $this->getValue($release, 'category_name'), - 'added' => Carbon::parse($this->getValue($release, 'adddate'))->toRssString(), - 'size' => $this->getValue($release, 'size'), - 'files' => $this->getValue($release, 'totalpart'), - 'grabs' => $this->nullIfZero($this->getValue($release, 'grabs')), - 'comments' => $this->nullIfZero($this->getValue($release, 'comments')), - 'password' => $this->getValue($release, 'passwordstatus'), - 'usenetdate' => Carbon::parse($this->getValue($release, 'postdate'))->toRssString(), - ]; - } - - /** - * Get movie-specific data fields. - * - * @return array - */ - protected function getMovieSpecificData(Release|\stdClass $release): array - { - return [ - 'imdbid' => $this->nullIfZero($this->getValue($release, 'imdbid')), - 'tmdbid' => $this->nullIfZero($this->getValue($release, 'tmdbid')), - 'traktid' => $this->nullIfZero($this->getValue($release, 'traktid')), - ]; - } - - /** - * Get TV-specific data fields. - * - * @return array - */ - protected function getTvSpecificData(Release|\stdClass $release): array - { - return [ - 'episode_title' => $this->getValue($release, 'title') ?? $this->null(), - 'season' => $this->getValue($release, 'series') ?? $this->null(), - 'episode' => $this->getValue($release, 'episode') ?? $this->null(), - 'tvairdate' => $this->getValue($release, 'firstaired') ?? $this->null(), - 'tvdbid' => $this->nullIfZero($this->getValue($release, 'tvdb')), - 'traktid' => $this->nullIfZero($this->getValue($release, 'trakt')), - 'tvrageid' => $this->nullIfZero($this->getValue($release, 'tvrage')), - 'tvmazeid' => $this->nullIfZero($this->getValue($release, 'tvmaze')), - 'imdbid' => $this->nullIfZero($this->getValue($release, 'imdb')), - 'tmdbid' => $this->nullIfZero($this->getValue($release, 'tmdb')), - ]; - } - - /** - * Generate the details URL for a release. - */ - protected function getDetailsUrl(string $guid): string - { - return url('/details/'.$guid); - } - - /** - * Generate the download URL for a release. - */ - protected function getDownloadUrl(string $guid): string - { - return url('/getnzb').'?id='.$guid.'.nzb&r='.$this->user->api_token; - } - - /** - * Return null if the value is zero, otherwise return the value. - * - * @param mixed $value - * @return mixed - */ - protected function nullIfZero($value) - { - return ($value !== null && $value !== 0 && $value !== '' && $value !== '0') ? $value : $this->null(); - } -} diff --git a/app/Transformers/CategoryTransformer.php b/app/Transformers/CategoryTransformer.php deleted file mode 100644 index fdd8b83fb..000000000 --- a/app/Transformers/CategoryTransformer.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ - public function transform(RootCategory $category): array - { - return [ - 'id' => $category->id, - 'name' => $category->title, - 'subcategories' => $category->categories()->pluck('title', 'id'), - ]; - } -} diff --git a/app/Transformers/DetailsTransformer.php b/app/Transformers/DetailsTransformer.php deleted file mode 100644 index 2b83c915b..000000000 --- a/app/Transformers/DetailsTransformer.php +++ /dev/null @@ -1,66 +0,0 @@ -user = $user; - } - - /** - * Transform a release into a details array. - * - * @return array - */ - public function transform(Release $release): array - { - // Base data common to all releases - $data = [ - 'title' => $release->searchname, - 'details' => url('/').'/details/'.$release->guid, - 'link' => url('/').'/getnzb?id='.$release->guid.'.nzb&r='.$this->user->api_token, - 'category' => $release->categories_id, - 'category_name' => $release->category_name, - 'added' => Carbon::parse($release->adddate)->toRssString(), - 'size' => $release->size, - 'files' => $release->totalpart, - 'grabs' => $release->grabs, - 'comments' => $release->comments, - 'password' => $release->passwordstatus, - 'usenetdate' => Carbon::parse($release->postdate)->toRssString(), - ]; - - // Add movie-specific data - if (\in_array($release->categories_id, Category::MOVIES_GROUP, true)) { - $data['imdbid'] = $release->imdbid; - } - - // Add TV-specific data - if (\in_array($release->categories_id, Category::TV_GROUP, true)) { - $data['tvairdate'] = $release->firstaired; - $data['tvdbid'] = $release->tvdb; - $data['traktid'] = $release->trakt; - $data['tvrageid'] = $release->tvrage; - $data['tvmazeid'] = $release->tvmaze; - $data['imdbid'] = $release->imdb; // @phpstan-ignore property.notFound - $data['tmdbid'] = $release->tmdb; - } - - return $data; - } -} diff --git a/composer.json b/composer.json index 0f7b18e03..9ac3758bb 100644 --- a/composer.json +++ b/composer.json @@ -66,11 +66,12 @@ "riari/laravel-forum": "8.x-dev", "sentry/sentry": "^4.21", "sentry/sentry-laravel": "^4.13", + "spatie/laravel-data": "^4", "spatie/laravel-directory-cleanup": "^1.10", - "spatie/laravel-fractal": "^6.3", "spatie/laravel-image-optimizer": "^1.8", "spatie/laravel-passkeys": "^1.7", "spatie/laravel-permission": "^7.0.0", + "spatie/laravel-typescript-transformer": "^2", "stechstudio/laravel-zipstream": "^5.4", "symfony/process": "^7.0", "voku/simple_html_dom": "^4.8" diff --git a/composer.lock b/composer.lock index 2274dc60d..8d2bd8c71 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "07cc99b3bba0a8670515889435ba9f56", + "content-hash": "aaf81ade6c4cbf0e3f5d46ebd40b715f", "packages": [ { "name": "aharen/omdbapi", @@ -2935,76 +2935,6 @@ }, "time": "2026-01-23T15:30:45+00:00" }, - { - "name": "league/fractal", - "version": "0.20.2", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/fractal.git", - "reference": "573ca2e0e348a7fe573a3e8fbc29a6588ece8c4e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/fractal/zipball/573ca2e0e348a7fe573a3e8fbc29a6588ece8c4e", - "reference": "573ca2e0e348a7fe573a3e8fbc29a6588ece8c4e", - "shasum": "" - }, - "require": { - "php": ">=7.4" - }, - "require-dev": { - "doctrine/orm": "^2.5", - "illuminate/contracts": "~5.0", - "laminas/laminas-paginator": "~2.12", - "mockery/mockery": "^1.3", - "pagerfanta/pagerfanta": "~1.0.0|~4.0.0", - "phpstan/phpstan": "^1.4", - "phpunit/phpunit": "^9.5", - "squizlabs/php_codesniffer": "~3.4", - "vimeo/psalm": "^4.30" - }, - "suggest": { - "illuminate/pagination": "The Illuminate Pagination component.", - "laminas/laminas-paginator": "Laminas Framework Paginator", - "pagerfanta/pagerfanta": "Pagerfanta Paginator" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.20.x-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Fractal\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Phil Sturgeon", - "email": "me@philsturgeon.uk", - "homepage": "http://philsturgeon.uk/", - "role": "Developer" - } - ], - "description": "Handle the output of complex data structures ready for API output.", - "homepage": "http://fractal.thephpleague.com/", - "keywords": [ - "api", - "json", - "league", - "rest" - ], - "support": { - "issues": "https://github.com/thephpleague/fractal/issues", - "source": "https://github.com/thephpleague/fractal/tree/0.20.2" - }, - "time": "2025-02-14T21:33:14+00:00" - }, { "name": "league/mime-type-detection", "version": "1.16.0", @@ -4646,6 +4576,78 @@ }, "time": "2024-03-15T13:55:21+00:00" }, + { + "name": "phpdocumentor/reflection", + "version": "6.6.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/Reflection.git", + "reference": "c8d36446027506a005103d57265ba5ea56beabfc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/Reflection/zipball/c8d36446027506a005103d57265ba5ea56beabfc", + "reference": "c8d36446027506a005103d57265ba5ea56beabfc", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "nikic/php-parser": "~4.18 || ^5.0", + "php": "8.1.*|8.2.*|8.3.*|8.4.*|8.5.*", + "phpdocumentor/reflection-common": "^2.1", + "phpdocumentor/reflection-docblock": "^5", + "phpdocumentor/type-resolver": "^1.4", + "symfony/polyfill-php80": "^1.28", + "webmozart/assert": "^1.7" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "doctrine/coding-standard": "^13.0", + "eliashaeussler/phpunit-attributes": "^1.8", + "mikey179/vfsstream": "~1.2", + "mockery/mockery": "~1.6.0", + "phpspec/prophecy-phpunit": "^2.4", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^10.5.53", + "psalm/phar": "^6.0", + "rector/rector": "^1.0.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-5.x": "5.3.x-dev", + "dev-6.x": "6.0.x-dev" + } + }, + "autoload": { + "files": [ + "src/php-parser/Modifiers.php" + ], + "psr-4": { + "phpDocumentor\\": "src/phpDocumentor" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Reflection library to do Static Analysis for PHP Projects", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/Reflection/issues", + "source": "https://github.com/phpDocumentor/Reflection/tree/6.6.0" + }, + "time": "2026-04-12T18:07:10+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -6199,68 +6201,6 @@ ], "time": "2026-04-07T12:55:27+00:00" }, - { - "name": "spatie/fractalistic", - "version": "2.11.1", - "source": { - "type": "git", - "url": "https://github.com/spatie/fractalistic.git", - "reference": "85d6ff9a93f00d902e17924bb1475163b373c890" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/fractalistic/zipball/85d6ff9a93f00d902e17924bb1475163b373c890", - "reference": "85d6ff9a93f00d902e17924bb1475163b373c890", - "shasum": "" - }, - "require": { - "league/fractal": "^0.20.1", - "php": "^7.4|^8.0" - }, - "require-dev": { - "illuminate/pagination": "~5.3.0|~5.4.0|^9.0|^13.0", - "pestphp/pest": "^1.22", - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Spatie\\Fractalistic\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" - } - ], - "description": "A developer friendly wrapper around Fractal", - "homepage": "https://github.com/spatie/fractalistic", - "keywords": [ - "api", - "fractal", - "fractalistic", - "spatie", - "transform" - ], - "support": { - "issues": "https://github.com/spatie/fractalistic/issues", - "source": "https://github.com/spatie/fractalistic/tree/2.11.1" - }, - "funding": [ - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2026-02-21T21:10:25+00:00" - }, { "name": "spatie/image-optimizer", "version": "1.8.1", @@ -6316,6 +6256,88 @@ }, "time": "2025-11-26T10:57:19+00:00" }, + { + "name": "spatie/laravel-data", + "version": "4.22.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-data.git", + "reference": "ec254c0ebc3f3b37515cd7449e2dbb10588e606b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-data/zipball/ec254c0ebc3f3b37515cd7449e2dbb10588e606b", + "reference": "ec254c0ebc3f3b37515cd7449e2dbb10588e606b", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "phpdocumentor/reflection": "^6.0", + "spatie/laravel-package-tools": "^1.9.0", + "spatie/php-structure-discoverer": "^2.0" + }, + "require-dev": { + "fakerphp/faker": "^1.14", + "friendsofphp/php-cs-fixer": "^3.0", + "inertiajs/inertia-laravel": "^2.0|^3.0", + "livewire/livewire": "^3.0|^4.0", + "mockery/mockery": "^1.6", + "nesbot/carbon": "^2.63|^3.0", + "orchestra/testbench": "^8.37.0|^9.16|^10.9|^11.0", + "pestphp/pest": "^2.36|^3.8|^4.3", + "pestphp/pest-plugin-laravel": "^2.4|^3.0|^4.0", + "pestphp/pest-plugin-livewire": "^2.1|^3.0|^4.0", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "spatie/invade": "^1.0", + "spatie/laravel-typescript-transformer": "^2.5", + "spatie/pest-plugin-snapshots": "^2.1", + "spatie/test-time": "^1.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\LaravelData\\LaravelDataServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\LaravelData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "role": "Developer" + } + ], + "description": "Create unified resources and data transfer objects", + "homepage": "https://github.com/spatie/laravel-data", + "keywords": [ + "laravel", + "laravel-data", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-data/issues", + "source": "https://github.com/spatie/laravel-data/tree/4.22.1" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-04-27T07:30:53+00:00" + }, { "name": "spatie/laravel-directory-cleanup", "version": "1.11.0", @@ -6389,87 +6411,6 @@ ], "time": "2026-02-22T18:46:38+00:00" }, - { - "name": "spatie/laravel-fractal", - "version": "6.4.0", - "source": { - "type": "git", - "url": "https://github.com/spatie/laravel-fractal.git", - "reference": "d34114259233540dcb405e45dff863164fb426c5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-fractal/zipball/d34114259233540dcb405e45dff863164fb426c5", - "reference": "d34114259233540dcb405e45dff863164fb426c5", - "shasum": "" - }, - "require": { - "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", - "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", - "league/fractal": "^0.20.1|^0.20", - "nesbot/carbon": "^2.63|^3.0", - "php": "^8.0", - "spatie/fractalistic": "^2.9.5|^2.9", - "spatie/laravel-package-tools": "^1.11" - }, - "require-dev": { - "ext-json": "*", - "orchestra/testbench": "^7.0|^8.0|^9.2|^10.0|^11.0", - "pestphp/pest": "^1.22|^2.34|^3.0|^4.0" - }, - "type": "library", - "extra": { - "laravel": { - "aliases": { - "Fractal": "Spatie\\Fractal\\Facades\\Fractal" - }, - "providers": [ - "Spatie\\Fractal\\FractalServiceProvider" - ] - } - }, - "autoload": { - "files": [ - "src/helpers.php" - ], - "psr-4": { - "Spatie\\Fractal\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" - } - ], - "description": "An easy to use Fractal integration for Laravel applications", - "homepage": "https://github.com/spatie/laravel-fractal", - "keywords": [ - "api", - "fractal", - "laravel", - "laravel-fractal", - "lumen", - "spatie", - "transform" - ], - "support": { - "source": "https://github.com/spatie/laravel-fractal/tree/6.4.0" - }, - "funding": [ - { - "url": "https://spatie.be/open-source/support-us", - "type": "custom" - } - ], - "time": "2026-02-21T15:58:07+00:00" - }, { "name": "spatie/laravel-image-optimizer", "version": "1.8.3", @@ -6764,6 +6705,165 @@ ], "time": "2026-04-07T15:19:42+00:00" }, + { + "name": "spatie/laravel-typescript-transformer", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-typescript-transformer.git", + "reference": "d1954994a0965a8ba64ac2722a94326fca0419a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-typescript-transformer/zipball/d1954994a0965a8ba64ac2722a94326fca0419a0", + "reference": "d1954994a0965a8ba64ac2722a94326fca0419a0", + "shasum": "" + }, + "require": { + "illuminate/console": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.12", + "spatie/typescript-transformer": "^2.4" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.0", + "mockery/mockery": "^1.4", + "nesbot/carbon": "^2.63|^3.0", + "orchestra/testbench": "^8.37.0|^9.16|^10.9|^11.0", + "pestphp/pest": "^2.36|^3.0", + "spatie/data-transfer-object": "^2.0", + "spatie/enum": "^3.0", + "spatie/laravel-model-states": "^2.0", + "spatie/pest-plugin-snapshots": "^2.0", + "spatie/phpunit-snapshot-assertions": "^5.0", + "spatie/temporary-directory": "^1.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\LaravelTypeScriptTransformer\\TypeScriptTransformerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\LaravelTypeScriptTransformer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Transform your PHP structures to TypeScript types", + "homepage": "https://github.com/spatie/typescript-transformer", + "keywords": [ + "spatie", + "typescript-transformer" + ], + "support": { + "issues": "https://github.com/spatie/laravel-typescript-transformer/issues", + "source": "https://github.com/spatie/laravel-typescript-transformer/tree/2.6.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-02-25T16:20:54+00:00" + }, + { + "name": "spatie/php-structure-discoverer", + "version": "2.4.2", + "source": { + "type": "git", + "url": "https://github.com/spatie/php-structure-discoverer.git", + "reference": "10cd4e0018450d23e2bd8f8472569ad0c445c0fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/10cd4e0018450d23e2bd8f8472569ad0c445c0fc", + "reference": "10cd4e0018450d23e2bd8f8472569ad0c445c0fc", + "shasum": "" + }, + "require": { + "illuminate/collections": "^11.0|^12.0|^13.0", + "php": "^8.3", + "spatie/laravel-package-tools": "^1.92.7", + "symfony/finder": "^6.0|^7.3.5|^8.0" + }, + "require-dev": { + "amphp/parallel": "^2.3.2", + "illuminate/console": "^11.0|^12.0|^13.0", + "nunomaduro/collision": "^7.0|^8.8.3", + "orchestra/testbench": "^9.5|^10.8|^11.0", + "pestphp/pest": "^3.8|^4.0", + "pestphp/pest-plugin-laravel": "^3.2|^4.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.2", + "spatie/laravel-ray": "^1.43.1" + }, + "suggest": { + "amphp/parallel": "When you want to use the Parallel discover worker" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\StructureDiscoverer\\StructureDiscovererServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\StructureDiscoverer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "role": "Developer" + } + ], + "description": "Automatically discover structures within your PHP application", + "homepage": "https://github.com/spatie/php-structure-discoverer", + "keywords": [ + "discover", + "laravel", + "php", + "php-structure-discoverer" + ], + "support": { + "issues": "https://github.com/spatie/php-structure-discoverer/issues", + "source": "https://github.com/spatie/php-structure-discoverer/tree/2.4.2" + }, + "funding": [ + { + "url": "https://github.com/LaravelAutoDiscoverer", + "type": "github" + } + ], + "time": "2026-04-28T06:26:02+00:00" + }, { "name": "spatie/temporary-directory", "version": "2.3.1", @@ -6825,6 +6925,78 @@ ], "time": "2026-01-12T07:42:22+00:00" }, + { + "name": "spatie/typescript-transformer", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/typescript-transformer.git", + "reference": "dd7cbb90b6b8c34f2aee68701cf39c5432400c0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/typescript-transformer/zipball/dd7cbb90b6b8c34f2aee68701cf39c5432400c0d", + "reference": "dd7cbb90b6b8c34f2aee68701cf39c5432400c0d", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18|^5.0", + "php": "^8.1", + "phpdocumentor/type-resolver": "^1.6.2", + "symfony/process": "^5.2|^6.0|^7.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.40", + "larapack/dd": "^1.1", + "myclabs/php-enum": "^1.7", + "pestphp/pest": "^1.22", + "phpstan/extension-installer": "^1.1", + "phpunit/phpunit": "^9.0", + "spatie/data-transfer-object": "^2.0", + "spatie/enum": "^3.0", + "spatie/pest-plugin-snapshots": "^1.1", + "spatie/temporary-directory": "^1.2|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\TypeScriptTransformer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Transform your PHP structures to TypeScript types", + "homepage": "https://github.com/spatie/typescript-transformer", + "keywords": [ + "spatie", + "typescript-transformer" + ], + "support": { + "issues": "https://github.com/spatie/typescript-transformer/issues", + "source": "https://github.com/spatie/typescript-transformer/tree/2.5.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-04-25T13:53:57+00:00" + }, { "name": "spomky-labs/cbor-php", "version": "3.2.3", @@ -10819,23 +10991,23 @@ }, { "name": "webmozart/assert", - "version": "2.3.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4" + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/eb0d790f735ba6cff25c683a85a1da0eadeff9e4", - "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", "shasum": "" }, "require": { "ext-ctype": "*", "ext-date": "*", "ext-filter": "*", - "php": "^8.2" + "php": "^7.2 || ^8.0" }, "suggest": { "ext-intl": "", @@ -10845,7 +11017,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-feature/2-0": "2.0-dev" + "dev-master": "1.10-dev" } }, "autoload": { @@ -10861,10 +11033,6 @@ { "name": "Bernhard Schussek", "email": "bschussek@gmail.com" - }, - { - "name": "Woody Gilk", - "email": "woody.gilk@gmail.com" } ], "description": "Assertions to validate method input/output with nice error messages.", @@ -10875,9 +11043,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.3.0" + "source": "https://github.com/webmozarts/assert/tree/1.12.1" }, - "time": "2026-04-11T10:33:05+00:00" + "time": "2025-10-29T15:56:20+00:00" } ], "packages-dev": [ diff --git a/config/typescript-transformer.php b/config/typescript-transformer.php new file mode 100644 index 000000000..f499a86d3 --- /dev/null +++ b/config/typescript-transformer.php @@ -0,0 +1,94 @@ + [ + app_path(), + ], + + /* + * Collectors will search for classes in the `auto_discover_types` paths and choose the correct + * transformer to transform them. By default, we include a DefaultCollector which will search + * for @typescript annotated and #[TypeScript] attributed classes to transform. + */ + + 'collectors' => [ + DefaultCollector::class, + EnumCollector::class, + ], + + /* + * Transformers take PHP classes(e.g., enums) as an input and will output + * a TypeScript representation of the PHP class. + */ + + 'transformers' => [ + SpatieStateTransformer::class, + EnumTransformer::class, + DtoTransformer::class, + ], + + /* + * In your classes, you sometimes have types that should always be replaced + * by the same TypeScript representations. For example, you can replace a + * Datetime always with a string. You define these replacements here. + */ + + 'default_type_replacements' => [ + DateTime::class => 'string', + DateTimeImmutable::class => 'string', + CarbonInterface::class => 'string', + CarbonImmutable::class => 'string', + Carbon\Carbon::class => 'string', + ], + + /* + * The package will write the generated TypeScript to this file. + */ + + 'output_file' => resource_path('js/types/generated.d.ts'), + + /* + * When the package is writing types to the output file, a writer is used to + * determine the format. By default, this is the `TypeDefinitionWriter`. + * But you can also use the `ModuleWriter` or implement your own. + */ + + 'writer' => TypeDefinitionWriter::class, + + /* + * The generated TypeScript file can be formatted. We ship a Prettier formatter + * out of the box: `PrettierFormatter` but you can also implement your own one. + * The generated TypeScript will not be formatted when no formatter was set. + */ + + 'formatter' => null, + + /* + * Enums can be transformed into types or native TypeScript enums, by default + * the package will transform them to types. + */ + + 'transform_to_native_enums' => false, + + /* + * By default, this package will convert PHP nullable properties to TypeScript + * types using a `null` type union. Setting `transform_null_to_optional` will + * make them optional instead. + */ + + 'transform_null_to_optional' => false, +]; diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 6e456e4d3..b8e472aed 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -73,6 +73,9 @@ if [ "$1" != 'php' ] && [ "$1" != 'sh' ]; then php artisan config:cache php artisan route:cache + echo "Caching spatie/laravel-data structures..." + php artisan data:cache-structures || true + # Run Laravel-specific post-installation commands echo "NNTmux installation..." php artisan nntmux:install --yes diff --git a/docs/nntmux_api_v2.md b/docs/nntmux_api_v2.md index 3ed345a8c..7acc23676 100644 --- a/docs/nntmux_api_v2.md +++ b/docs/nntmux_api_v2.md @@ -62,17 +62,35 @@ Sorting examples: JSON sorting response snippet (`sort=size_desc`): -```json -{ - "Total": 2, - "Results": [ - { "title": "Ubuntu ISO x64", "size": 734003200 }, - { "title": "Ubuntu ISO x86", "size": 367001600 } - ] -} +```http +HTTP/1.1 200 OK +Content-Type: application/json +X-Total-Count: 2 +X-Api-Current: 0 +X-Api-Max: 100 +X-Grab-Current: 0 +X-Grab-Max: 100 +X-Api-Oldest-Time: +X-Grab-Oldest-Time: + +[ + { "title": "Ubuntu ISO x64", "size": 734003200 }, + { "title": "Ubuntu ISO x86", "size": 367001600 } +] ``` -`Results` are ordered largest-to-smallest because `sort=size_desc`. +Releases are ordered largest-to-smallest because `sort=size_desc`. + +> **Breaking change (April 2026):** the legacy `Results` (capital R) JSON +> envelope — previously produced by `spatie/laravel-fractal` — has been +> removed entirely. Search endpoints now return a bare top-level JSON array of +> `App\Data\Api\ReleaseData` payloads. Pagination total and per-user API/grab +> quotas have moved to response headers (`X-Total-Count`, `X-Api-Current`, +> `X-Api-Max`, `X-Grab-Current`, `X-Grab-Max`, `X-Api-Oldest-Time`, +> `X-Grab-Oldest-Time`). Movie/TV-only fields (`tvdbid`, `imdbid`, `season`, …) +> are omitted from each release object when not applicable to its category, +> instead of being emitted as `null`. TypeScript definitions are auto-generated +> to `resources/js/types/generated.d.ts`. ## Endpoints @@ -100,7 +118,7 @@ Behavior: - If `id` is present: text search. - If `id` is omitted: browse mode. -- Includes API usage counters in response (`apiCurrent`, `apiMax`, `grabCurrent`, `grabMax`, `apiOldestTime`, `grabOldestTime`). +- Includes API usage counters via response headers (`X-Api-Current`, `X-Api-Max`, `X-Grab-Current`, `X-Grab-Max`, `X-Api-Oldest-Time`, `X-Grab-Oldest-Time`). ## 3) TV Search @@ -185,7 +203,7 @@ Selectors: "grabMax": 100, "apiOldestTime": "Wed, 20 Nov 2024 12:00:00 +0000", "grabOldestTime": "", - "Results": [] + "results": [] } ``` diff --git a/package.json b/package.json index 3861026b1..4bed16f6c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", + "build": "php artisan typescript:transform --quiet && vite build", + "types": "php artisan typescript:transform", "format": "prettier --write 'resources/**/*.{css,js,vue}'", "package-update": "ncu -u -x pnotify,tinymce" }, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 94fd66428..b643d0f96 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,5 +1,11 @@ parameters: ignoreErrors: + - + message: '#^Parameter \#2 \$authenticatorAssertionResponse of method Webauthn\\AuthenticatorAssertionResponseValidator\:\:check\(\) expects Webauthn\\AuthenticatorAssertionResponse, Webauthn\\AuthenticatorResponse given\.$#' + identifier: argument.type + count: 1 + path: app/Actions/Passkeys/FindPasskeyToAuthenticateAction.php + - message: '#^Method App\\Console\\Commands\\NntmuxOffsetPopulate\:\:createOffsetRanges\(\) should return array\ but returns list\\>\>\.$#' identifier: return.type @@ -12,6 +18,12 @@ parameters: count: 1 path: app/Console/Commands/NntmuxOffsetPopulate.php + - + message: '#^Method App\\Console\\Commands\\NntmuxPopulateSearchIndexes\:\:processSecondaryElasticChunks\(\) has parameter \$query with generic class Illuminate\\Database\\Eloquent\\Builder but does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Console/Commands/NntmuxPopulateSearchIndexes.php + - message: '#^Method App\\Console\\Commands\\NntmuxResetPostProcessing\:\:normalizeCategories\(\) should return array\ but returns array\\.$#' identifier: return.type @@ -24,42 +36,18 @@ parameters: count: 1 path: app/Facades/Elasticsearch.php - - - message: '#^Class App\\Facades\\Elasticsearch has PHPDoc tag @method for method bulk\(\) return type with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: app/Facades/Elasticsearch.php - - message: '#^Class App\\Facades\\Elasticsearch has PHPDoc tag @method for method deleteByQuery\(\) parameter \#1 \$params with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: app/Facades/Elasticsearch.php - - - message: '#^Class App\\Facades\\Elasticsearch has PHPDoc tag @method for method deleteByQuery\(\) return type with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: app/Facades/Elasticsearch.php - - message: '#^Class App\\Facades\\Elasticsearch has PHPDoc tag @method for method get\(\) parameter \#1 \$params with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: app/Facades/Elasticsearch.php - - - message: '#^Class App\\Facades\\Elasticsearch has PHPDoc tag @method for method get\(\) return type with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: app/Facades/Elasticsearch.php - - - - message: '#^Class App\\Facades\\Elasticsearch has PHPDoc tag @method for method search\(\) return type with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: app/Facades/Elasticsearch.php - - message: '#^Class App\\Facades\\Search has PHPDoc tag @method for method autocomplete\(\) return type with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -78,6 +66,18 @@ parameters: count: 1 path: app/Facades/Search.php + - + message: '#^Class App\\Facades\\Search has PHPDoc tag @method for method bulkInsertSecondary\(\) parameter \#2 \$documents with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Facades/Search.php + + - + message: '#^Class App\\Facades\\Search has PHPDoc tag @method for method bulkInsertSecondary\(\) return type with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Facades/Search.php + - message: '#^Class App\\Facades\\Search has PHPDoc tag @method for method fuzzySearchReleases\(\) parameter \#1 \$phrases with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -102,6 +102,30 @@ parameters: count: 1 path: app/Facades/Search.php + - + message: '#^Class App\\Facades\\Search has PHPDoc tag @method for method insertSecondary\(\) parameter \#3 \$document with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Facades/Search.php + + - + message: '#^Class App\\Facades\\Search has PHPDoc tag @method for method searchAnimeTitle\(\) return type with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Facades/Search.php + + - + message: '#^Class App\\Facades\\Search has PHPDoc tag @method for method searchMoviesByFields\(\) parameter \#1 \$fieldTerms with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Facades/Search.php + + - + message: '#^Class App\\Facades\\Search has PHPDoc tag @method for method searchMoviesByFields\(\) return type with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Facades/Search.php + - message: '#^Class App\\Facades\\Search has PHPDoc tag @method for method searchPredb\(\) parameter \#1 \$searchTerm with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -150,6 +174,18 @@ parameters: count: 1 path: app/Facades/Search.php + - + message: '#^Class App\\Facades\\Search has PHPDoc tag @method for method searchReleasesFiltered\(\) parameter \#1 \$criteria with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Facades/Search.php + + - + message: '#^Class App\\Facades\\Search has PHPDoc tag @method for method searchReleasesFiltered\(\) return type with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Facades/Search.php + - message: '#^Class App\\Facades\\Search has PHPDoc tag @method for method searchReleasesWithCategoryFilter\(\) parameter \#2 \$categoryIds with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -174,6 +210,12 @@ parameters: count: 1 path: app/Facades/Search.php + - + message: '#^Class App\\Facades\\Search has PHPDoc tag @method for method searchSecondary\(\) return type with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Facades/Search.php + - message: '#^Class App\\Facades\\Search has PHPDoc tag @method for method suggest\(\) return type with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -204,6 +246,36 @@ parameters: count: 1 path: app/Facades/TvProcessing.php + - + message: '#^Unable to resolve the template type TKey in call to function collect$#' + identifier: argument.templateType + count: 2 + path: app/Http/Controllers/Admin/AdminPageController.php + + - + message: '#^Unable to resolve the template type TValue in call to function collect$#' + identifier: argument.templateType + count: 2 + path: app/Http/Controllers/Admin/AdminPageController.php + + - + message: '#^Using nullsafe method call on non\-nullable type Carbon\\Carbon\. Use \-\> instead\.$#' + identifier: nullsafe.neverNull + count: 2 + path: app/Http/Controllers/Admin/AdminRegistrationController.php + + - + message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$id\.$#' + identifier: property.notFound + count: 1 + path: app/Http/Controllers/Admin/AdminUserController.php + + - + message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$username\.$#' + identifier: property.notFound + count: 1 + path: app/Http/Controllers/Admin/AdminUserController.php + - message: '#^Offset 1 does not exist on array\\.$#' identifier: offsetAccess.notFound @@ -211,23 +283,196 @@ parameters: path: app/Http/Controllers/Admin/DeletedUsersController.php - - message: '#^Method App\\Http\\Controllers\\Api\\ApiController\:\:categoryID\(\) should return array\ but returns list\\.$#' + message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$id\.$#' + identifier: property.notFound + count: 1 + path: app/Http/Controllers/Api/ApiController.php + + - + message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$is_disabled\.$#' + identifier: property.notFound + count: 1 + path: app/Http/Controllers/Api/ApiController.php + + - + message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$role\.$#' + identifier: property.notFound + count: 2 + path: app/Http/Controllers/Api/ApiController.php + + - + message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Model\:\:hasRole\(\)\.$#' + identifier: method.notFound + count: 1 + path: app/Http/Controllers/Api/ApiController.php + + - + message: '#^Method App\\Http\\Controllers\\Api\\ApiController\:\:categoryID\(\) should return array\ but returns array\\.$#' + identifier: return.type + count: 3 + path: app/Http/Controllers/Api/ApiController.php + + - + message: '#^Method App\\Http\\Controllers\\Api\\ApiController\:\:categoryID\(\) should return array\ but returns list\\.$#' identifier: return.type count: 1 path: app/Http/Controllers/Api/ApiController.php - - message: '#^PHPDoc tag @return with type list\ is incompatible with native type bool\|int\|string\.$#' - identifier: return.phpDocType + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable count: 1 path: app/Http/Controllers/Api/ApiController.php + - + message: '#^Access to an undefined property App\\Models\\User\:\:\$is_disabled\.$#' + identifier: property.notFound + count: 1 + path: app/Http/Controllers/Api/ApiInformController.php - - message: '#^Unable to resolve the template type TValue in call to function collect$#' + message: '#^Access to an undefined property App\\Models\\User\:\:\$is_disabled\.$#' + identifier: property.notFound + count: 1 + path: app/Http/Controllers/Api/ApiV2Controller.php + + - + message: '#^Call to function is_array\(\) with bool\|int\|string will always evaluate to false\.$#' + identifier: function.impossibleType + count: 1 + path: app/Http/Controllers/Api/ApiV2Controller.php + + - + message: '#^Cannot call method map\(\) on list\\.$#' + identifier: method.nonObject + count: 1 + path: app/Http/Controllers/Api/ApiV2Controller.php + + - + message: '#^Offset 0 on \*NEVER\* on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: app/Http/Controllers/Api/ApiV2Controller.php + + - + message: '#^Call to function is_int\(\) with string\|null will always evaluate to false\.$#' + identifier: function.impossibleType + count: 1 + path: app/Http/Controllers/Api/RSS.php + + - + message: '#^Property App\\Http\\Controllers\\Auth\\LoginController\:\:\$decayMinutes \(int\) in isset\(\) is not nullable\.$#' + identifier: isset.property + count: 1 + path: app/Http/Controllers/Auth/LoginController.php + + - + message: '#^Property App\\Http\\Controllers\\Auth\\LoginController\:\:\$maxAttempts \(int\) in isset\(\) is not nullable\.$#' + identifier: isset.property + count: 1 + path: app/Http/Controllers/Auth/LoginController.php + + - + message: '#^Property App\\Http\\Controllers\\Auth\\LoginController\:\:\$redirectTo \(string\) in isset\(\) is not nullable\.$#' + identifier: isset.property + count: 1 + path: app/Http/Controllers/Auth/LoginController.php + + - + message: '#^Cannot call method toIso8601String\(\) on string\.$#' + identifier: method.nonObject + count: 1 + path: app/Http/Controllers/Auth/PasskeyManagementController.php + + - + message: '#^Method App\\Http\\Controllers\\Auth\\PasskeyManagementController\:\:store\(\) never returns Illuminate\\Http\\RedirectResponse so it can be removed from the return type\.$#' + identifier: return.unusedType + count: 1 + path: app/Http/Controllers/Auth/PasskeyManagementController.php + + - + message: '#^Using nullsafe property access on non\-nullable type Spatie\\LaravelPasskeys\\Models\\Passkey\. Use \-\> instead\.$#' + identifier: nullsafe.neverNull + count: 4 + path: app/Http/Controllers/Auth/PasskeyManagementController.php + + - + message: '#^Method App\\Http\\Controllers\\Auth\\RegisterController\:\:rejectUnavailableRegistration\(\) has parameter \$status with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Http/Controllers/Auth/RegisterController.php + + - + message: '#^Method App\\Http\\Controllers\\Auth\\RegisterController\:\:rejectUnavailableRegistration\(\) never returns Illuminate\\Routing\\Redirector so it can be removed from the return type\.$#' + identifier: return.unusedType + count: 1 + path: app/Http/Controllers/Auth/RegisterController.php + + - + message: '#^Property App\\Http\\Controllers\\Auth\\RegisterController\:\:\$redirectTo \(string\) in isset\(\) is not nullable\.$#' + identifier: isset.property + count: 1 + path: app/Http/Controllers/Auth/RegisterController.php + + - + message: '#^Property App\\Http\\Controllers\\Auth\\ResetPasswordController\:\:\$redirectTo \(string\) in isset\(\) is not nullable\.$#' + identifier: isset.property + count: 1 + path: app/Http/Controllers/Auth/ResetPasswordController.php + + - + message: '#^Unable to resolve the template type TCacheValue in call to static method Illuminate\\Cache\\Repository\:\:remember\(\)$#' identifier: argument.templateType count: 1 - path: app/Http/Controllers/SeriesController.php + path: app/Http/Controllers/BasePageController.php + + - + message: '#^Using nullsafe method call on non\-nullable type App\\Models\\User\. Use \-\> instead\.$#' + identifier: nullsafe.neverNull + count: 1 + path: app/Http/Controllers/BasePageController.php + + - + message: '#^Type mixed in generic type Illuminate\\Database\\Eloquent\\Collection\ in PHPDoc tag @return is not subtype of template type TModel of Illuminate\\Database\\Eloquent\\Model of class Illuminate\\Database\\Eloquent\\Collection\.$#' + identifier: generics.notSubtype + count: 3 + path: app/Http/Controllers/ContentController.php + + - + message: '#^Method App\\Http\\Controllers\\CoverController\:\:respondWithPlaceholder\(\) never returns Illuminate\\Http\\Response so it can be removed from the return type\.$#' + identifier: return.unusedType + count: 1 + path: app/Http/Controllers/CoverController.php + + - + message: '#^Access to an undefined property App\\Models\\User\:\:\$is_disabled\.$#' + identifier: property.notFound + count: 2 + path: app/Http/Controllers/GetNzbController.php + + - + message: '#^Access to an undefined property App\\Models\\User\:\:\$is_disabled\.$#' + identifier: property.notFound + count: 1 + path: app/Http/Controllers/RssController.php + + - + message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$roles_id\.$#' + identifier: property.notFound + count: 1 + path: app/Http/Middleware/ThrottleApiRequestsByToken.php + + - + message: '#^Method App\\Http\\Middleware\\ThrottleApiRequestsByToken\:\:resolveUser\(\) should return App\\Models\\User\|null but returns Illuminate\\Database\\Eloquent\\Model\|null\.$#' + identifier: return.type + count: 1 + path: app/Http/Middleware/ThrottleApiRequestsByToken.php + + - + message: '#^Method App\\Http\\Requests\\Admin\\RegistrationPeriodRequest\:\:after\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Http/Requests/Admin/RegistrationPeriodRequest.php - message: '#^Method App\\Models\\Category\:\:getForApi\(\) should return list\ but returns Illuminate\\Database\\Eloquent\\Collection\\.$#' @@ -271,6 +516,12 @@ parameters: count: 1 path: app/Models/Content.php + - + message: '#^Method App\\Models\\Content\:\:scopeOrdered\(\) return type with generic class Illuminate\\Database\\Eloquent\\Builder does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/Content.php + - message: '#^Method App\\Models\\Genre\:\:scopeDisabled\(\) return type with generic class Illuminate\\Database\\Eloquent\\Builder does not specify its types\: TModel$#' identifier: missingType.generics @@ -319,6 +570,30 @@ parameters: count: 1 path: app/Models/Invitation.php + - + message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Builder\:\:enabled\(\)\.$#' + identifier: method.notFound + count: 1 + path: app/Models/RegistrationPeriod.php + + - + message: '#^Method App\\Models\\RegistrationPeriod\:\:scopeActiveAt\(\) has parameter \$query with generic class Illuminate\\Database\\Eloquent\\Builder but does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/RegistrationPeriod.php + + - + message: '#^Method App\\Models\\RegistrationPeriod\:\:scopeEnabled\(\) has parameter \$query with generic class Illuminate\\Database\\Eloquent\\Builder but does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/RegistrationPeriod.php + + - + message: '#^Method App\\Models\\RegistrationPeriod\:\:scopeUpcoming\(\) has parameter \$query with generic class Illuminate\\Database\\Eloquent\\Builder but does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Models/RegistrationPeriod.php + - message: '#^Property App\\Models\\Release\:\:\$image \(string\|null\) does not accept bool\|null\.$#' identifier: assign.propertyType @@ -331,6 +606,18 @@ parameters: count: 1 path: app/Models/Release.php + - + message: '#^Access to an undefined property App\\Models\\User\:\:\$full_name\.$#' + identifier: property.notFound + count: 1 + path: app/Models/User.php + + - + message: '#^Method App\\Models\\User\:\:findVerifiedByApiToken\(\) should return App\\Models\\User\|null but returns Illuminate\\Database\\Eloquent\\Model\|null\.$#' + identifier: return.type + count: 1 + path: app/Models/User.php + - message: '#^Method App\\Models\\User\:\:scopeActive\(\) return type with generic class Illuminate\\Database\\Eloquent\\Builder does not specify its types\: TModel$#' identifier: missingType.generics @@ -379,6 +666,35 @@ parameters: count: 1 path: app/Models/UserActivityStat.php + - + message: '#^Method App\\Models\\Video\:\:getSeriesListUncached\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Models/Video.php + + - + message: '#^Access to an undefined property TeamTeaTime\\Forum\\Models\\Post\:\:\$author_id\.$#' + identifier: property.notFound + count: 1 + path: app/Policies/PostPolicy.php + + - + message: '#^Access to an undefined property TeamTeaTime\\Forum\\Models\\Thread\:\:\$author_id\.$#' + identifier: property.notFound + count: 1 + path: app/Policies/ThreadPolicy.php + + - + message: '#^Parameter \#1 \$categories of method App\\Support\\Forum\\CategoryTreeBuilder\:\:build\(\) expects Illuminate\\Database\\Eloquent\\Collection\, Illuminate\\Database\\Eloquent\\Collection\ given\.$#' + identifier: argument.type + count: 1 + path: app/Providers/ForumServiceProvider.php + + - + message: '#^Parameter \#3 \$fail \(Closure\(string\)\: void\) of method App\\Rules\\RecaptchaRule\:\:validate\(\) should be compatible with parameter \$fail \(Closure\(string, string\|null\=\)\: Illuminate\\Translation\\PotentiallyTranslatedString\) of method Illuminate\\Contracts\\Validation\\ValidationRule\:\:validate\(\)$#' + identifier: method.childParameterType + count: 1 + path: app/Rules/RecaptchaRule.php - message: '#^Method App\\Services\\AdditionalProcessing\\ArchiveExtractionService\:\:sortFilesWithNfoPriority\(\) should return array\ but returns list\\.$#' @@ -399,11 +715,46 @@ parameters: path: app/Services/AdditionalProcessing/NzbContentParser.php - - message: '#^PHPDoc tag @return with type list\ is incompatible with native type string\|false\.$#' - identifier: return.phpDocType - count: 1 - path: app/Services/AdditionalProcessing/NzbContentParser.php + message: '#^Using nullsafe property access "\?\-\>id" on left side of \?\? is unnecessary\. Use \-\> instead\.$#' + identifier: nullsafe.neverNull + count: 2 + path: app/Services/AdditionalProcessing/ReleaseFileManager.php + - + message: '#^Using nullsafe property access "\?\-\>title" on left side of \?\? is unnecessary\. Use \-\> instead\.$#' + identifier: nullsafe.neverNull + count: 2 + path: app/Services/AdditionalProcessing/ReleaseFileManager.php + + - + message: '#^Parameter \#2 \$messageIDs of method App\\Services\\AdditionalProcessing\\UsenetDownloadService\:\:download\(\) expects array\\|string, list\ given\.$#' + identifier: argument.type + count: 1 + path: app/Services/AdditionalProcessing/ReleaseFilesArchiveFallback.php + + - + message: '#^Parameter \#1 \$nzbContents of method App\\Services\\AdditionalProcessing\\NzbContentParser\:\:extractMessageIDs\(\) expects array\, list\\> given\.$#' + identifier: argument.type + count: 1 + path: app/Services/AdditionalProcessing/ReleaseProcessor.php + + - + message: '#^Parameter \#2 \$messageIDs of method App\\Services\\AdditionalProcessing\\UsenetDownloadService\:\:download\(\) expects array\\|string, list\ given\.$#' + identifier: argument.type + count: 3 + path: app/Services/AdditionalProcessing/ReleaseProcessor.php + + - + message: '#^Right side of \|\| is always false\.$#' + identifier: booleanOr.rightAlwaysFalse + count: 2 + path: app/Services/AdditionalProcessing/ReleaseProcessor.php + + - + message: '#^Strict comparison using \=\=\= between non\-empty\-string and null will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 1 + path: app/Services/AdditionalProcessing/ReleaseProcessor.php - message: '#^Method App\\Services\\Binaries\\BinariesService\:\:groupMissingPartsIntoRanges\(\) should return array\ but returns list\\>\.$#' @@ -417,6 +768,11 @@ parameters: count: 1 path: app/Services/Binaries/BinariesService.php + - + message: '#^Strict comparison using \!\=\= between non\-empty\-array\ and array\{\} will always evaluate to true\.$#' + identifier: notIdentical.alwaysTrue + count: 1 + path: app/Services/BookService.php - message: '#^Method App\\Services\\Categorization\\CategorizationService\:\:batchCategorize\(\) should return array\ but returns list\\>\.$#' @@ -430,6 +786,23 @@ parameters: count: 1 path: app/Services/Categorization/CategorizationService.php + - + message: '#^Method App\\Services\\Concurrency\\TimeoutAwareProcessDriver\:\:run\(\) has parameter \$tasks with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/Concurrency/TimeoutAwareProcessDriver.php + + - + message: '#^Method App\\Services\\Concurrency\\TimeoutAwareProcessDriver\:\:run\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/Concurrency/TimeoutAwareProcessDriver.php + + - + message: '#^Strict comparison using \!\=\= between non\-empty\-array\ and array\{\} will always evaluate to true\.$#' + identifier: notIdentical.alwaysTrue + count: 1 + path: app/Services/ConsoleService.php - message: '#^Unable to resolve the template type TValue in call to function collect$#' @@ -437,7 +810,6 @@ parameters: count: 6 path: app/Services/FanartTvService.php - - message: '#^Method App\\Services\\GenreService\:\:loadGenres\(\) should return array\ but returns array\\.$#' identifier: return.type @@ -450,18 +822,215 @@ parameters: count: 1 path: app/Services/GenreService.php + - + message: '#^Class App\\Services\\IGDB\\DataNode implements generic interface ArrayAccess but does not specify its types\: TKey, TValue$#' + identifier: missingType.generics + count: 1 + path: app/Services/IGDB/DataNode.php + + - + message: '#^Class App\\Services\\IGDB\\DataNode implements generic interface Illuminate\\Contracts\\Support\\Arrayable but does not specify its types\: TKey, TValue$#' + identifier: missingType.generics + count: 1 + path: app/Services/IGDB/DataNode.php + + - + message: '#^Method App\\Services\\IGDB\\Models\\BaseModel\:\:__callStatic\(\) has parameter \$parameters with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/IGDB/Models/BaseModel.php + + - + message: '#^Method App\\Services\\IGDB\\Models\\BaseModel\:\:query\(\) return type with generic class App\\Services\\IGDB\\QueryBuilder does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Services/IGDB/Models/BaseModel.php + + - + message: '#^Method App\\Services\\IGDB\\QueryBuilder\:\:limit\(\) return type with generic class App\\Services\\IGDB\\QueryBuilder does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Services/IGDB/QueryBuilder.php + + - + message: '#^Method App\\Services\\IGDB\\QueryBuilder\:\:orderBy\(\) return type with generic class App\\Services\\IGDB\\QueryBuilder does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Services/IGDB/QueryBuilder.php + + - + message: '#^Method App\\Services\\IGDB\\QueryBuilder\:\:orderByDesc\(\) return type with generic class App\\Services\\IGDB\\QueryBuilder does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Services/IGDB/QueryBuilder.php + + - + message: '#^Method App\\Services\\IGDB\\QueryBuilder\:\:search\(\) return type with generic class App\\Services\\IGDB\\QueryBuilder does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Services/IGDB/QueryBuilder.php + + - + message: '#^Method App\\Services\\IGDB\\QueryBuilder\:\:where\(\) return type with generic class App\\Services\\IGDB\\QueryBuilder does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Services/IGDB/QueryBuilder.php + + - + message: '#^Method App\\Services\\IGDB\\QueryBuilder\:\:whereIn\(\) return type with generic class App\\Services\\IGDB\\QueryBuilder does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Services/IGDB/QueryBuilder.php + + - + message: '#^Method App\\Services\\IGDB\\QueryBuilder\:\:with\(\) return type with generic class App\\Services\\IGDB\\QueryBuilder does not specify its types\: TModel$#' + identifier: missingType.generics + count: 1 + path: app/Services/IGDB/QueryBuilder.php + + - + message: '#^Access to an undefined property App\\Services\\IGDB\\Models\\Game\:\:\$id\.$#' + identifier: property.notFound + count: 1 + path: app/Services/IGDBService.php + + - + message: '#^Access to an undefined property App\\Services\\IGDB\\Models\\Game\:\:\$name\.$#' + identifier: property.notFound + count: 1 + path: app/Services/IGDBService.php + + - + message: '#^Call to an undefined static method App\\Services\\IGDB\\Models\\Company\:\:find\(\)\.$#' + identifier: staticMethod.notFound + count: 3 + path: app/Services/IGDBService.php + + - + message: '#^Call to an undefined static method App\\Services\\IGDB\\Models\\Game\:\:search\(\)\.$#' + identifier: staticMethod.notFound + count: 1 + path: app/Services/IGDBService.php + + - + message: '#^Call to an undefined static method App\\Services\\IGDB\\Models\\Game\:\:where\(\)\.$#' + identifier: staticMethod.notFound + count: 1 + path: app/Services/IGDBService.php + - message: '#^Method App\\Services\\IGDBService\:\:extractGenres\(\) should return array\ but returns array\, mixed\>\.$#' identifier: return.type count: 1 path: app/Services/IGDBService.php + - + message: '#^Method App\\Services\\IGDBService\:\:searchExact\(\) has parameter \$platformIds with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/IGDBService.php + + - + message: '#^Method App\\Services\\IGDBService\:\:searchFuzzy\(\) has parameter \$platformIds with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/IGDBService.php + + - + message: '#^Method App\\Services\\IGDBService\:\:searchWithStrategies\(\) has parameter \$platformIds with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/IGDBService.php + - message: '#^PHPDoc tag @return with type array\, mixed\> is incompatible with native type string\.$#' identifier: return.phpDocType count: 1 path: app/Services/IGDBService.php + - + message: '#^Expression on left side of \?\? is not nullable\.$#' + identifier: nullCoalesce.expr + count: 7 + path: app/Services/ImdbScraper.php + + - + message: '#^Method App\\Services\\ImdbScraper\:\:extractCover\(\) has parameter \$jsonLd with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/ImdbScraper.php + + - + message: '#^Method App\\Services\\ImdbScraper\:\:extractGenres\(\) has parameter \$jsonLd with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/ImdbScraper.php + + - + message: '#^Method App\\Services\\ImdbScraper\:\:extractLanguage\(\) has parameter \$jsonLd with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/ImdbScraper.php + + - + message: '#^Method App\\Services\\ImdbScraper\:\:extractPlot\(\) has parameter \$jsonLd with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/ImdbScraper.php + + - + message: '#^Method App\\Services\\ImdbScraper\:\:extractRating\(\) has parameter \$jsonLd with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/ImdbScraper.php + + - + message: '#^Method App\\Services\\ImdbScraper\:\:extractTitle\(\) has parameter \$jsonLd with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/ImdbScraper.php + + - + message: '#^Method App\\Services\\ImdbScraper\:\:extractType\(\) has parameter \$jsonLd with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/ImdbScraper.php + + - + message: '#^Method App\\Services\\ImdbScraper\:\:extractYearFromEntity\(\) has parameter \$jsonLd with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/ImdbScraper.php + + - + message: '#^Ternary operator condition is always false\.$#' + identifier: ternary.alwaysFalse + count: 1 + path: app/Services/ImdbScraper.php + + - + message: '#^Ternary operator condition is always true\.$#' + identifier: ternary.alwaysTrue + count: 2 + path: app/Services/ImdbScraper.php + + - + message: '#^Using nullsafe method call on non\-nullable type voku\\helper\\SimpleHtmlDomInterface\. Use \-\> instead\.$#' + identifier: nullsafe.neverNull + count: 7 + path: app/Services/ImdbScraper.php + + - + message: '#^Method App\\Services\\LogViewerService\:\:searchLog\(\) return type with generic class Illuminate\\Pagination\\LengthAwarePaginator does not specify its types\: TKey, TValue$#' + identifier: missingType.generics + count: 1 + path: app/Services/LogViewerService.php + + - + message: '#^Strict comparison using \!\=\= between non\-empty\-array\ and array\{\} will always evaluate to true\.$#' + identifier: notIdentical.alwaysTrue + count: 1 + path: app/Services/MusicService.php - message: '#^Method App\\Services\\NfoService\:\:extractAllMediaIds\(\) should return array\ but returns list\\>\.$#' @@ -505,6 +1074,30 @@ parameters: count: 1 path: app/Services/NfoService.php + - + message: '#^Unable to resolve the template type TCacheValue in call to static method Illuminate\\Cache\\Repository\:\:remember\(\)$#' + identifier: argument.templateType + count: 1 + path: app/Services/NfoService.php + + - + message: '#^Using nullsafe method call on non\-nullable type Carbon\\Carbon\. Use \-\> instead\.$#' + identifier: nullsafe.neverNull + count: 2 + path: app/Services/RegistrationStatusService.php + + - + message: '#^Call to an undefined static method App\\Support\\Data\\ReleaseCreationResult\:\:fromArray\(\)\.$#' + identifier: staticMethod.notFound + count: 1 + path: app/Services/ReleaseProcessingService.php + + - + message: '#^Call to function is_int\(\) with string\|null will always evaluate to false\.$#' + identifier: function.impossibleType + count: 2 + path: app/Services/Releases/ReleaseBrowseService.php + - message: '#^Method App\\Services\\Releases\\ReleaseBrowseService\:\:getBrowseOrder\(\) should return array\ but returns array\\.$#' identifier: return.type @@ -512,7 +1105,61 @@ parameters: path: app/Services/Releases/ReleaseBrowseService.php - - message: '#^Method App\\Services\\Search\\Drivers\\ElasticSearchDriver\:\:buildHostsArray\(\) should return array\ but returns list\\>\.$#' + message: '#^Parameter \#4 \$orderBy of method App\\Services\\Releases\\ReleaseBrowseService\:\:executeApiBrowseViaSearchIndex\(\) expects array\{string, string\}, array\ given\.$#' + identifier: argument.type + count: 1 + path: app/Services/Releases/ReleaseBrowseService.php + + - + message: '#^Call to function is_int\(\) with string\|null will always evaluate to false\.$#' + identifier: function.impossibleType + count: 3 + path: app/Services/Releases/ReleaseSearchService.php + + - + message: '#^Method App\\Services\\Releases\\ReleaseSearchService\:\:apiSearchLegacyMysql\(\) has parameter \$excludedCats with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/Releases/ReleaseSearchService.php + + - + message: '#^Method App\\Services\\Releases\\ReleaseSearchService\:\:intersectReleaseIdsWithSearchFilters\(\) has parameter \$releaseIds with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/Releases/ReleaseSearchService.php + + - + message: '#^Offset 0 does not exist on array\\.$#' + identifier: offsetAccess.notFound + count: 8 + path: app/Services/Releases/ReleaseSearchService.php + + - + message: '#^Offset 1 does not exist on array\\.$#' + identifier: offsetAccess.notFound + count: 7 + path: app/Services/Releases/ReleaseSearchService.php + + - + message: '#^Constant App\\Services\\Search\\Drivers\\ElasticSearchDriver\:\:DEFAULT_CONNECT_TIMEOUT is unused\.$#' + identifier: classConstant.unused + count: 1 + path: app/Services/Search/Drivers/ElasticSearchDriver.php + + - + message: '#^Constant App\\Services\\Search\\Drivers\\ElasticSearchDriver\:\:DEFAULT_TIMEOUT is unused\.$#' + identifier: classConstant.unused + count: 1 + path: app/Services/Search/Drivers/ElasticSearchDriver.php + + - + message: '#^Method App\\Services\\Search\\Drivers\\ElasticSearchDriver\:\:buildElasticsearchReleaseFilters\(\) has parameter \$criteria with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/Search/Drivers/ElasticSearchDriver.php + + - + message: '#^Method App\\Services\\Search\\Drivers\\ElasticSearchDriver\:\:executeSearch\(\) should return array\ but returns list\.$#' identifier: return.type count: 1 path: app/Services/Search/Drivers/ElasticSearchDriver.php @@ -524,17 +1171,41 @@ parameters: path: app/Services/Search/Drivers/ElasticSearchDriver.php - - message: '#^PHPDoc tag @return with type list\\> is incompatible with native type bool\.$#' - identifier: return.phpDocType - count: 1 + message: '#^Parameter \#1 \$params of method Elastic\\Elasticsearch\\Client\:\:delete\(\) expects array\{id\: string, index\: string, wait_for_active_shards\?\: string, refresh\?\: string, routing\?\: string, timeout\?\: int\|string, if_seq_no\?\: int, if_primary_term\?\: int, \.\.\.\}\|null, array\{index\: string, id\: int\\|int\<1, max\>\} given\.$#' + identifier: argument.type + count: 4 path: app/Services/Search/Drivers/ElasticSearchDriver.php - - message: '#^Method App\\Services\\Search\\Drivers\\ManticoreSearchDriver\:\:searchReleasesByExternalId\(\) should return array\ but returns list\.$#' + message: '#^Strict comparison using \=\=\= between int\|non\-falsy\-string and '''' will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 2 + path: app/Services/Search/Drivers/ElasticSearchDriver.php + + - + message: '#^Method App\\Services\\Search\\Drivers\\ManticoreSearchDriver\:\:phrasesToSearchArray\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Services/Search/Drivers/ManticoreSearchDriver.php + + - + message: '#^Method App\\Services\\Search\\Drivers\\ManticoreSearchDriver\:\:searchReleasesByMultipleExternalIds\(\) should return array\ but returns list\\.$#' identifier: return.type count: 1 path: app/Services/Search/Drivers/ManticoreSearchDriver.php + - + message: '#^Parameter \#3 \$column of method App\\Services\\Search\\Drivers\\ManticoreSearchDriver\:\:searchIndexes\(\) expects array\, list\ given\.$#' + identifier: argument.type + count: 1 + path: app/Services/Search/Drivers/ManticoreSearchDriver.php + + - + message: '#^Using nullsafe property access "\?\-\>value" on left side of \?\? is unnecessary\. Use \-\> instead\.$#' + identifier: nullsafe.neverNull + count: 2 + path: app/Services/SiteStatusService.php + - message: '#^Method App\\Services\\SteamService\:\:getDLCList\(\) should return array\ but returns list\\>\.$#' identifier: return.type @@ -590,13 +1261,7 @@ parameters: path: app/Services/SystemMetricsService.php - - message: '#^PHPDoc tag @return with type array\ is incompatible with native type float\.$#' - identifier: return.phpDocType - count: 1 - path: app/Services/SystemMetricsService.php - - - - message: '#^Method App\\Services\\UserStatsService\:\:getApiHitsPerMinute\(\) should return array\ but returns list\\>\.$#' + message: '#^Method App\\Services\\UserStatsService\:\:getApiHitsPerMinute\(\) should return array\ but returns list\\>\.$#' identifier: return.type count: 1 path: app/Services/UserStatsService.php @@ -614,13 +1279,13 @@ parameters: path: app/Services/UserStatsService.php - - message: '#^Method App\\Services\\UserStatsService\:\:getDownloadsPerMinute\(\) should return array\ but returns list\\>\.$#' + message: '#^Method App\\Services\\UserStatsService\:\:getDownloadsPerMinute\(\) should return array\ but returns list\\>\.$#' identifier: return.type 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\<0, max\>, downloads_week\: float\|int, api_hits_today\: int\<0, max\>, api_hits_week\: float\|int\}\.$#' + 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 @@ -631,6 +1296,78 @@ parameters: count: 1 path: app/Services/XrefService.php + - + message: '#^Using nullsafe property access "\?\-\>approx" on left side of \?\? is unnecessary\. Use \-\> instead\.$#' + identifier: nullsafe.neverNull + count: 1 + path: app/Support/ApproximateRowCount.php + + - + message: '#^Call to an undefined method Illuminate\\Database\\Eloquent\\Collection\\:\:linkNodes\(\)\.$#' + identifier: method.notFound + count: 1 + path: app/Support/Forum/CategoryTreeBuilder.php + + - + message: '#^Parameter \#1 \$categories of method App\\Support\\Forum\\CategoryTreeBuilder\:\:build\(\) expects Illuminate\\Database\\Eloquent\\Collection\, Aimeos\\Nestedset\\Collection given\.$#' + identifier: argument.type + count: 2 + path: app/Support/Forum/CategoryTreeBuilder.php + + - + message: '#^Method App\\Support\\SecondaryIndexDocuments\:\:book\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Support/SecondaryIndexDocuments.php + + - + message: '#^Method App\\Support\\SecondaryIndexDocuments\:\:console\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Support/SecondaryIndexDocuments.php + + - + message: '#^Method App\\Support\\SecondaryIndexDocuments\:\:games\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Support/SecondaryIndexDocuments.php + + - + message: '#^Method App\\Support\\SecondaryIndexDocuments\:\:music\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Support/SecondaryIndexDocuments.php + + - + message: '#^Method App\\Support\\SecondaryIndexDocuments\:\:steam\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: app/Support/SecondaryIndexDocuments.php + + - + message: '#^Using nullsafe property access "\?\-\>anilist_id" on left side of \?\? is unnecessary\. Use \-\> instead\.$#' + identifier: nullsafe.neverNull + count: 1 + path: app/Support/SecondaryIndexDocuments.php + + - + message: '#^Using nullsafe property access "\?\-\>mal_id" on left side of \?\? is unnecessary\. Use \-\> instead\.$#' + identifier: nullsafe.neverNull + count: 1 + path: app/Support/SecondaryIndexDocuments.php + + - + message: '#^Using nullsafe property access "\?\-\>media_type" on left side of \?\? is unnecessary\. Use \-\> instead\.$#' + identifier: nullsafe.neverNull + count: 1 + path: app/Support/SecondaryIndexDocuments.php + + - + message: '#^Using nullsafe property access "\?\-\>status" on left side of \?\? is unnecessary\. Use \-\> instead\.$#' + identifier: nullsafe.neverNull + count: 1 + path: app/Support/SecondaryIndexDocuments.php + - message: '#^Method App\\Support\\UpdatePerformanceHelper\:\:checkSystemResources\(\) should return array\ but returns list\\.$#' identifier: return.type @@ -648,3 +1385,15 @@ parameters: identifier: return.phpDocType count: 1 path: app/Support/UpdatePerformanceHelper.php + + - + message: '#^Parameter \#1 \$view of function view expects view\-string\|null, string given\.$#' + identifier: argument.type + count: 1 + path: app/View/Components/AppLayout.php + + - + message: '#^Unable to resolve the template type TCacheValue in call to static method Illuminate\\Cache\\Repository\:\:remember\(\)$#' + identifier: argument.templateType + count: 1 + path: app/View/Composers/GlobalDataComposer.php diff --git a/tests/Feature/AdditionalProcessingNzbSplitRenameTest.php b/tests/Feature/AdditionalProcessingNzbSplitRenameTest.php index b9f467605..e502311e3 100644 --- a/tests/Feature/AdditionalProcessingNzbSplitRenameTest.php +++ b/tests/Feature/AdditionalProcessingNzbSplitRenameTest.php @@ -7,7 +7,7 @@ namespace Tests\Feature; use App\Models\Category; use App\Models\Release; use App\Services\AdditionalProcessing\Config\ProcessingConfiguration; -use App\Services\AdditionalProcessing\DTO\ReleaseProcessingContext; +use App\Services\AdditionalProcessing\State\ReleaseProcessingContext; use App\Services\AdditionalProcessing\ReleaseFileManager; use App\Services\NameFixing\FileNameCleaner; use App\Services\NameFixing\NameFixingService; diff --git a/tests/Unit/AdditionalProcessing/ReleaseFilesArchiveFallbackTest.php b/tests/Unit/AdditionalProcessing/ReleaseFilesArchiveFallbackTest.php index df6326d01..a262db3ac 100644 --- a/tests/Unit/AdditionalProcessing/ReleaseFilesArchiveFallbackTest.php +++ b/tests/Unit/AdditionalProcessing/ReleaseFilesArchiveFallbackTest.php @@ -6,7 +6,7 @@ use App\Models\Release; use App\Models\ReleaseFile; use App\Services\AdditionalProcessing\ArchiveExtractionService; use App\Services\AdditionalProcessing\ConsoleOutputService; -use App\Services\AdditionalProcessing\DTO\ReleaseProcessingContext; +use App\Services\AdditionalProcessing\State\ReleaseProcessingContext; use App\Services\AdditionalProcessing\MediaExtractionService; use App\Services\AdditionalProcessing\ReleaseFileManager; use App\Services\AdditionalProcessing\ReleaseFilesArchiveFallback; diff --git a/tests/Unit/AdditionalProcessing/ReleaseProcessorTest.php b/tests/Unit/AdditionalProcessing/ReleaseProcessorTest.php index 200282fb3..7e5975376 100644 --- a/tests/Unit/AdditionalProcessing/ReleaseProcessorTest.php +++ b/tests/Unit/AdditionalProcessing/ReleaseProcessorTest.php @@ -5,7 +5,7 @@ namespace Tests\Unit\AdditionalProcessing; use App\Models\Release; use App\Services\AdditionalProcessing\ArchiveExtractionService; use App\Services\AdditionalProcessing\ConsoleOutputService; -use App\Services\AdditionalProcessing\DTO\ReleaseProcessingContext; +use App\Services\AdditionalProcessing\State\ReleaseProcessingContext; use App\Services\AdditionalProcessing\MediaExtractionService; use App\Services\AdditionalProcessing\NzbContentParser; use App\Services\AdditionalProcessing\ReleaseFileManager; diff --git a/tests/Unit/SteamDTOsTest.php b/tests/Unit/SteamDTOsTest.php index df6d5e523..b5d6a824f 100644 --- a/tests/Unit/SteamDTOsTest.php +++ b/tests/Unit/SteamDTOsTest.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace Tests\Unit; -use App\Support\DTOs\SteamGameData; -use App\Support\DTOs\SteamPriceData; -use PHPUnit\Framework\TestCase; +use App\Support\Data\SteamGameData; +use App\Support\Data\SteamPriceData; +use Tests\TestCase; /** * Unit tests for Steam DTOs. diff --git a/tests/Unit/Support/BookMatchScorerTest.php b/tests/Unit/Support/BookMatchScorerTest.php index 1bf1c8cb3..b8e3f5f48 100644 --- a/tests/Unit/Support/BookMatchScorerTest.php +++ b/tests/Unit/Support/BookMatchScorerTest.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Tests\Unit\Support; use App\Support\BookMatchScorer; -use App\Support\DTOs\BookParseResult; +use App\Support\Data\BookParseResult; use Tests\TestCase; class BookMatchScorerTest extends TestCase