From 1052f6fde0762a9b3dd432ea0d5c4423a0fbe17c Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Thu, 28 Sep 2023 21:19:02 -0400 Subject: [PATCH] Add DDD classes for post media entities --- src/Content/Post/Collection/PostMedias.php | 45 +++++ src/Content/Post/Entity/PostMedia.php | 220 +++++++++++++++++++++ src/Content/Post/Factory/PostMedia.php | 117 +++++++++++ src/Content/Post/Repository/PostMedia.php | 204 +++++++++++++++++++ 4 files changed, 586 insertions(+) create mode 100644 src/Content/Post/Collection/PostMedias.php create mode 100644 src/Content/Post/Entity/PostMedia.php create mode 100644 src/Content/Post/Factory/PostMedia.php create mode 100644 src/Content/Post/Repository/PostMedia.php diff --git a/src/Content/Post/Collection/PostMedias.php b/src/Content/Post/Collection/PostMedias.php new file mode 100644 index 0000000000..5e75d908a7 --- /dev/null +++ b/src/Content/Post/Collection/PostMedias.php @@ -0,0 +1,45 @@ +. + * + */ + +namespace Friendica\Content\Post\Collection; + +use Friendica\BaseCollection; +use Friendica\Content\Post\Entity; + +class PostMedias extends BaseCollection +{ + /** + * @param Entity\PostMedia[] $entities + * @param int|null $totalCount + */ + public function __construct(array $entities = [], int $totalCount = null) + { + parent::__construct($entities, $totalCount); + } + + /** + * @return Entity\PostMedia + */ + public function current(): Entity\PostMedia + { + return parent::current(); + } +} diff --git a/src/Content/Post/Entity/PostMedia.php b/src/Content/Post/Entity/PostMedia.php new file mode 100644 index 0000000000..ca064d4c7e --- /dev/null +++ b/src/Content/Post/Entity/PostMedia.php @@ -0,0 +1,220 @@ +. + * + */ + +namespace Friendica\Content\Post\Entity; + +use Friendica\BaseEntity; +use Friendica\Network\Entity\MimeType; +use Friendica\Util\Proxy; +use Psr\Http\Message\UriInterface; + + +/** + * @property-read int $id + * @property-read int $uriId + * @property-read ?int $activityUriId + * @property-read UriInterface $url + * @property-read int $type + * @property-read MimeType $mimetype + * @property-read ?int $width + * @property-read ?int $height + * @property-read ?int $size + * @property-read ?UriInterface $preview + * @property-read ?int $previewWidth + * @property-read ?int $previewHeight + * @property-read ?string $description + * @property-read ?string $name + * @property-read ?UriInterface $authorUrl + * @property-read ?string $authorName + * @property-read ?UriInterface $authorImage + * @property-read ?UriInterface $publisherUrl + * @property-read ?string $publisherName + * @property-read ?UriInterface $publisherImage + * @property-read ?string $blurhash + */ +class PostMedia extends BaseEntity +{ + const TYPE_UNKNOWN = 0; + const TYPE_IMAGE = 1; + const TYPE_VIDEO = 2; + const TYPE_AUDIO = 3; + const TYPE_TEXT = 4; + const TYPE_APPLICATION = 5; + const TYPE_TORRENT = 16; + const TYPE_HTML = 17; + const TYPE_XML = 18; + const TYPE_PLAIN = 19; + const TYPE_ACTIVITY = 20; + const TYPE_ACCOUNT = 21; + const TYPE_DOCUMENT = 128; + + /** @var int */ + protected $id; + /** @var int */ + protected $uriId; + /** @var UriInterface */ + protected $url; + /** @var int One of TYPE_* */ + protected $type; + /** @var MimeType */ + protected $mimetype; + /** @var ?int */ + protected $activityUriId; + /** @var ?int In pixels */ + protected $width; + /** @var ?int In pixels */ + protected $height; + /** @var ?int In bytes */ + protected $size; + /** @var ?UriInterface Preview URL */ + protected $preview; + /** @var ?int In pixels */ + protected $previewWidth; + /** @var ?int In pixels */ + protected $previewHeight; + /** @var ?string Alternative text like for images */ + protected $description; + /** @var ?string */ + protected $name; + /** @var ?UriInterface */ + protected $authorUrl; + /** @var ?string */ + protected $authorName; + /** @var ?UriInterface Image URL */ + protected $authorImage; + /** @var ?UriInterface */ + protected $publisherUrl; + /** @var ?string */ + protected $publisherName; + /** @var ?UriInterface Image URL */ + protected $publisherImage; + /** @var ?string Blurhash string representation for images + * @see https://github.com/woltapp/blurhash + * @see https://blurha.sh/ + */ + protected $blurhash; + + public function __construct( + int $uriId, + UriInterface $url, + int $type, + MimeType $mimetype, + ?int $activityUriId, + ?int $width = null, + ?int $height = null, + ?int $size = null, + ?UriInterface $preview = null, + ?int $previewWidth = null, + ?int $previewHeight = null, + ?string $description = null, + ?string $name = null, + ?UriInterface $authorUrl = null, + ?string $authorName = null, + ?UriInterface $authorImage = null, + ?UriInterface $publisherUrl = null, + ?string $publisherName = null, + ?UriInterface $publisherImage = null, + ?string $blurhash = null, + int $id = null + ) + { + $this->uriId = $uriId; + $this->url = $url; + $this->type = $type; + $this->mimetype = $mimetype; + $this->activityUriId = $activityUriId; + $this->width = $width; + $this->height = $height; + $this->size = $size; + $this->preview = $preview; + $this->previewWidth = $previewWidth; + $this->previewHeight = $previewHeight; + $this->description = $description; + $this->name = $name; + $this->authorUrl = $authorUrl; + $this->authorName = $authorName; + $this->authorImage = $authorImage; + $this->publisherUrl = $publisherUrl; + $this->publisherName = $publisherName; + $this->publisherImage = $publisherImage; + $this->blurhash = $blurhash; + $this->id = $id; + } + + + /** + * Get media link for given media id + * + * @param string $size One of the Proxy::SIZE_* constants + * @return string media link + */ + public function getPhotoPath(string $size = ''): string + { + $url = '/photo/media/'; + switch ($size) { + case Proxy::SIZE_MICRO: + $url .= Proxy::PIXEL_MICRO . '/'; + break; + case Proxy::SIZE_THUMB: + $url .= Proxy::PIXEL_THUMB . '/'; + break; + case Proxy::SIZE_SMALL: + $url .= Proxy::PIXEL_SMALL . '/'; + break; + case Proxy::SIZE_MEDIUM: + $url .= Proxy::PIXEL_MEDIUM . '/'; + break; + case Proxy::SIZE_LARGE: + $url .= Proxy::PIXEL_LARGE . '/'; + break; + } + return $url . $this->id; + } + + /** + * Get preview path for given media id relative to the base URL + * + * @param string $size One of the Proxy::SIZE_* constants + * @return string preview link + */ + public function getPreviewPath(string $size = ''): string + { + $url = '/photo/preview/'; + switch ($size) { + case Proxy::SIZE_MICRO: + $url .= Proxy::PIXEL_MICRO . '/'; + break; + case Proxy::SIZE_THUMB: + $url .= Proxy::PIXEL_THUMB . '/'; + break; + case Proxy::SIZE_SMALL: + $url .= Proxy::PIXEL_SMALL . '/'; + break; + case Proxy::SIZE_MEDIUM: + $url .= Proxy::PIXEL_MEDIUM . '/'; + break; + case Proxy::SIZE_LARGE: + $url .= Proxy::PIXEL_LARGE . '/'; + break; + } + return $url . $this->id; + } +} diff --git a/src/Content/Post/Factory/PostMedia.php b/src/Content/Post/Factory/PostMedia.php new file mode 100644 index 0000000000..fe71b21973 --- /dev/null +++ b/src/Content/Post/Factory/PostMedia.php @@ -0,0 +1,117 @@ +. + * + */ + +namespace Friendica\Content\Post\Factory; + +use Friendica\BaseFactory; +use Friendica\Capabilities\ICanCreateFromTableRow; +use Friendica\Content\Post\Entity; +use Friendica\Network; +use GuzzleHttp\Psr7\Uri; +use Psr\Log\LoggerInterface; +use stdClass; + +class PostMedia extends BaseFactory implements ICanCreateFromTableRow +{ + /** @var Network\Factory\MimeType */ + private $mimeTypeFactory; + + public function __construct(Network\Factory\MimeType $mimeTypeFactory, LoggerInterface $logger) + { + parent::__construct($logger); + + $this->mimeTypeFactory = $mimeTypeFactory; + } + + /** + * @inheritDoc + */ + public function createFromTableRow(array $row) + { + return new Entity\PostMedia( + $row['uri-id'], + $row['url'] ? new Uri($row['url']) : null, + $row['type'], + $this->mimeTypeFactory->createFromContentType($row['mimetype']), + $row['media-uri-id'], + $row['width'], + $row['height'], + $row['size'], + $row['preview'] ? new Uri($row['preview']) : null, + $row['preview-width'], + $row['preview-height'], + $row['description'], + $row['name'], + $row['author-url'] ? new Uri($row['author-url']) : null, + $row['author-name'], + $row['author-image'] ? new Uri($row['author-image']) : null, + $row['publisher-url'] ? new Uri($row['publisher-url']) : null, + $row['publisher-name'], + $row['publisher-image'] ? new Uri($row['publisher-image']) : null, + $row['blurhash'], + $row['id'] + ); + } + + public function createFromBlueskyImageEmbed(int $uriId, stdClass $image): Entity\PostMedia + { + return new Entity\PostMedia( + $uriId, + new Uri($image->fullsize), + Entity\PostMedia::TYPE_IMAGE, + new Network\Entity\MimeType('unkn', 'unkn'), + null, + null, + null, + null, + new Uri($image->thumb), + null, + null, + $image->alt, + ); + } + + + public function createFromBlueskyExternalEmbed(int $uriId, stdClass $external): Entity\PostMedia + { + return new Entity\PostMedia( + $uriId, + new Uri($external->uri), + Entity\PostMedia::TYPE_HTML, + new Network\Entity\MimeType('text', 'html'), + null, + null, + null, + null, + null, + null, + null, + $external->description, + $external->title + ); + } + + public function createFromAttachment(int $uriId, array $attachment) + { + $attachment['uri-id'] = $uriId; + return $this->createFromTableRow($attachment); + } +} diff --git a/src/Content/Post/Repository/PostMedia.php b/src/Content/Post/Repository/PostMedia.php new file mode 100644 index 0000000000..405d9eb86b --- /dev/null +++ b/src/Content/Post/Repository/PostMedia.php @@ -0,0 +1,204 @@ +. + * + */ + +namespace Friendica\Content\Post\Repository; + +use Friendica\BaseCollection; +use Friendica\BaseRepository; +use Friendica\Content\Post\Collection; +use Friendica\Content\Post\Entity; +use Friendica\Content\Post\Factory; +use Friendica\Database\Database; +use Friendica\Util\Strings; +use Psr\Log\LoggerInterface; + +class PostMedia extends BaseRepository +{ + protected static $table_name = 'post-media'; + + public function __construct(Database $database, LoggerInterface $logger, Factory\PostMedia $factory) + { + parent::__construct($database, $logger, $factory); + } + + protected function _select(array $condition, array $params = []): BaseCollection + { + $rows = $this->db->selectToArray(static::$table_name, [], $condition, $params); + + $Entities = new Collection\PostMedias(); + foreach ($rows as $fields) { + $Entities[] = $this->factory->createFromTableRow($fields); + } + + return $Entities; + } + + public function selectOneById(int $postMediaId): Entity\PostMedia + { + return $this->_selectOne(['id' => $postMediaId]); + } + + public function selectByUriId(int $uriId): Collection\PostMedias + { + return $this->_select(['uri-id' => $uriId]); + } + + public function save(Entity\PostMedia $PostMedia): Entity\PostMedia + { + $fields = [ + 'uri-id' => $PostMedia->uriId, + 'url' => $PostMedia->url->__toString(), + 'type' => $PostMedia->type, + 'mimetype' => $PostMedia->mimetype->__toString(), + 'height' => $PostMedia->height, + 'width' => $PostMedia->width, + 'size' => $PostMedia->size, + 'preview' => $PostMedia->preview ? $PostMedia->preview->__toString() : null, + 'preview-height' => $PostMedia->previewHeight, + 'preview-width' => $PostMedia->previewWidth, + 'description' => $PostMedia->description, + 'name' => $PostMedia->name, + 'author-url' => $PostMedia->authorUrl ? $PostMedia->authorUrl->__toString() : null, + 'author-name' => $PostMedia->authorName, + 'author-image' => $PostMedia->authorImage ? $PostMedia->authorImage->__toString() : null, + 'publisher-url' => $PostMedia->publisherUrl ? $PostMedia->publisherUrl->__toString() : null, + 'publisher-name' => $PostMedia->publisherName, + 'publisher-image' => $PostMedia->publisherImage ? $PostMedia->publisherImage->__toString() : null, + 'media-uri-id' => $PostMedia->activityUriId, + 'blurhash' => $PostMedia->blurhash, + ]; + + if ($PostMedia->id) { + $this->db->update(self::$table_name, $fields, ['id' => $PostMedia->id]); + } else { + $this->db->insert(self::$table_name, $fields, Database::INSERT_IGNORE); + + $newPostMediaId = $this->db->lastInsertId(); + + $PostMedia = $this->selectOneById($newPostMediaId); + } + + return $PostMedia; + } + + + /** + * Split the attachment media in the three segments "visual", "link" and "additional" + * + * @param int $uri_id URI id + * @param array $links list of links that shouldn't be added + * @param bool $has_media + * @return Collection\PostMedias[] Three collections in "visual", "link" and "additional" keys + */ + public function splitAttachments(int $uri_id, array $links = [], bool $has_media = true): array + { + $attachments = [ + 'visual' => new Collection\PostMedias(), + 'link' => new Collection\PostMedias(), + 'additional' => new Collection\PostMedias(), + ]; + + if (!$has_media) { + return $attachments; + } + + $PostMedias = $this->selectByUriId($uri_id); + if (!count($PostMedias)) { + return $attachments; + } + + $heights = []; + $selected = ''; + $previews = []; + + foreach ($PostMedias as $PostMedia) { + foreach ($links as $link) { + if (Strings::compareLink($link, $PostMedia->url)) { + continue 2; + } + } + + // Avoid adding separate media entries for previews + foreach ($previews as $preview) { + if (Strings::compareLink($preview, $PostMedia->url)) { + continue 2; + } + } + + // Currently these two types are ignored here. + // Posts are added differently and contacts are not displayed as attachments. + if (in_array($PostMedia->type, [Entity\PostMedia::TYPE_ACCOUNT, Entity\PostMedia::TYPE_ACTIVITY])) { + continue; + } + + if (!empty($PostMedia->preview)) { + $previews[] = $PostMedia->preview; + } + + //$PostMedia->filetype = $filetype; + //$PostMedia->subtype = $subtype; + + if ($PostMedia->type == Entity\PostMedia::TYPE_HTML || ($PostMedia->mimetype->type == 'text' && $PostMedia->mimetype->subtype == 'html')) { + $attachments['link'][] = $PostMedia; + continue; + } + + if ( + in_array($PostMedia->type, [Entity\PostMedia::TYPE_AUDIO, Entity\PostMedia::TYPE_IMAGE]) || + in_array($PostMedia->mimetype->type, ['audio', 'image']) + ) { + $attachments['visual'][] = $PostMedia; + } elseif (($PostMedia->type == Entity\PostMedia::TYPE_VIDEO) || ($PostMedia->mimetype->type == 'video')) { + if (!empty($PostMedia->height)) { + // Peertube videos are delivered in many different resolutions. We pick a moderate one. + // Since only Peertube provides a "height" parameter, this wouldn't be executed + // when someone for example on Mastodon was sharing multiple videos in a single post. + $heights[$PostMedia->height] = (string)$PostMedia->url; + $video[(string) $PostMedia->url] = $PostMedia; + } else { + $attachments['visual'][] = $PostMedia; + } + } else { + $attachments['additional'][] = $PostMedia; + } + } + + if (!empty($heights)) { + ksort($heights); + foreach ($heights as $height => $url) { + if (empty($selected) || $height <= 480) { + $selected = $url; + } + } + + if (!empty($selected)) { + $attachments['visual'][] = $video[$selected]; + unset($video[$selected]); + foreach ($video as $element) { + $attachments['additional'][] = $element; + } + } + } + + return $attachments; + } + +}