Skip to content

Commit

Permalink
refactor: backfill actions (#716)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kyrch authored Jul 29, 2024
1 parent 7c0e2b1 commit e1b25f4
Show file tree
Hide file tree
Showing 63 changed files with 1,509 additions and 5,114 deletions.
150 changes: 150 additions & 0 deletions app/Actions/Models/BackfillWikiAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php

declare(strict_types=1);

namespace App\Actions\Models;

use App\Actions\ActionResult;
use App\Actions\Models\Wiki\ApiAction;
use App\Concerns\Models\CanCreateExternalResource;
use App\Concerns\Models\CanCreateImageFromUrl;
use App\Enums\Models\Wiki\ImageFacet;
use App\Enums\Models\Wiki\ResourceSite;
use App\Models\BaseModel;
use App\Models\Wiki\ExternalResource;
use App\Models\Wiki\Image;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

/**
* Class BackfillWikiAction.
*/
abstract class BackfillWikiAction
{
use CanCreateExternalResource;
use CanCreateImageFromUrl;

final public const RESOURCES = 'resources';
final public const IMAGES = 'images';

/**
* Create a new action instance.
*
* @param BaseModel $model
* @param array $toBackfill
*/
public function __construct(protected BaseModel $model, protected array $toBackfill)
{
}

/**
* Handle the action.
*
* @return ActionResult
*/
abstract public function handle(): ActionResult;

/**
* Get the api actions available for the backfill action.
*
* @return array
*/
abstract protected function getApis(): array;

/**
* Create the resources given the response.
*
* @param ApiAction $api
* @return void
*/
protected function forResources(ApiAction $api): void
{
$toBackfill = $this->toBackfill[self::RESOURCES];

foreach ($api->getResources() as $site => $url) {
$site = ResourceSite::from($site);

if (!in_array($site, $toBackfill)) {
Log::info("Resource {$site->localize()} should not be backfilled for {$this->label()} {$this->getModel()->getName()}");
continue;
}

if ($this->getModel()->resources()->getQuery()->where(ExternalResource::ATTRIBUTE_SITE, $site->value)->exists()) {
Log::info("Resource {$site->localize()} already exists for {$this->label()} {$this->getModel()->getName()}");
continue;
}

$resource = $this->getOrCreateResource($this->getModel()::class, $site, $url);

Log::info("Attaching Resource '{$resource->getName()}' to {$this->label()} '{$this->getModel()->getName()}'");
$this->getModel()->resources()->attach($resource);

$this->backfilled($site, self::RESOURCES);
}
}

/**
* Create the images given the response.
*
* @param ApiAction $api
* @return void
*/
protected function forImages(ApiAction $api): void
{
$toBackfill = $this->toBackfill[self::IMAGES];

foreach ($api->getImages() as $facet => $url) {
$facet = ImageFacet::from($facet);

if (!in_array($facet, $toBackfill)) {
Log::info("Skipping {$facet->localize()} for {$this->label()} {$this->getModel()->getName()}");
continue;
}

if ($this->getModel()->images()->getQuery()->where(Image::ATTRIBUTE_FACET, $facet->value)->exists()) {
Log::info("Image {$facet->localize()} already exists for {$this->label()} {$this->getModel()->getName()}");
continue;
}

$image = $this->createImage($url, $facet, $this->getModel());

Log::info("Attaching Image '{$image->getName()}' to {$this->label()} '{$this->getModel()->getName()}'");
$this->getModel()->images()->attach($image);

$this->backfilled($facet, self::IMAGES);
}
}

/**
* Remove element already backfilled.
*
* @param mixed $enum
* @param string $scope
* @return void
*/
protected function backfilled(mixed $enum, string $scope): void
{
$index = array_search($enum, $this->toBackfill[$scope]);

if ($index !== false) {
unset($this->toBackfill[$scope][$index]);
}
}

/**
* Get the model for the action.
*
* @return BaseModel
*/
abstract protected function getModel(): BaseModel;

/**
* Get the human-friendly label for the underlying model.
*
* @return string
*/
protected function label(): string
{
return Str::headline(class_basename($this->getModel()));
}
}
173 changes: 173 additions & 0 deletions app/Actions/Models/Wiki/Anime/ApiAction/AnilistAnimeApiAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<?php

declare(strict_types=1);

namespace App\Actions\Models\Wiki\Anime\ApiAction;

use App\Actions\Models\Wiki\ApiAction;
use App\Enums\Models\Wiki\AnimeSynonymType;
use App\Enums\Models\Wiki\ImageFacet;
use App\Enums\Models\Wiki\ResourceSite;
use App\Models\Wiki\ExternalResource;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;

/**
* Class AnilistAnimeApiAction.
*/
class AnilistAnimeApiAction extends ApiAction
{
/**
* Get the site to backfill.
*
* @return ResourceSite
*/
public function getSite(): ResourceSite
{
return ResourceSite::ANILIST;
}

/**
* Set the response after the request.
*
* @param BelongsToMany<ExternalResource> $resources
* @return static
*/
public function handle(BelongsToMany $resources): static
{
$resource = $resources->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value);

if ($resource instanceof ExternalResource) {
$query = '
query ($id: Int) {
Media (id: $id, type: ANIME) {
title {
romaji
english
native
}
coverImage {
extraLarge
medium
}
externalLinks {
url
site
language
}
}
}
';

$variables = [
'id' => $resource->external_id,
];

$response = Http::post('https://graphql.anilist.co', [
'query' => $query,
'variables' => $variables,
])
->throw()
->json();

$this->response = $response;
}

return $this;
}

/**
* Get the mapped resources.
*
* @return array<int, string>
*/
public function getResources(): array
{
$resources = [];

if ($response = $this->response) {
$links = Arr::get($response, 'data.Media.externalLinks');

foreach ($links as $link) {
$url = Arr::get($link, 'url');
$siteAnilist = Arr::get($link, 'site');
$language = Arr::get($link, 'language');

foreach ($this->getResourcesMapping() as $site => $key) {
if ($siteAnilist === $key) {
if (in_array($siteAnilist, ['Official Site', 'Twitter']) && !in_array($language, ['Japanese', null])) continue;

$resources[$site] = $url;
}
}
}
}

return $resources;
}

/**
* Get the mapped synonyms.
*
* @return array<int|string, string>
*/
public function getSynonyms(): array
{
$synonyms = [];

if ($this->response) {
foreach ($this->getSynonymsMapping() as $type => $key) {
$synonyms[$type] = Arr::get($this->response, $key);
}
}

return $synonyms;
}

/**
* Get the available sites to backfill.
*
* @return array
*/
protected function getResourcesMapping(): array
{
return [
ResourceSite::TWITTER->value => 'Twitter',
ResourceSite::OFFICIAL_SITE->value => 'Official Site',
ResourceSite::NETFLIX->value => 'Netflix',
ResourceSite::CRUNCHYROLL->value => 'Crunchyroll',
ResourceSite::HIDIVE->value => 'HIDIVE',
ResourceSite::AMAZON_PRIME_VIDEO->value => 'Amazon Prime Video',
ResourceSite::HULU->value => 'Hulu',
ResourceSite::DISNEY_PLUS->value => 'Disney Plus',
];
}

/**
* Get the mapping for the images.
*
* @return array<int, string>
*/
protected function getImagesMapping(): array
{
return [
ImageFacet::COVER_SMALL->value => 'data.Media.coverImage.medium',
ImageFacet::COVER_LARGE->value => 'data.Media.coverImage.extraLarge',
];
}

/**
* Get the mapping for the synonyms.
*
* @return array<int, string>
*/
protected function getSynonymsMapping(): array
{
return [
AnimeSynonymType::ENGLISH->value => 'data.Media.title.english',
AnimeSynonymType::NATIVE->value => 'data.Media.title.native',
AnimeSynonymType::OTHER->value => 'data.Media.title.romaji',
];
}
}
Loading

0 comments on commit e1b25f4

Please sign in to comment.