mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 21:01:22 +00:00
Update DTOs and add spatie/laravel-data package to manage them
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Api;
|
||||
|
||||
use App\Models\RootCategory;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
|
||||
|
||||
/**
|
||||
* API v2 representation of a {@see RootCategory} with its sub-categories.
|
||||
*
|
||||
* Replaces the legacy `App\Transformers\CategoryTransformer`.
|
||||
*/
|
||||
#[TypeScript]
|
||||
final class CategoryData extends Data
|
||||
{
|
||||
/**
|
||||
* @param array<int|string, string> $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<int|string, string> $subcategories */
|
||||
$subcategories = $category->categories()->pluck('title', 'id')->all();
|
||||
|
||||
return new self(
|
||||
id: (int) $category->id,
|
||||
name: (string) $category->title,
|
||||
subcategories: $subcategories,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Api;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Release;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Optional;
|
||||
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
|
||||
|
||||
/**
|
||||
* API v2 representation of a single release's full detail payload.
|
||||
*
|
||||
* Replaces the legacy `App\Transformers\DetailsTransformer`.
|
||||
*/
|
||||
#[TypeScript]
|
||||
final class DetailsData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public string $title,
|
||||
public string $details,
|
||||
public string $link,
|
||||
public int $category,
|
||||
public ?string $category_name,
|
||||
public string $added,
|
||||
public int|string|null $size,
|
||||
public int|string|null $files,
|
||||
public int|string|null $grabs,
|
||||
public int|string|null $comments,
|
||||
public int|string|null $password,
|
||||
public string $usenetdate,
|
||||
public Optional|int|string|null $imdbid = new Optional,
|
||||
public Optional|int|string|null $tmdbid = new Optional,
|
||||
public Optional|int|string|null $traktid = new Optional,
|
||||
public Optional|string|null $tvairdate = new Optional,
|
||||
public Optional|int|string|null $tvdbid = new Optional,
|
||||
public Optional|int|string|null $tvrageid = new Optional,
|
||||
public Optional|int|string|null $tvmazeid = new Optional,
|
||||
) {}
|
||||
|
||||
public static function fromRelease(Release $release, User $user): self
|
||||
{
|
||||
$categoriesId = (int) $release->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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Api;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Release;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Optional;
|
||||
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
|
||||
|
||||
/**
|
||||
* API v2 representation of a release search result.
|
||||
*
|
||||
* Replaces the legacy `App\Transformers\ApiTransformer`.
|
||||
*
|
||||
* Movie/TV-only fields are typed as Optional so they are omitted from the
|
||||
* serialised output for releases of other categories — matching the previous
|
||||
* Fractal `null()` primitive behaviour.
|
||||
*/
|
||||
#[TypeScript]
|
||||
final class ReleaseData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public string $title,
|
||||
public string $details,
|
||||
public string $url,
|
||||
public int $category,
|
||||
public ?string $category_name,
|
||||
public string $added,
|
||||
public int|string|null $size,
|
||||
public int|string|null $files,
|
||||
public int|string|null $grabs,
|
||||
public int|string|null $comments,
|
||||
public int|string|null $password,
|
||||
public string $usenetdate,
|
||||
// Movie/TV optional fields
|
||||
public Optional|int|string|null $imdbid = new Optional,
|
||||
public Optional|int|string|null $tmdbid = new Optional,
|
||||
public Optional|int|string|null $traktid = new Optional,
|
||||
// TV-only
|
||||
public Optional|string|null $episode_title = new Optional,
|
||||
public Optional|string|null $season = new Optional,
|
||||
public Optional|string|null $episode = new Optional,
|
||||
public Optional|string|null $tvairdate = new Optional,
|
||||
public Optional|int|string|null $tvdbid = new Optional,
|
||||
public Optional|int|string|null $tvrageid = new Optional,
|
||||
public Optional|int|string|null $tvmazeid = new Optional,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Build a ReleaseData from an Eloquent {@see Release} or stdClass row.
|
||||
*/
|
||||
public static function fromRelease(Release|\stdClass $release, User $user): self
|
||||
{
|
||||
$get = static fn (string $key, mixed $default = null): mixed => $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;
|
||||
}
|
||||
}
|
||||
@@ -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<int, Release|\stdClass> $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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+6
-1
@@ -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
|
||||
{
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
|
||||
+12
-8
@@ -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<string, mixed> $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 = [],
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\DTOs;
|
||||
|
||||
/**
|
||||
* Configuration settings for ProcessReleases operations.
|
||||
*/
|
||||
final readonly class ProcessReleasesSettings
|
||||
{
|
||||
public function __construct(
|
||||
public int $collectionDelayTime,
|
||||
public int $crossPostTime,
|
||||
public int $releaseCreationLimit,
|
||||
public int $completion,
|
||||
public int $collectionTimeout,
|
||||
public int $maxSizeToFormRelease,
|
||||
public int $minSizeToFormRelease,
|
||||
public int $minFilesToFormRelease,
|
||||
public int $releaseRetentionDays,
|
||||
public bool $deletePasswordedRelease,
|
||||
public int $miscOtherRetentionHours,
|
||||
public int $miscHashedRetentionHours,
|
||||
public int $partRetentionHours,
|
||||
public ?string $lastRunTime,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Default settings values.
|
||||
*/
|
||||
private const DEFAULTS = [
|
||||
'collectionDelayTime' => 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<string, mixed> $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;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\DTOs;
|
||||
|
||||
/**
|
||||
* Data transfer object for release creation results.
|
||||
*/
|
||||
final readonly class ReleaseCreationResult
|
||||
{
|
||||
public function __construct(
|
||||
public int $added = 0,
|
||||
public int $dupes = 0,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get total number of processed collections.
|
||||
*/
|
||||
public function total(): int
|
||||
{
|
||||
return $this->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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\DTOs;
|
||||
|
||||
/**
|
||||
* Data transfer object for release deletion statistics.
|
||||
*
|
||||
* Tracks the number of releases deleted by category during cleanup operations.
|
||||
*/
|
||||
final readonly class ReleaseDeleteStats
|
||||
{
|
||||
public function __construct(
|
||||
public int $retention = 0,
|
||||
public int $password = 0,
|
||||
public int $duplicate = 0,
|
||||
public int $completion = 0,
|
||||
public int $disabledCategory = 0,
|
||||
public int $categoryMinSize = 0,
|
||||
public int $disabledGenre = 0,
|
||||
public int $miscOther = 0,
|
||||
public int $miscHashed = 0,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a new instance with an incremented counter.
|
||||
*/
|
||||
public function increment(string $field): self
|
||||
{
|
||||
$values = [
|
||||
'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,
|
||||
];
|
||||
|
||||
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<string, int>
|
||||
*/
|
||||
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<string, int> $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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\DTOs;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for Steam Game data.
|
||||
*
|
||||
* Provides a type-safe, immutable representation of game data
|
||||
* retrieved from the Steam API.
|
||||
*/
|
||||
final readonly class SteamGameData
|
||||
{
|
||||
/**
|
||||
* @param int $steamId Steam App ID
|
||||
* @param string $title Game title
|
||||
* @param string $type App type (game, dlc, demo, etc.)
|
||||
* @param string|null $description Short description
|
||||
* @param string|null $detailedDescription Full description with HTML
|
||||
* @param string|null $about About the game text
|
||||
* @param string|null $coverUrl Header image URL
|
||||
* @param string|null $backdropUrl Background image URL
|
||||
* @param array<int, array{thumbnail: ?string, full: ?string}> $screenshots Screenshot URLs
|
||||
* @param array<int, array{id: ?int, name: ?string, thumbnail: ?string, webm: ?string, mp4: ?string}> $movies Movie/trailer data
|
||||
* @param string|null $trailerUrl Primary trailer URL
|
||||
* @param string|null $publisher Publisher name(s)
|
||||
* @param array<string> $developers Developer names
|
||||
* @param string|null $releaseDate Release date (Y-m-d format)
|
||||
* @param array<string> $genres Genre names
|
||||
* @param array<string> $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<string> $platforms Supported platforms
|
||||
* @param array<string, array{minimum: ?string, recommended: ?string}> $requirements System requirements
|
||||
* @param array<int> $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<string, mixed> $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<string, mixed>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\Data\Factories;
|
||||
|
||||
use App\Support\Data\SteamGameData;
|
||||
use App\Support\Data\SteamPriceData;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* Factory for building {@see SteamGameData} from a raw Steam Store API payload.
|
||||
*
|
||||
* Extracted from the previous static `SteamGameData::fromApiResponse` so the
|
||||
* Data object stays a pure structure and reshaping logic lives separately.
|
||||
*/
|
||||
final class SteamGameDataFactory
|
||||
{
|
||||
/**
|
||||
* Build a SteamGameData from a raw Steam API response array.
|
||||
*
|
||||
* @param array<string, mixed> $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<string, mixed> $data
|
||||
* @return array<int, array{thumbnail: ?string, full: ?string}>
|
||||
*/
|
||||
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<string, mixed> $data
|
||||
* @return array<int, array{id: ?int, name: ?string, thumbnail: ?string, webm: ?string, mp4: ?string}>
|
||||
*/
|
||||
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<string, mixed> $data
|
||||
* @return array<int, string>
|
||||
*/
|
||||
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<string, mixed> $data
|
||||
* @return array<int, string>
|
||||
*/
|
||||
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<string, mixed> $data
|
||||
* @return array<int, string>
|
||||
*/
|
||||
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<string, mixed> $data
|
||||
* @return array<string, array{minimum: ?string, recommended: ?string}>
|
||||
*/
|
||||
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<string, mixed> $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<string, mixed> $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<string, mixed> $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<string, mixed> $data
|
||||
* @return array<int, string>
|
||||
*/
|
||||
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'])));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\Data;
|
||||
|
||||
use App\Models\Settings;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
|
||||
|
||||
/**
|
||||
* Configuration settings for ProcessReleases operations.
|
||||
*
|
||||
* Hydrate from raw {@see Settings} rows via {@see self::forDatabase()}
|
||||
* (renamed from `fromDatabase` to avoid spatie/laravel-data's magical-creation
|
||||
* recursion on `self::from()`).
|
||||
*/
|
||||
#[TypeScript]
|
||||
final class ProcessReleasesSettings extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $collectionDelayTime = 2,
|
||||
public int $crossPostTime = 2,
|
||||
public int $releaseCreationLimit = 1000,
|
||||
public int $completion = 0,
|
||||
public int $collectionTimeout = 48,
|
||||
public int $maxSizeToFormRelease = 0,
|
||||
public int $minSizeToFormRelease = 0,
|
||||
public int $minFilesToFormRelease = 0,
|
||||
public int $releaseRetentionDays = 0,
|
||||
public bool $deletePasswordedRelease = false,
|
||||
public int $miscOtherRetentionHours = 0,
|
||||
public int $miscHashedRetentionHours = 0,
|
||||
public int $partRetentionHours = 24,
|
||||
public ?string $lastRunTime = null,
|
||||
) {
|
||||
// Clamp completion to a sane upper bound (legacy `min(100, …)`).
|
||||
if ($this->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<string, mixed> $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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\Data;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
|
||||
|
||||
/**
|
||||
* Data transfer object for release creation results.
|
||||
*/
|
||||
#[TypeScript]
|
||||
final class ReleaseCreationResult extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $added = 0,
|
||||
public int $dupes = 0,
|
||||
) {}
|
||||
|
||||
public function total(): int
|
||||
{
|
||||
return $this->added + $this->dupes;
|
||||
}
|
||||
|
||||
public function hasAddedReleases(): bool
|
||||
{
|
||||
return $this->added > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\Data;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
|
||||
|
||||
/**
|
||||
* Data transfer object for release deletion statistics.
|
||||
*
|
||||
* Tracks the number of releases deleted by category during cleanup operations.
|
||||
*/
|
||||
#[TypeScript]
|
||||
final class ReleaseDeleteStats extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public int $retention = 0,
|
||||
public int $password = 0,
|
||||
public int $duplicate = 0,
|
||||
public int $completion = 0,
|
||||
public int $disabledCategory = 0,
|
||||
public int $categoryMinSize = 0,
|
||||
public int $disabledGenre = 0,
|
||||
public int $miscOther = 0,
|
||||
public int $miscHashed = 0,
|
||||
) {}
|
||||
|
||||
public function increment(string $field): self
|
||||
{
|
||||
$values = [
|
||||
'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,
|
||||
];
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\Data;
|
||||
|
||||
use App\Support\Data\Factories\SteamGameDataFactory;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
|
||||
|
||||
/**
|
||||
* Data Transfer Object for Steam Game data.
|
||||
*
|
||||
* Provides a type-safe representation of game data retrieved from the Steam API.
|
||||
* Reshaping/parsing logic lives in {@see SteamGameDataFactory}.
|
||||
*/
|
||||
#[TypeScript]
|
||||
final class SteamGameData extends Data
|
||||
{
|
||||
/**
|
||||
* @param array<int, array{thumbnail: ?string, full: ?string}> $screenshots
|
||||
* @param array<int, array{id: ?int, name: ?string, thumbnail: ?string, webm: ?string, mp4: ?string}> $movies
|
||||
* @param array<int, string> $developers
|
||||
* @param array<int, string> $genres
|
||||
* @param array<int, string> $categories
|
||||
* @param array<int, string> $platforms
|
||||
* @param array<string, array{minimum: ?string, recommended: ?string}> $requirements
|
||||
* @param array<int, int> $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<string, mixed> $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<string, mixed>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<string, mixed> $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<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'currency' => $this->currency,
|
||||
'initial' => $this->initial,
|
||||
'final' => $this->final,
|
||||
'discount_percent' => $this->discountPercent,
|
||||
'formatted' => $this->formattedPrice,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Transformers;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Release;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Carbon;
|
||||
use League\Fractal\TransformerAbstract;
|
||||
|
||||
class ApiTransformer extends TransformerAbstract
|
||||
{
|
||||
protected User $user;
|
||||
|
||||
/**
|
||||
* ApiTransformer constructor.
|
||||
*
|
||||
* @param User $user The authenticated user for API access
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->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<string, mixed> 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<string, mixed>
|
||||
*/
|
||||
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<string, mixed>
|
||||
*/
|
||||
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<string, mixed>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Transformers;
|
||||
|
||||
use App\Models\RootCategory;
|
||||
use League\Fractal\TransformerAbstract;
|
||||
|
||||
class CategoryTransformer extends TransformerAbstract
|
||||
{
|
||||
/**
|
||||
* Transform a root category into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function transform(RootCategory $category): array
|
||||
{
|
||||
return [
|
||||
'id' => $category->id,
|
||||
'name' => $category->title,
|
||||
'subcategories' => $category->categories()->pluck('title', 'id'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Transformers;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Release;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Carbon;
|
||||
use League\Fractal\TransformerAbstract;
|
||||
|
||||
class DetailsTransformer extends TransformerAbstract
|
||||
{
|
||||
protected User $user;
|
||||
|
||||
/**
|
||||
* DetailsTransformer constructor.
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a release into a details array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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"
|
||||
|
||||
Generated
+394
-226
@@ -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": [
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Carbon\CarbonInterface;
|
||||
use Spatie\LaravelTypeScriptTransformer\Transformers\DtoTransformer;
|
||||
use Spatie\LaravelTypeScriptTransformer\Transformers\SpatieStateTransformer;
|
||||
use Spatie\TypeScriptTransformer\Collectors\DefaultCollector;
|
||||
use Spatie\TypeScriptTransformer\Collectors\EnumCollector;
|
||||
use Spatie\TypeScriptTransformer\Transformers\EnumTransformer;
|
||||
use Spatie\TypeScriptTransformer\Writers\TypeDefinitionWriter;
|
||||
|
||||
return [
|
||||
/*
|
||||
* The paths where typescript-transformer will look for PHP classes
|
||||
* to transform, this will be the `app` path by default.
|
||||
*/
|
||||
|
||||
'auto_discover_types' => [
|
||||
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,
|
||||
];
|
||||
@@ -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
|
||||
|
||||
+29
-11
@@ -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": []
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+2
-1
@@ -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"
|
||||
},
|
||||
|
||||
+797
-48
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user