1
0
Fork 0

Allow bundles to be updated (LEGO and critics for now)

This commit is contained in:
Richard Dern 2024-04-24 20:53:37 +02:00
parent ff9dc1e1b7
commit 7d2ada5e86
17 changed files with 495 additions and 68 deletions

View File

@ -98,17 +98,25 @@ public function add(array $data): string
unset($data['contents']);
} elseif (!empty($data['url'])) {
$existingRef = $this->findAttachmentByOriginalUrl($data['url']);
if (!empty($existingRef)) {
return $existingRef;
}
// Adding from a URL which implies downloading the resource
$contents = Http::throw()->get($data['url'])->body();
$data['filename'] = basename($data['url']);
$data['filename'] = basename($data['url']);
$data['original_url'] = $data['url'];
unset($data['url']);
}
$filename = $this->getFormatedFilename($data['filename']);
$fullPath = sprintf('%s/%s', $this->targetForFiles, $filename);
$relativePath = sprintf('%s/%s/%s', $this->attachmentsDir, $this->kind, $filename);
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$fullPath = sprintf('%s/%s/original.%s', $this->targetForFiles, $reference, $extension);
$relativePath = sprintf('%s/%s/%s/original.%s', $this->attachmentsDir, $this->kind, $reference, $extension);
$data['filename'] = $relativePath;
@ -319,6 +327,20 @@ public function repair()
}
}
/**
* Find an attachment from its original URL, if specified
*/
private function findAttachmentByOriginalUrl(string $url)
{
foreach ($this->manager->get('files', []) as $ref => $data) {
if (array_key_exists('original_url', $data) && $data['original_url'] === $url) {
return $ref;
}
}
return null;
}
/**
* Generate new, random reference for a file
*/

View File

@ -50,6 +50,7 @@ public function handle()
$path = $creator->createBundle();
$this->info('Bundle created successfully!');
$this->call('bundle:update', ['path' => $path]);
$this->line($path);
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Console\Commands\Bundle;
use App\Classes\Bundle;
use App\Exceptions\BundleUpdaterCannotBeFound;
use App\Exceptions\UnableToFindWikidataEntityId;
use App\Services\BundleUpdaters\Facades\BundleUpdater;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class Update extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bundle:update { path? : Path to a specific bundle to update }';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update bundles extended metadata';
/**
* Execute the console command.
*/
public function handle()
{
$path = $this->argument('path') ?? '/';
$bundles = Bundle::findBundles(Storage::disk(env('CONTENT_DISK')), $path, true);
foreach ($bundles as $bundle) {
$this->output->write(sprintf('Updating %s... ', $bundle->getPath()));
try {
$updater = BundleUpdater::getBundleUpdaterFor($bundle);
} catch (BundleUpdaterCannotBeFound $ex) {
$this->line('Not updatable');
continue;
}
try {
while (!$updater->canUpdateBundle()) {
$specs = $updater->formSpecs();
foreach ($specs as $key => $func) {
$result = $func();
$bundle->metadata()->set($key, $result);
$bundle->save();
}
}
} catch (UnableToFindWikidataEntityId $ex) {
$this->comment('Unable to find wikidata entity Id');
continue;
}
$updater->update();
$this->info('OK');
}
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class BundleUpdaterCannotBeFound extends Exception
{
//
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class UnableToFindWikidataEntityId extends Exception
{
//
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Providers;
use App\Services\BundleUpdaters\BundleUpdaterFactory;
use Illuminate\Support\ServiceProvider;
class BundleUpdaterServiceProvider extends ServiceProvider
{
protected array $bundleUpdaters = [
\App\Services\BundleUpdaters\Updaters\CriticUpdater::class,
\App\Services\BundleUpdaters\Updaters\LegoUpdater::class,
];
/**
* Register services.
*/
public function register(): void
{
$this->app->singleton('bundleUpdater.factory', function ($app) {
$factory = new BundleUpdaterFactory();
foreach ($this->bundleUpdaters as $class) {
$factory->registerBundleUpdater($class);
}
return $factory;
});
}
/**
* Bootstrap services.
*/
public function boot(): void
{
//
}
}

View File

@ -29,9 +29,9 @@ public function __construct(protected ?array $data, protected FilesystemAdapter
public function createBundle(): string
{
$rebrickable = app()->make(RebrickableClient::class);
$setData = $rebrickable->getSet($this->data['rebrickable_id']);
$setData = $rebrickable->getSet($this->data['rebrickableId']);
$theme = $rebrickable->getTheme($setData['theme_id']);
$minifigs = $rebrickable->getMinifigs($this->data['rebrickable_id']);
$minifigs = $rebrickable->getMinifigs($this->data['rebrickableId']);
$themeSlug = Str::slug($theme['name']);
$path = sprintf(
@ -39,7 +39,7 @@ public function createBundle(): string
static::$section,
static::$brand,
$themeSlug,
$this->data['product_id']
$this->data['productId']
);
$bundle = new Bundle($path, $this->disk);
@ -53,8 +53,10 @@ public function createBundle(): string
$bundle->markdown()->set('');
$bundle->metadata()->setMany([
'title' => $setData['name'],
'date' => now()->toIso8601String(),
'title' => $setData['name'],
'date' => now()->toIso8601String(),
'productId' => $this->data['productId'],
'rebrickableId' => $this->data['rebrickableId'],
]);
$bundle->metadata('metadata')->setMany([
@ -106,7 +108,7 @@ public function createBundle(): string
*/
public function canCreateBundle(): bool
{
return !empty($this->data['product_id']) && !empty($this->data['rebrickable_id']);
return !empty($this->data['productId']) && !empty($this->data['rebrickableId']);
}
/**
@ -117,13 +119,13 @@ public function formSpecs(): ?array
{
$specs = [];
if (empty($this->data['product_id'])) {
$specs['product_id'] = fn () => text('Product ID', '', '', true);
if (empty($this->data['productId'])) {
$specs['productId'] = fn () => text('Product ID', '', '', true);
} else {
if (empty($this->data['rebrickable_id'])) {
$specs['rebrickable_id'] = fn () => select(
if (empty($this->data['rebrickableId'])) {
$specs['rebrickableId'] = fn () => select(
'Rebrickable Set ID',
app()->make(RebrickableClient::class)->searchSet($this->data['product_id'])
app()->make(RebrickableClient::class)->searchSet($this->data['productId'])
);
}
}

View File

@ -4,13 +4,9 @@
use App\Classes\Bundle;
use App\Exceptions\BundleAlreadyExists;
use App\Services\Wikidata\WikidataClient;
use App\Services\Wikidata\WikidataExtractor;
use Exception;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Str;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\select;
use function Laravel\Prompts\text;
@ -28,12 +24,6 @@ public function __construct(protected ?array $data, protected FilesystemAdapter
*/
public function createBundle(): string
{
$entityId = $this->data['entity_id'];
if ($entityId === false) {
throw new Exception('Bundle creation cancelled');
}
$kind = $this->data['kind'];
$title = $this->data['title'];
$slug = Str::slug($title);
@ -47,30 +37,9 @@ public function createBundle(): string
);
}
$wikidata = app()->make(WikidataClient::class);
$extractor = app()->make(WikidataExtractor::class);
$completeEntity = $wikidata->getEntityData($entityId, true)['entities'][$entityId];
$extractor->extract($completeEntity, $entityId);
$bundle->metadata('wikidata/included')->setMany($extractor->included());
$bundle->metadata('wikidata/excluded')->setMany($extractor->excluded());
$bundle->metadata('wikidata/unused')->setMany($extractor->unused());
$bundle->metadata('wikidata/entity')->setMany($extractor->everythingElse());
$frenchTitle = $bundle->metadata('wikidata/entity')->get('labels.fr.value', null);
$originalTitle = $bundle->metadata('wikidata/entity')->get('labels.en.value', null);
if (!empty($originalTitle)) {
$bundle->metadata()->set('title', $originalTitle);
if (!empty($frenchTitle) && $frenchTitle !== $originalTitle) {
$bundle->metadata()->set('subTitle', $frenchTitle);
}
}
$bundle->metadata()->setMany([
'date' => $date->toIso8601String(),
'title' => $title,
'date' => $date->toIso8601String(),
]);
$bundle->markdown()->set('');
@ -88,8 +57,7 @@ public function canCreateBundle(): bool
{
return
!empty($this->data['kind'])
&& !empty($this->data['title'])
&& array_key_exists('entity_id', $this->data);
&& !empty($this->data['title']);
}
/**
@ -108,18 +76,6 @@ public function formSpecs(): ?array
$specs['title'] = fn () => text('Work title', '', '', true);
}
if (!empty($this->data['kind']) && !empty($this->data['title']) && empty($this->data['entity_id'])) {
$options = app()->make(WikidataClient::class)->searchEntityId($this->data['title']);
if (!empty($options)) {
$specs['entity_id'] = fn () => select('Confirm searched work', $options);
} else {
$specs['entity_id'] = fn () => confirm(
'No entityId was found in Wikidata for this work. Do you want to create the bundle anyway?'
);
}
}
return $specs;
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Services\BundleUpdaters;
use App\Classes\Bundle;
use App\Exceptions\BundleUpdaterCannotBeFound;
class BundleUpdaterFactory
{
/**
* Registered bundle updaters
*/
protected static $bundleUpdaters = [];
/**
* Return a bundle updater instance for specified bundle, if available
*/
public function getBundleUpdaterFor(Bundle $bundle)
{
foreach (self::$bundleUpdaters as $bundleUpdater) {
if ($bundleUpdater::handles($bundle)) {
return $bundleUpdater::make($bundle);
}
}
throw new BundleUpdaterCannotBeFound();
}
/**
* Register a bundle updater
*/
public static function registerBundleUpdater($bundleUpdater)
{
if (!in_array($bundleUpdater, self::$bundleUpdaters)) {
self::$bundleUpdaters[] = $bundleUpdater;
}
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Services\BundleUpdaters\Contracts;
use App\Classes\Bundle;
interface UpdatesBundle
{
/**
* Return a boolean value indicating if the updater can actually make the
* update using known data.
*/
public function canUpdateBundle(): bool;
/**
* Return an array describing what kind of data the updater needs in
* addition to the one it already has
*/
public function formSpecs(): ?array;
/**
* Updates bundle's extended metadata
*/
public function update();
/**
* Return a boolean value indicating if this updater in particular can
* update specified bundle
*/
public static function handles(Bundle $bundle): bool;
/**
* Return an instance of the updater for specified bundle
*/
public static function make(Bundle $bundle): UpdatesBundle;
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Services\BundleUpdaters\Facades;
use Illuminate\Support\Facades\Facade;
class BundleUpdater extends Facade
{
protected static function getFacadeAccessor()
{
return 'bundleUpdater.factory';
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Services\BundleUpdaters\Updaters;
use App\Classes\Bundle;
use App\Services\BundleUpdaters\Contracts\UpdatesBundle;
abstract class BaseUpdater implements UpdatesBundle
{
public function __construct(protected Bundle $bundle)
{
}
/**
* Return an instance of the updater for specified bundle
*/
public static function make(Bundle $bundle): UpdatesBundle
{
return new static($bundle);
}
}

View File

@ -0,0 +1,102 @@
<?php
namespace App\Services\BundleUpdaters\Updaters;
use App\Classes\Bundle;
use App\Exceptions\UnableToFindWikidataEntityId;
use App\Services\Wikidata\WikidataClient;
use App\Services\Wikidata\WikidataExtractor;
use Carbon\Carbon;
use function Laravel\Prompts\select;
use function Laravel\Prompts\text;
class CriticUpdater extends BaseUpdater
{
/**
* Return a boolean value indicating if the updater can actually make the
* update using known data.
*/
public function canUpdateBundle(): bool
{
return !empty($this->bundle->metadata()->get('title'))
&& !empty($this->bundle->metadata()->get('wikidataEntityId'));
}
/**
* Return an array describing what kind of data the updater needs in
* addition to the one it already has
*/
public function formSpecs(): ?array
{
$specs = [];
if (empty($this->bundle->metadata()->get('title'))) {
$specs['title'] = fn () => text('Work title', '', '', true);
}
if (!empty($this->bundle->metadata()->get('title')) && empty($this->bundle->metadata()->get('wikidataEntityId'))) {
$options = app()->make(WikidataClient::class)->searchEntityId($this->bundle->metadata()->get('title'));
if (!empty($options)) {
$specs['wikidataEntityId'] = fn () => select('Confirm searched work', $options);
} else {
throw new UnableToFindWikidataEntityId();
}
}
return $specs;
}
/**
* Updates bundle's extended metadata
*/
public function update()
{
$lastUpdate = Carbon::parse($this->bundle->metadata('wikidata/entity')->get('modified', '1970-01-01'));
$entityId = $this->bundle->metadata()->get('wikidataEntityId');
$wikidata = app()->make(WikidataClient::class);
$extractor = app()->make(WikidataExtractor::class);
if ($lastUpdate->lt(now()->subMonth())) {
$completeEntity = $wikidata->getEntityData($entityId, true)['entities'][$entityId];
} else {
$completeEntity = $this->bundle->metadata('wikidata/entity')->all();
}
$extractor->extract($completeEntity, $entityId);
$this->bundle->metadata('wikidata/included')->setMany($extractor->included());
$this->bundle->metadata('wikidata/excluded')->setMany($extractor->excluded());
$this->bundle->metadata('wikidata/unused')->setMany($extractor->unused());
$this->bundle->metadata('wikidata/entity')->setMany($extractor->everythingElse());
$frenchTitle = $this->bundle->metadata('wikidata/entity')->get('labels.fr.value', null);
$originalTitle = $this->bundle->metadata('wikidata/entity')->get('labels.en.value', null);
if (!empty($originalTitle)) {
$this->bundle->metadata()->set('title', $originalTitle);
if (!empty($frenchTitle) && $frenchTitle !== $originalTitle) {
$this->bundle->metadata()->set('subTitle', $frenchTitle);
}
}
$this->bundle->save();
}
/**
* Return a boolean value indicating if this updater in particular can
* update specified bundle
*/
public static function handles(Bundle $bundle): bool
{
$parts = preg_split('#/#', $bundle->getPath(), -1, PREG_SPLIT_NO_EMPTY);
if ($parts[0] !== 'critiques' || count($parts) !== 3) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace App\Services\BundleUpdaters\Updaters;
use App\Classes\AttachmentsManager;
use App\Classes\Bundle;
use App\Services\Rebrickable\RebrickableClient;
use function Laravel\Prompts\select;
class LegoUpdater extends BaseUpdater
{
/**
* Return a boolean value indicating if the updater can actually make the
* update using known data.
*/
public function canUpdateBundle(): bool
{
return !empty($this->bundle->metadata()->get('rebrickableId'));
}
/**
* Return an array describing what kind of data the updater needs in
* addition to the one it already has
*/
public function formSpecs(): ?array
{
$specs = [];
if (empty($this->bundle->metadata()->get('rebrickableId'))) {
$productId = basename($this->bundle->getPath());
$specs['rebrickableId'] = fn () => select(
'Rebrickable Set ID',
app()->make(RebrickableClient::class)->searchSet($productId)
);
}
return $specs;
}
/**
* Updates bundle's extended metadata
*/
public function update()
{
$productId = basename($this->bundle->getPath());
$rebrickable = app()->make(RebrickableClient::class);
$setData = $rebrickable->getSet($this->bundle->metadata()->get('rebrickableId'));
$theme = $rebrickable->getTheme($setData['theme_id']);
$minifigs = $rebrickable->getMinifigs($this->bundle->metadata()->get('rebrickableId'));
$this->bundle->metadata()->setMany([
'title' => $setData['name'],
'productId' => $productId,
]);
$this->bundle->metadata('metadata')->setMany([
'details' => [
'Identifiant' => $productId,
'Date de sortie' => $setData['year'],
'Nombre de pièces' => $setData['num_parts'],
],
'theme' => [
$theme['name'],
],
]);
$this->bundle->metadata('rebrickable/set')->setMany($setData);
$this->bundle->metadata('rebrickable/theme')->setMany($theme);
$this->bundle->metadata('rebrickable/minifigs')->setMany($minifigs);
if (!empty($setData['set_img_url'])) {
$ref = $this->bundle->attachments(AttachmentsManager::Images)->add([
'url' => $setData['set_img_url'],
'attribution' => '&copy; [Rebrickable](https://rebrickable.com/)',
]);
$this->bundle->metadata()->set('cover', $ref);
}
if (!empty($minifigs)) {
foreach ($minifigs as $minifig) {
if (empty($minifig['set_img_url'])) {
continue;
}
$this->bundle->attachments(AttachmentsManager::Images)->add([
'url' => $minifig['set_img_url'],
'attribution' => '&copy; [Rebrickable](https://rebrickable.com/)',
]);
}
}
$this->bundle->save();
}
/**
* Return a boolean value indicating if this updater in particular can
* update specified bundle
*/
public static function handles(Bundle $bundle): bool
{
$parts = preg_split('#/#', $bundle->getPath(), -1, PREG_SPLIT_NO_EMPTY);
if ($parts[0] !== 'collections' || $parts[1] !== 'lego' || count($parts) !== 4) {
return false;
}
return true;
}
}

View File

@ -23,7 +23,7 @@ public function searchSet(string $setId)
$options = [];
foreach ($response['results'] as $result) {
$options[$result['set_num']] = $result['name'];
$options[$result['set_num']] = sprintf('[%s] %s', $result['set_num'], $result['name']);
}
return $options;

View File

@ -22,7 +22,6 @@ class WikidataExtractor
public function __construct(protected array $exclusions, protected array $inclusions)
{
}
public function included()
@ -63,8 +62,6 @@ public function extract(array $entityData, string $entityId)
$this->excluded = $result['excluded'];
$this->unused = $result['unused'];
unset($entityData['claims']);
$this->everythingElse = $entityData;
}
@ -79,7 +76,7 @@ private function getDeclaredPropertiesInEntity(string $data)
$ids = collect(array_values($matches[0]))->unique()->all();
$properties = WikidataProperty::whereIn('property_id', $ids)->get();
$result = collect($ids)->combine($properties->pluck('label'));
$result = $properties->pluck('label', 'property_id');
return $result->toArray();
}
@ -183,7 +180,7 @@ private function parseClaim(array $data, bool $parentIncluded)
private function parseSnak(array $data, bool $parentIncluded)
{
if (empty($data['datavalue']['value'])) {
dd($data);
return $data;
}
$value = $data['datavalue']['value'];
@ -206,7 +203,7 @@ private function parseSnak(array $data, bool $parentIncluded)
$value = $value['text'];
break;
default:
dd($data['mainsnak']);
return $data;
}
return $value;

View File

@ -4,6 +4,7 @@
App\Providers\AppServiceProvider::class,
App\Providers\BundleCreatorServiceProvider::class,
App\Providers\BundleRendererServiceProvider::class,
App\Providers\BundleUpdaterServiceProvider::class,
App\Providers\PartnersServiceProvider::class,
App\Providers\RebrickableServiceProvider::class,
App\Providers\TelescopeServiceProvider::class,