diff --git a/src/Domain/AssetFile/AbstractAssetFileStatusFacade.php b/src/Domain/AssetFile/AbstractAssetFileStatusFacade.php index 779ee037..33827eba 100644 --- a/src/Domain/AssetFile/AbstractAssetFileStatusFacade.php +++ b/src/Domain/AssetFile/AbstractAssetFileStatusFacade.php @@ -21,7 +21,6 @@ use AnzuSystems\CoreDamBundle\Exception\AssetFileProcessFailed; use AnzuSystems\CoreDamBundle\Exception\DuplicateAssetFileException; use AnzuSystems\CoreDamBundle\Exception\ForbiddenOperationException; -use AnzuSystems\CoreDamBundle\Exception\RuntimeException; use AnzuSystems\CoreDamBundle\Logger\DamLogger; use AnzuSystems\CoreDamBundle\Messenger\Message\AssetRefreshPropertiesMessage; use AnzuSystems\CoreDamBundle\Model\Dto\Asset\AssetAdmFinishDto; @@ -37,6 +36,7 @@ use Doctrine\ORM\NonUniqueResultException; use League\Flysystem\FilesystemException; use Psr\Cache\InvalidArgumentException; +use RuntimeException; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\Service\Attribute\Required; use Throwable; @@ -196,9 +196,16 @@ public function finishUpload(AssetAdmFinishDto $assetFinishDto, AssetFile $asset */ public function storeAndProcess(AssetFile $assetFile, ?AdapterFile $file = null, bool $dispatchPropertyRefresh = true): AssetFile { + $lockName = $assetFile->getAssetType()->value . '_' . $assetFile->getLicence()->getId(); + try { if ($assetFile->getAssetAttributes()->getStatus()->is(AssetFileProcessStatus::Uploaded)) { - $file = $this->store($assetFile, $file); + $file = $file ?: $this->createFile($assetFile); + $this->fileAttributesPostProcessor->processAttributes($assetFile, $file); + $this->fileAttributesPostProcessor->processChecksum($assetFile, $file); + // we need to lock process due to duplicity checks + $this->resourceLocker->lock($lockName); + $this->store($assetFile, $file); } if (null === $file) { throw new RuntimeException(sprintf('AssetFile (%s) cant be processed without file', $assetFile->getId())); @@ -206,8 +213,10 @@ public function storeAndProcess(AssetFile $assetFile, ?AdapterFile $file = null, if ($assetFile->getAssetAttributes()->getStatus()->is(AssetFileProcessStatus::Stored)) { $this->chunkFileManager->clearChunks($assetFile); $this->process($assetFile, $file, $dispatchPropertyRefresh); + $this->resourceLocker->unLock($lockName); } } catch (DuplicateAssetFileException $duplicateAssetFileException) { + $this->resourceLocker->unLock($lockName); $assetFile->getAssetAttributes()->setOriginAssetId( (string) $duplicateAssetFileException->getOldAsset()->getId() ); @@ -218,6 +227,7 @@ public function storeAndProcess(AssetFile $assetFile, ?AdapterFile $file = null, $this->assetStatusManager->toDuplicate($assetFile); $this->assetFileEventDispatcher->dispatchAssetFileChanged($assetFile); } catch (AssetFileProcessFailed $assetFileProcessFailed) { + $this->resourceLocker->unLock($lockName); $this->assetStatusManager->toFailed( $assetFile, $assetFileProcessFailed->getAssetFileFailedType(), @@ -225,12 +235,15 @@ public function storeAndProcess(AssetFile $assetFile, ?AdapterFile $file = null, ); $this->assetFileEventDispatcher->dispatchAssetFileChanged($assetFile); } catch (Throwable $exception) { + $this->resourceLocker->unLock($lockName); $this->assetStatusManager->toFailed( $assetFile, AssetFileFailedType::Unknown, $exception ); $this->assetFileEventDispatcher->dispatchAssetFileChanged($assetFile); + } finally { + $this->resourceLocker->unLock($lockName); } return $assetFile; @@ -244,20 +257,10 @@ public function storeAndProcess(AssetFile $assetFile, ?AdapterFile $file = null, * @throws TransportExceptionInterface * @throws Throwable */ - public function store(AssetFile $assetFile, ?AdapterFile $file = null): AdapterFile + public function store(AssetFile $assetFile, AdapterFile $file): AdapterFile { - $file = $file ?: $this->createFile($assetFile); - - $this->fileAttributesPostProcessor->processAttributes($assetFile, $file); - $this->fileAttributesPostProcessor->processChecksum($assetFile, $file); - - $lockName = $assetFile->getAssetType()->value . '_' . $assetFile->getLicence()->getId(); - $this->resourceLocker->lock($lockName); - $originAssetFile = $this->checkDuplicate($assetFile); if ($originAssetFile) { - $this->resourceLocker->unLock($lockName); - throw new DuplicateAssetFileException( oldAsset: $originAssetFile, newAsset: $assetFile @@ -269,9 +272,7 @@ public function store(AssetFile $assetFile, ?AdapterFile $file = null): AdapterF $this->assetFileStorageOperator->save($assetFile, $file); $this->assetStatusManager->toStored($assetFile); $this->assetManager->commit(); - $this->resourceLocker->unLock($lockName); } catch (Throwable $exception) { - $this->resourceLocker->unLock($lockName); $this->assetManager->rollback(); throw $exception; diff --git a/src/Domain/AssetFile/AssetFileStatusInterface.php b/src/Domain/AssetFile/AssetFileStatusInterface.php index a1f0d34a..434206a5 100644 --- a/src/Domain/AssetFile/AssetFileStatusInterface.php +++ b/src/Domain/AssetFile/AssetFileStatusInterface.php @@ -11,7 +11,7 @@ #[AutoconfigureTag] interface AssetFileStatusInterface { - public function store(AssetFile $assetFile, ?AdapterFile $file): AdapterFile; + public function store(AssetFile $assetFile, AdapterFile $file): AdapterFile; public function process(AssetFile $assetFile, AdapterFile $file, bool $dispatchPropertyRefresh): AssetFile; diff --git a/src/Domain/AssetMetadata/AssetMetadataProcessor.php b/src/Domain/AssetMetadata/AssetMetadataProcessor.php index 2ea75aff..94d44cf3 100644 --- a/src/Domain/AssetMetadata/AssetMetadataProcessor.php +++ b/src/Domain/AssetMetadata/AssetMetadataProcessor.php @@ -75,6 +75,6 @@ private function provideCommonMetadata(array $rawMetadata, array $allowedMetadat private function parseValue(string $value): string { - return htmlspecialchars(strip_tags($value)); + return strip_tags($value); } } diff --git a/src/Domain/Job/Processor/JobPodcastSynchronizerProcessor.php b/src/Domain/Job/Processor/JobPodcastSynchronizerProcessor.php index ec11e393..bf558456 100644 --- a/src/Domain/Job/Processor/JobPodcastSynchronizerProcessor.php +++ b/src/Domain/Job/Processor/JobPodcastSynchronizerProcessor.php @@ -15,6 +15,7 @@ use AnzuSystems\CoreDamBundle\Model\ValueObject\PodcastSynchronizerPointer; use AnzuSystems\CoreDamBundle\Repository\PodcastRepository; use AnzuSystems\SerializerBundle\Exception\SerializerException; +use DateTimeImmutable; use DateTimeInterface; use Generator; @@ -28,9 +29,17 @@ public function __construct( private readonly PodcastImportIterator $importIterator, private readonly PodcastRepository $podcastRepository, private int $bulkSize = self::BULK_SIZE, + private ?DateTimeImmutable $minImportFrom = null ) { } + public function setMinImportFrom(?DateTimeImmutable $minImportFrom): self + { + $this->minImportFrom = $minImportFrom; + + return $this; + } + public function setBulkSize(int $bulkSize): self { $this->bulkSize = $bulkSize; @@ -45,15 +54,26 @@ public static function getSupportedJob(): string /** * @param JobPodcastSynchronizer $job - * * @throws SerializerException */ public function process(JobInterface $job): void + { + $this->start($job); + $this->processPodcasts($job); + } + + /** + * @throws SerializerException + */ + private function processPodcasts(JobPodcastSynchronizer $job): void { if ($job->isFullSync()) { $this->importFull( job: $job, - generator: $this->importIterator->iterate(PodcastSynchronizerPointer::fromString($job->getLastBatchProcessedRecord())) + generator: $this->importIterator->iterate( + pointer: PodcastSynchronizerPointer::fromString($job->getLastBatchProcessedRecord()), + minImportFrom: $this->minImportFrom + ) ); return; @@ -72,8 +92,9 @@ public function process(JobInterface $job): void job: $job, generator: $this->importIterator->iteratePodcast( pointer: PodcastSynchronizerPointer::fromString($job->getLastBatchProcessedRecord()), - podcastToImport: $podcast - ) + podcastToImport: $podcast, + minImportFrom: $this->minImportFrom + ), ); } } diff --git a/src/Domain/Podcast/PodcastImportIterator.php b/src/Domain/Podcast/PodcastImportIterator.php index 08f750a6..5a6beb53 100644 --- a/src/Domain/Podcast/PodcastImportIterator.php +++ b/src/Domain/Podcast/PodcastImportIterator.php @@ -4,6 +4,7 @@ namespace AnzuSystems\CoreDamBundle\Domain\Podcast; +use AnzuSystems\CoreDamBundle\App; use AnzuSystems\CoreDamBundle\Entity\Podcast; use AnzuSystems\CoreDamBundle\Exception\InvalidArgumentException; use AnzuSystems\CoreDamBundle\HttpClient\RssClient; @@ -16,13 +17,15 @@ use DateTimeImmutable; use Generator; -final readonly class PodcastImportIterator +final class PodcastImportIterator { + private const string MIN_IMPORT_FROM_MODIFIER = '- 3 months'; + public function __construct( - private RssClient $client, - private PodcastRssReader $reader, - private PodcastRepository $podcastRepository, - private DamLogger $damLogger, + private readonly RssClient $client, + private readonly PodcastRssReader $reader, + private readonly PodcastRepository $podcastRepository, + private readonly DamLogger $damLogger, ) { } @@ -31,7 +34,7 @@ public function __construct( * * @throws SerializerException */ - public function iterate(PodcastSynchronizerPointer $pointer): Generator + public function iterate(PodcastSynchronizerPointer $pointer, ?DateTimeImmutable $minImportFrom = null): Generator { $podcastToImport = $this->getPodcastToImport($pointer); if (null === $podcastToImport) { @@ -39,7 +42,7 @@ public function iterate(PodcastSynchronizerPointer $pointer): Generator } while ($podcastToImport) { - foreach ($this->iteratePodcast($pointer, $podcastToImport) as $item) { + foreach ($this->iteratePodcast($pointer, $podcastToImport, $minImportFrom) as $item) { yield $item; } @@ -58,7 +61,7 @@ public function iterate(PodcastSynchronizerPointer $pointer): Generator * * @throws SerializerException */ - public function iteratePodcast(PodcastSynchronizerPointer $pointer, Podcast $podcastToImport): Generator + public function iteratePodcast(PodcastSynchronizerPointer $pointer, Podcast $podcastToImport, ?DateTimeImmutable $minImportFrom = null): Generator { try { $this->reader->initReader($this->client->readPodcastRss($podcastToImport)); @@ -74,7 +77,7 @@ public function iteratePodcast(PodcastSynchronizerPointer $pointer, Podcast $pod return; } - $startFromDate = $this->getImportFrom($pointer, $podcastToImport); + $startFromDate = $this->getImportFrom($pointer, $minImportFrom); foreach ($this->reader->readItems($startFromDate) as $podcastItem) { yield new PodcastImportIteratorDto( @@ -85,18 +88,16 @@ public function iteratePodcast(PodcastSynchronizerPointer $pointer, Podcast $pod } } - private function getImportFrom(PodcastSynchronizerPointer $pointer, Podcast $podcast): ?DateTimeImmutable + private function getImportFrom(PodcastSynchronizerPointer $pointer, ?DateTimeImmutable $minImportFrom): ?DateTimeImmutable { - if (null === $podcast->getDates()->getImportFrom()) { - return $pointer->getPubDate(); - } + $minImportFrom = $minImportFrom ?? App::getAppDate()->modify(self::MIN_IMPORT_FROM_MODIFIER); if (null === $pointer->getPubDate()) { - return $podcast->getDates()->getImportFrom(); + return $minImportFrom; } - return $podcast->getDates()->getImportFrom() > $pointer->getPubDate() - ? $podcast->getDates()->getImportFrom() + return $minImportFrom > $pointer->getPubDate() + ? $minImportFrom : $pointer->getPubDate(); } diff --git a/src/Domain/Podcast/PodcastRssReader.php b/src/Domain/Podcast/PodcastRssReader.php index 1481157b..e4c04fe9 100644 --- a/src/Domain/Podcast/PodcastRssReader.php +++ b/src/Domain/Podcast/PodcastRssReader.php @@ -20,6 +20,7 @@ final class PodcastRssReader { + public const string RSS_DATE_FORMAT = 'D, d M Y H:i:s T'; private const string ITUNES_KEY_KEY = 'itunes'; private SimpleXMLElement $body; @@ -91,6 +92,7 @@ public function readItems(?DateTimeImmutable $from = null): Generator { foreach (array_reverse($this->body->channel?->xpath('item') ?? []) as $item) { $item = $this->readItem($item); + if ($item->getPubDate() && $from && $from > $item->getPubDate()) { continue; } @@ -145,7 +147,7 @@ private function getPublicationDate(SimpleXMLElement $element): ?DateTimeImmutab { $publicationDateString = (string) $element->pubDate; $publicationDate = DateTimeImmutable::createFromFormat( - 'D, d M Y H:i:s T', + self::RSS_DATE_FORMAT, $publicationDateString, ); diff --git a/src/Elasticsearch/IndexFactory/AssetIndexFactory.php b/src/Elasticsearch/IndexFactory/AssetIndexFactory.php index 89e5b1cf..f1447016 100644 --- a/src/Elasticsearch/IndexFactory/AssetIndexFactory.php +++ b/src/Elasticsearch/IndexFactory/AssetIndexFactory.php @@ -12,7 +12,6 @@ use AnzuSystems\CoreDamBundle\Entity\DocumentFile; use AnzuSystems\CoreDamBundle\Entity\ImageFile; use AnzuSystems\CoreDamBundle\Entity\Interfaces\ExtSystemIndexableInterface; -use AnzuSystems\CoreDamBundle\Entity\Keyword; use AnzuSystems\CoreDamBundle\Entity\PodcastEpisode; use AnzuSystems\CoreDamBundle\Entity\VideoFile; use AnzuSystems\CoreDamBundle\Helper\CollectionHelper; @@ -48,14 +47,13 @@ public function buildFromEntity(ExtSystemIndexableInterface $entity): array return [ 'id' => $entity->getId(), + 'mainFileId' => $entity->getMainFile()?->getId(), 'fileIds' => array_values(CollectionHelper::traversableToIds( $entity->getSlots(), fn (AssetSlot $slot): string => (string) $slot->getAssetFile()->getId() )), - 'keywordIds' => array_values(CollectionHelper::traversableToIds( - $entity->getKeywords(), - fn (Keyword $keyword): string => (string) $keyword->getId() - )), + 'keywordIds' => array_values(CollectionHelper::traversableToIds($entity->getKeywords())), + 'authorIds' => array_values(CollectionHelper::traversableToIds($entity->getAuthors())), 'type' => $entity->getAttributes()->getAssetType()->toString(), 'status' => $entity->getAttributes()->getStatus(), 'described' => $entity->getAssetFlags()->isDescribed(), @@ -64,6 +62,7 @@ public function buildFromEntity(ExtSystemIndexableInterface $entity): array 'generatedBySystem' => $entity->getAssetFlags()->isGeneratedBySystem(), 'modifiedAt' => $entity->getModifiedAt()->getTimestamp(), 'createdAt' => $entity->getCreatedAt()->getTimestamp(), + 'createdById' => $entity->getCreatedBy()->getId(), 'licence' => $entity->getLicence()->getId(), 'distributedInServices' => array_values($entity->getAssetFileProperties()->getDistributesInServices()), 'slotNames' => array_values($entity->getAssetFileProperties()->getSlotNames()), diff --git a/src/Elasticsearch/QueryFactory/AssetQueryFactory.php b/src/Elasticsearch/QueryFactory/AssetQueryFactory.php index 5b067cf2..5f6a2f69 100644 --- a/src/Elasticsearch/QueryFactory/AssetQueryFactory.php +++ b/src/Elasticsearch/QueryFactory/AssetQueryFactory.php @@ -12,6 +12,7 @@ use AnzuSystems\CoreDamBundle\Entity\AssetLicence; use AnzuSystems\CoreDamBundle\Entity\CustomFormElement; use AnzuSystems\CoreDamBundle\Entity\ExtSystem; +use AnzuSystems\CoreDamBundle\Helper\UuidHelper; final class AssetQueryFactory extends AbstractQueryFactory { @@ -40,6 +41,10 @@ protected function getMust(SearchDtoInterface $searchDto, ExtSystem $extSystem): $customDataFields = array_unique($customDataFields); $customDataFields = array_merge($customDataFields, ['title']); + if (UuidHelper::isUuid($searchDto->getText())) { + return parent::getMust($searchDto, $extSystem); + } + if ($searchDto->getText()) { return [ 'multi_match' => [ @@ -61,6 +66,23 @@ protected function getMust(SearchDtoInterface $searchDto, ExtSystem $extSystem): protected function getFilter(SearchDtoInterface $searchDto): array { $filter = []; + if ($searchDto instanceof AssetAdmSearchLicenceCollectionDto) { + $this->applyLicenceCollectionFilter($filter, $searchDto); + } + + if (UuidHelper::isUuid($searchDto->getText())) { + $filter[] = $this->getAssetIdAndMainFileIdFilter([$searchDto->getText()]); + + // other filters should not be applied + return $filter; + } + + if (false === empty($searchDto->getAssetAndMainFileIds())) { + $filter[] = $this->getAssetIdAndMainFileIdFilter($searchDto->getAssetAndMainFileIds()); + + // other filters should not be applied + return $filter; + } if (false === (null === $searchDto->isVisible())) { $filter[] = ['terms' => ['visible' => [$searchDto->isVisible()]]]; @@ -107,9 +129,18 @@ protected function getFilter(SearchDtoInterface $searchDto): array if (false === empty($searchDto->getAssetIds())) { $filter[] = ['terms' => ['fileIds' => $searchDto->getAssetIds()]]; } + if (false === empty($searchDto->getMainFileIds())) { + $filter[] = ['terms' => ['mainFileId' => $searchDto->getMainFileIds()]]; + } if (false === empty($searchDto->getKeywordIds())) { $filter[] = ['terms' => ['keywordIds.keywordId' => $searchDto->getKeywordIds()]]; } + if (false === empty($searchDto->getAuthorIds())) { + $filter[] = ['terms' => ['authorIds.authorId' => $searchDto->getAuthorIds()]]; + } + if (false === empty($searchDto->getCreatedByIds())) { + $filter[] = ['terms' => ['createdById' => $searchDto->getCreatedByIds()]]; + } $this->applyRangeFilter($filter, 'pixelSize', $searchDto->getPixelSizeFrom(), $searchDto->getPixelSizeUntil()); $this->applyRangeFilter($filter, 'ratioWidth', $searchDto->getRatioWidthFrom(), $searchDto->getRatioWidthUntil()); @@ -122,13 +153,21 @@ protected function getFilter(SearchDtoInterface $searchDto): array $this->applyRangeFilter($filter, 'slotsCount', $searchDto->getSlotsCountFrom(), $searchDto->getSlotsCountUntil()); $this->applyRangeFilter($filter, 'createdAt', $searchDto->getCreatedAtFrom()?->getTimestamp(), $searchDto->getCreatedAtUntil()?->getTimestamp()); - if ($searchDto instanceof AssetAdmSearchLicenceCollectionDto) { - $this->applyLicenceCollectionFilter($filter, $searchDto); - } - return $filter; } + private function getAssetIdAndMainFileIdFilter(array $ids): array + { + return [ + 'bool' => [ + 'should' => [ + ['terms' => ['id' => $ids]], + ['terms' => ['mainFileId' => $ids]], + ], + ], + ]; + } + private function applyLicenceCollectionFilter(array &$filter, AssetAdmSearchLicenceCollectionDto $dto): void { if ($dto->getLicences()->isEmpty()) { diff --git a/src/Elasticsearch/QueryFactory/AuthorQueryFactory.php b/src/Elasticsearch/QueryFactory/AuthorQueryFactory.php index 56f73323..cbb906f1 100644 --- a/src/Elasticsearch/QueryFactory/AuthorQueryFactory.php +++ b/src/Elasticsearch/QueryFactory/AuthorQueryFactory.php @@ -26,11 +26,10 @@ protected function getMust(SearchDtoInterface $searchDto, ExtSystem $extSystem): return [ 'multi_match' => [ 'query' => $searchDto->getText(), - 'type' => 'bool_prefix', + 'type' => 'most_fields', 'fields' => [ 'name', - 'name._2gram', - 'name._3gram', + 'name.edgegrams', ], ], ]; diff --git a/src/Elasticsearch/QueryFactory/KeywordQueryFactory.php b/src/Elasticsearch/QueryFactory/KeywordQueryFactory.php index c62639df..0f7a04a1 100644 --- a/src/Elasticsearch/QueryFactory/KeywordQueryFactory.php +++ b/src/Elasticsearch/QueryFactory/KeywordQueryFactory.php @@ -26,11 +26,10 @@ protected function getMust(SearchDtoInterface $searchDto, ExtSystem $extSystem): return [ 'multi_match' => [ 'query' => $searchDto->getText(), - 'type' => 'bool_prefix', + 'type' => 'most_fields', 'fields' => [ 'name', - 'name._2gram', - 'name._3gram', + 'name.edgegrams', ], ], ]; diff --git a/src/Elasticsearch/SearchDto/AssetAdmSearchDto.php b/src/Elasticsearch/SearchDto/AssetAdmSearchDto.php index 41d2280d..7cbf2e98 100644 --- a/src/Elasticsearch/SearchDto/AssetAdmSearchDto.php +++ b/src/Elasticsearch/SearchDto/AssetAdmSearchDto.php @@ -23,10 +23,18 @@ class AssetAdmSearchDto extends AbstractSearchDto #[Serialize] protected string $text = ''; + #[Serialize(handler: ArrayStringHandler::class)] + #[Assert\Count(max: 20, maxMessage: ValidationException::ERROR_FIELD_LENGTH_MAX)] + protected array $assetAndMainFileIds = []; + #[Serialize(handler: ArrayStringHandler::class)] #[Assert\Count(max: 20, maxMessage: ValidationException::ERROR_FIELD_LENGTH_MAX)] protected array $assetIds = []; + #[Serialize(handler: ArrayStringHandler::class)] + #[Assert\Count(max: 20, maxMessage: ValidationException::ERROR_FIELD_LENGTH_MAX)] + protected array $mainFileIds = []; + #[Serialize(handler: ArrayStringHandler::class)] #[Assert\Count(max: 20, maxMessage: ValidationException::ERROR_FIELD_LENGTH_MAX)] protected array $podcastIds = []; @@ -35,6 +43,14 @@ class AssetAdmSearchDto extends AbstractSearchDto #[Assert\Count(max: 20, maxMessage: ValidationException::ERROR_FIELD_LENGTH_MAX)] protected array $keywordIds = []; + #[Serialize(handler: ArrayStringHandler::class)] + #[Assert\Count(max: 20, maxMessage: ValidationException::ERROR_FIELD_LENGTH_MAX)] + protected array $authorIds = []; + + #[Serialize(handler: ArrayStringHandler::class)] + #[Assert\Count(max: 20, maxMessage: ValidationException::ERROR_FIELD_LENGTH_MAX)] + protected array $createdByIds = []; + #[Serialize(handler: ArrayStringHandler::class)] #[Assert\Choice(choices: AssetType::CHOICES, multiple: true, multipleMessage: ValidationException::ERROR_FIELD_INVALID)] #[Assert\Count(max: 4, maxMessage: ValidationException::ERROR_FIELD_LENGTH_MAX)] @@ -634,4 +650,52 @@ public function setKeywordIds(array $keywordIds): void { $this->keywordIds = $keywordIds; } + + public function getAuthorIds(): array + { + return $this->authorIds; + } + + public function setAuthorIds(array $authorIds): self + { + $this->authorIds = $authorIds; + + return $this; + } + + public function getMainFileIds(): array + { + return $this->mainFileIds; + } + + public function setMainFileIds(array $mainFileIds): self + { + $this->mainFileIds = $mainFileIds; + + return $this; + } + + public function getCreatedByIds(): array + { + return $this->createdByIds; + } + + public function setCreatedByIds(array $createdByIds): self + { + $this->createdByIds = $createdByIds; + + return $this; + } + + public function getAssetAndMainFileIds(): array + { + return $this->assetAndMainFileIds; + } + + public function setAssetAndMainFileIds(array $assetAndMainFileIds): self + { + $this->assetAndMainFileIds = $assetAndMainFileIds; + + return $this; + } } diff --git a/src/Entity/Chunk.php b/src/Entity/Chunk.php index b9739d10..e818907e 100644 --- a/src/Entity/Chunk.php +++ b/src/Entity/Chunk.php @@ -16,7 +16,7 @@ use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: ChunkRepository::class)] -#[ORM\Index(fields: ['offset'], name: 'IDX_offset')] +#[ORM\Index(name: 'IDX_offset', fields: ['offset'])] class Chunk implements UuidIdentifiableInterface, FileSystemStorableInterface, AssetLicenceInterface { use UuidIdentityTrait; @@ -33,7 +33,7 @@ class Chunk implements UuidIdentifiableInterface, FileSystemStorableInterface, A private int $size; #[Serialize] - #[ORM\Column(type: Types::STRING, length: 32)] + #[ORM\Column(type: Types::STRING, length: 255)] private string $mimeType; #[ORM\Column(type: Types::STRING, length: 255)] diff --git a/src/Entity/Embeds/AssetFileAttributes.php b/src/Entity/Embeds/AssetFileAttributes.php index f17fee64..76288286 100644 --- a/src/Entity/Embeds/AssetFileAttributes.php +++ b/src/Entity/Embeds/AssetFileAttributes.php @@ -29,7 +29,7 @@ class AssetFileAttributes #[ORM\Column(type: Types::STRING, length: 255)] private string $originFileName; - #[ORM\Column(type: Types::STRING, length: 127)] + #[ORM\Column(type: Types::STRING, length: 255)] private string $mimeType; #[ORM\Column(type: Types::STRING, length: 127)] diff --git a/src/Entity/Embeds/PodcastDates.php b/src/Entity/Embeds/PodcastDates.php index 19e43da4..bdda485e 100644 --- a/src/Entity/Embeds/PodcastDates.php +++ b/src/Entity/Embeds/PodcastDates.php @@ -21,11 +21,17 @@ public function __construct() $this->setImportFrom(null); } + /** + * @deprecated + */ public function getImportFrom(): ?DateTimeImmutable { return $this->importFrom; } + /** + * @deprecated + */ public function setImportFrom(?DateTimeImmutable $importFrom): self { $this->importFrom = $importFrom; diff --git a/src/Helper/StringHelper.php b/src/Helper/StringHelper.php index 9572efa8..53efd976 100644 --- a/src/Helper/StringHelper.php +++ b/src/Helper/StringHelper.php @@ -15,6 +15,15 @@ public static function parseLength(string $input, int $length): string return mb_substr($input, 0, $length); } + public static function getFirstChar(string $string): string + { + if (false === empty($string)) { + return mb_substr($string, 0, 1); + } + + return ''; + } + public static function normalize(string $input, StringNormalizerConfiguration $configuration): string { if ($configuration->isTrim()) { @@ -33,7 +42,7 @@ public static function parseString( ?int $length = null, bool $trim = true, ): string { - $string = htmlspecialchars(strip_tags($input)); + $string = strip_tags($input); if ($length) { $string = self::parseLength($string, $length); } diff --git a/src/Helper/UuidHelper.php b/src/Helper/UuidHelper.php new file mode 100644 index 00000000..4f0bd175 --- /dev/null +++ b/src/Helper/UuidHelper.php @@ -0,0 +1,16 @@ + [ 'type' => 'keyword', ], + 'mainFileId' => [ + 'type' => 'keyword', + ], + 'createdById' => [ + 'type' => 'keyword', + ], 'type' => [ 'type' => 'keyword', ], @@ -32,6 +38,14 @@ ], ], ], + 'authorIds' => [ + 'type' => 'text', + 'fields' => [ + 'authorId' => [ + 'type' => 'keyword', + ], + ], + ], 'fileIds' => [ 'type' => 'keyword', ], diff --git a/src/Resources/config/elasticsearch/author.php b/src/Resources/config/elasticsearch/author.php index 9c32f373..1066d0eb 100644 --- a/src/Resources/config/elasticsearch/author.php +++ b/src/Resources/config/elasticsearch/author.php @@ -20,7 +20,14 @@ 'type' => 'boolean', ], 'name' => [ - 'type' => 'search_as_you_type', + 'type' => 'text', + 'analyzer' => 'exact_stop', + 'fields' => [ + 'edgegrams' => [ + 'type' => 'text', + 'analyzer' => 'edgegrams', + ], + ], ], 'type' => [ 'type' => 'keyword', diff --git a/src/Resources/config/elasticsearch/keyword.php b/src/Resources/config/elasticsearch/keyword.php index 17f55f48..41d089e1 100644 --- a/src/Resources/config/elasticsearch/keyword.php +++ b/src/Resources/config/elasticsearch/keyword.php @@ -17,7 +17,14 @@ 'type' => 'boolean', ], 'name' => [ - 'type' => 'search_as_you_type', + 'type' => 'text', + 'analyzer' => 'exact_stop', + 'fields' => [ + 'edgegrams' => [ + 'type' => 'text', + 'analyzer' => 'edgegrams', + ], + ], ], 'createdAt' => [ 'type' => 'date', diff --git a/tests/Domain/Job/JobPodcastSynchronizerProcessorTest.php b/tests/Domain/Job/JobPodcastSynchronizerProcessorTest.php index 320baf08..6c29486c 100644 --- a/tests/Domain/Job/JobPodcastSynchronizerProcessorTest.php +++ b/tests/Domain/Job/JobPodcastSynchronizerProcessorTest.php @@ -10,10 +10,12 @@ use AnzuSystems\CommonBundle\Model\Enum\JobStatus; use AnzuSystems\CommonBundle\Tests\AnzuKernelTestCase; use AnzuSystems\Contracts\Entity\AnzuUser; +use AnzuSystems\CoreDamBundle\App; use AnzuSystems\CoreDamBundle\DataFixtures\AssetLicenceFixtures as BaseAssetLicenceFixtures; use AnzuSystems\CoreDamBundle\DataFixtures\PodcastFixtures; use AnzuSystems\CoreDamBundle\Domain\Job\Processor\JobPodcastSynchronizerProcessor; use AnzuSystems\CoreDamBundle\Domain\Job\Processor\JobUserDataDeleteProcessor; +use AnzuSystems\CoreDamBundle\Domain\Podcast\PodcastRssReader; use AnzuSystems\CoreDamBundle\Entity\AssetLicence; use AnzuSystems\CoreDamBundle\Entity\JobPodcastSynchronizer; use AnzuSystems\CoreDamBundle\Entity\PodcastEpisode; @@ -23,6 +25,8 @@ use AnzuSystems\CoreDamBundle\Tests\Data\Entity\User; use AnzuSystems\CoreDamBundle\Tests\Data\Fixtures\AssetLicenceFixtures; use AnzuSystems\CoreDamBundle\Tests\Data\Fixtures\JobFixtures; +use AnzuSystems\CoreDamBundle\Tests\HttpClient\RssPodcastMock; +use DateTimeInterface; use Doctrine\ORM\EntityManagerInterface; final class JobPodcastSynchronizerProcessorTest extends CoreDamKernelTestCase @@ -59,19 +63,25 @@ public function testFullSyncProcess(): void $this->assertCount(3, $podcast1->getEpisodes()); $this->assertCount(1, $podcast3->getEpisodes()); $this->assertEquals(JobStatus::AwaitingBatchProcess, $job->getStatus()); - $this->assertEquals(sprintf('%s|%s', PodcastFixtures::PODCAST_1, '2023-03-05T23:01:45+00:00'), $job->getLastBatchProcessedRecord()); + + $pointerDate = App::getAppDate()->modify(RssPodcastMock::THIRD_RSS_DATE_MODEFIER)->format(DateTimeInterface::ATOM); + $this->assertEquals(sprintf('%s|%s', PodcastFixtures::PODCAST_1, $pointerDate), $job->getLastBatchProcessedRecord()); $this->synchronizerProcessor->process($job); $this->entityManager->refresh($podcast1); $this->assertCount(5, $podcast1->getEpisodes()); $this->assertEquals(JobStatus::AwaitingBatchProcess, $job->getStatus()); - $this->assertEquals(sprintf('%s|%s', PodcastFixtures::PODCAST_1, '2023-03-07T23:01:44+00:00'), $job->getLastBatchProcessedRecord()); + + $pointerDate = App::getAppDate()->modify(RssPodcastMock::FIRST_RSS_DATE_MODEFIER)->format(DateTimeInterface::ATOM); + $this->assertEquals(sprintf('%s|%s', PodcastFixtures::PODCAST_1, $pointerDate), $job->getLastBatchProcessedRecord()); $this->synchronizerProcessor->process($job); $this->entityManager->refresh($podcast2); $this->assertCount(2, $podcast2->getEpisodes()); $this->assertEquals(JobStatus::AwaitingBatchProcess, $job->getStatus()); - $this->assertEquals(sprintf('%s|%s', PodcastFixtures::PODCAST_2, '2023-03-07T23:01:44+00:00'), $job->getLastBatchProcessedRecord()); + + $pointerDate = App::getAppDate()->modify(RssPodcastMock::FIRST_RSS_DATE_MODEFIER)->format(DateTimeInterface::ATOM); + $this->assertEquals(sprintf('%s|%s', PodcastFixtures::PODCAST_2, $pointerDate), $job->getLastBatchProcessedRecord()); $this->synchronizerProcessor->process($job); $this->assertEquals(JobStatus::Done, $job->getStatus()); @@ -102,12 +112,11 @@ public function testSpecificPodcastSyncProcessAndImportFrom(): void $job = $this->entityManager->getRepository(JobPodcastSynchronizer::class)->findBy(['fullSync' => false])[0]; $this->assertInstanceOf(JobPodcastSynchronizer::class, $job); - $this->synchronizerProcessor->setBulkSize(2); + $this->synchronizerProcessor + ->setBulkSize(2) + ->setMinImportFrom(App::getAppDate()->modify('-7 weeks')) + ; $podcast1 = $this->podcastRepository->find(PodcastFixtures::PODCAST_1); - $podcast1->getDates()->setImportFrom(\DateTimeImmutable::createFromFormat( - \DateTimeInterface::ATOM, - '2023-03-07T22:01:44+00:00' - )); $this->synchronizerProcessor->process($job); $this->entityManager->refresh($podcast1); diff --git a/tests/HttpClient/RssPodcastMock.php b/tests/HttpClient/RssPodcastMock.php index 0a7c9051..b78059a2 100644 --- a/tests/HttpClient/RssPodcastMock.php +++ b/tests/HttpClient/RssPodcastMock.php @@ -4,12 +4,20 @@ namespace AnzuSystems\CoreDamBundle\Tests\HttpClient; +use AnzuSystems\CoreDamBundle\App; +use AnzuSystems\CoreDamBundle\Domain\Podcast\PodcastRssReader; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\HttpFoundation\Response; final class RssPodcastMock extends AbstractFileMock { + public const string FIRST_RSS_DATE_MODEFIER = '-6 weeks'; + public const string SECOND_RSS_DATE_MODEFIER = '-8 weeks'; + public const string THIRD_RSS_DATE_MODEFIER = '-10 weeks'; + private const string FIRST_PUB_DATE_RSS_PLACEHOLDER = '__FirstPubDatePlaceholder__'; + private const string SECOND_PUB_DATE_RSS_PLACEHOLDER = '__SecondPubDatePlaceholder__'; + private const string THIRD_PUB_DATE_RSS_PLACEHOLDER = '__ThirdPubDatePlaceholder__'; public function __invoke(): MockHttpClient { return new MockHttpClient( @@ -29,16 +37,23 @@ private function getResponse(string $method, string $url, array $options = []): private function getContent(string $url): string { + $fileContent = ''; if ('https://anchor.fm/s/8a651488/podcast/rss' === $url) { - return $this->getTestDataFile( 'firstPodcast.xml'); + $fileContent = $this->getTestDataFile( 'firstPodcast.xml'); } if ('https://anchor.fm/s/4d8e8b48/podcast/rss' === $url) { - return $this->getTestDataFile( 'secondPodcast.xml'); + $fileContent = $this->getTestDataFile( 'secondPodcast.xml'); } if ('https://anchor.fm/s/7758ecd4/podcast/rss' === $url) { - return $this->getTestDataFile( 'thirdPodcast.xml'); + $fileContent = $this->getTestDataFile( 'thirdPodcast.xml'); } + $firstEpisodeDate = App::getAppDate()->modify(self::FIRST_RSS_DATE_MODEFIER)->format(PodcastRssReader::RSS_DATE_FORMAT); + $secondEpisodeDate = App::getAppDate()->modify(self::SECOND_RSS_DATE_MODEFIER)->format(PodcastRssReader::RSS_DATE_FORMAT); + $thirdEpisodeDate = App::getAppDate()->modify(self::THIRD_RSS_DATE_MODEFIER)->format(PodcastRssReader::RSS_DATE_FORMAT); - return ''; + $fileContent = str_replace(self::FIRST_PUB_DATE_RSS_PLACEHOLDER, $firstEpisodeDate, $fileContent); + $fileContent = str_replace(self::SECOND_PUB_DATE_RSS_PLACEHOLDER, $secondEpisodeDate, $fileContent); + + return str_replace(self::THIRD_PUB_DATE_RSS_PLACEHOLDER, $thirdEpisodeDate, $fileContent); } } diff --git a/tests/data/Files/firstPodcast.xml b/tests/data/Files/firstPodcast.xml index c5fa8493..bfed7afb 100644 --- a/tests/data/Files/firstPodcast.xml +++ b/tests/data/Files/firstPodcast.xml @@ -20,7 +20,7 @@ https://www.thisamericanlife.org/789/the-runaround 3: Dobre rano description - Tue, 07 Mar 2023 23:01:44 GMT + __FirstPubDatePlaceholder__ fpg3 SME.sk @@ -36,7 +36,7 @@ https://www.thisamericanlife.org/789/the-runaround 2: Dobre rano description - Mon, 06 Mar 2023 23:01:44 GMT + __SecondPubDatePlaceholder__ fpg2 SME.sk @@ -52,7 +52,7 @@ https://www.thisamericanlife.org/789/the-runaround 3: Dobre rano description - Sun, 05 Mar 2023 23:01:45 GMT + __ThirdPubDatePlaceholder__ fpg1 SME.sk diff --git a/tests/data/Files/secondPodcast.xml b/tests/data/Files/secondPodcast.xml index e45ecf96..49bf502a 100644 --- a/tests/data/Files/secondPodcast.xml +++ b/tests/data/Files/secondPodcast.xml @@ -20,7 +20,7 @@ https://www.thisamericanlife.org/789/the-runaround 3: Klik description - Tue, 07 Mar 2023 23:01:44 GMT + __FirstPubDatePlaceholder__ spg2 SME.sk @@ -36,7 +36,7 @@ https://www.thisamericanlife.org/789/the-runaround 2: Klik description - Mon, 06 Mar 2023 23:01:44 GMT + __SecondPubDatePlaceholder__ spg1 SME.sk diff --git a/tests/data/Files/thirdPodcast.xml b/tests/data/Files/thirdPodcast.xml index cdde4f60..79884508 100644 --- a/tests/data/Files/thirdPodcast.xml +++ b/tests/data/Files/thirdPodcast.xml @@ -20,7 +20,7 @@ https://www.thisamericanlife.org/789/the-runaround 3: Rozprávky SME description - Tue, 07 Mar 2023 23:01:44 GMT + __FirstPubDatePlaceholder__ tpg1 SME.sk