Repository: LycheeOrg/Lychee (language: php)

Issue:
[Enhancement] Allow setting copyright per picture/album.
### Discussed in https://github.com/LycheeOrg/Lychee/discussions/1837

<div type='discussions-op-text'>

<sup>Originally posted by **elschman** May 14, 2023</sup>
Hi,
we (sports club) do have a couple (4,500) of pictures taken from several different photographs. All are in Lychee at our own webspace but we do not own the copyright but we have to state the copyright per picture together with the picture.
Is there any supported way to fulfil this request ?</div>

## TODO

- [ ] : Create field in the base_album table
- [ ] : Add returned filed to AlbumResource
- [ ] : Display field in Front-end
- [ ] : Add editing possibility in Front-end
- [ ] : Add end point for editing copyright in Backend.

- [ ] : Same as above for Photo if per photo integration is considered.


Discussion / hints:

Fix patch:
diff --git a/app/Contracts/Http/Requests/HasCopyright.php b/app/Contracts/Http/Requests/HasCopyright.php
new file mode 100644
index 00000000000..c6eb4d601f8
--- /dev/null
+++ b/app/Contracts/Http/Requests/HasCopyright.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace App\Contracts\Http\Requests;
+
+interface HasCopyright
+{
+	/**
+	 * @return string|null
+	 */
+	public function copyright(): ?string;
+}
diff --git a/app/Contracts/Http/Requests/RequestAttribute.php b/app/Contracts/Http/Requests/RequestAttribute.php
index 9927534abca..9356e3e7c42 100644
--- a/app/Contracts/Http/Requests/RequestAttribute.php
+++ b/app/Contracts/Http/Requests/RequestAttribute.php
@@ -84,5 +84,7 @@ class RequestAttribute
 	public const RESYNC_METADATA_ATTRIBUTE = 'resync_metadata';
 
 	public const FILE_LAST_MODIFIED_TIME = 'fileLastModifiedTime';
+
+	public const COPYRIGHT_ATTRIBUTE = 'copyright';
 	public const URLS_ATTRIBUTE = 'urls';
 }
\ No newline at end of file
diff --git a/app/Http/Controllers/AlbumController.php b/app/Http/Controllers/AlbumController.php
index ee6c625c207..e805e34c299 100644
--- a/app/Http/Controllers/AlbumController.php
+++ b/app/Http/Controllers/AlbumController.php
@@ -25,6 +25,7 @@
 use App\Http\Requests\Album\GetAlbumRequest;
 use App\Http\Requests\Album\MergeAlbumsRequest;
 use App\Http\Requests\Album\MoveAlbumsRequest;
+use App\Http\Requests\Album\SetAlbumCopyrightRequest;
 use App\Http\Requests\Album\SetAlbumCoverRequest;
 use App\Http\Requests\Album\SetAlbumDescriptionRequest;
 use App\Http\Requests\Album\SetAlbumHeaderRequest;
@@ -187,6 +188,21 @@ public function setDescription(SetAlbumDescriptionRequest $request): void
 		$request->album()->save();
 	}
 
+	/**
+	 * Change the copyright of the album.
+	 *
+	 * @param SetAlbumCopyrightRequest $request
+	 *
+	 * @return void
+	 *
+	 * @throws ModelDBException
+	 */
+	public function setCopyright(SetAlbumCopyrightRequest $request): void
+	{
+		$request->album()->copyright = $request->copyright();
+		$request->album()->save();
+	}
+
 	/**
 	 * Change show tags of the tag album.
 	 *
diff --git a/app/Http/Requests/Album/SetAlbumCopyrightRequest.php b/app/Http/Requests/Album/SetAlbumCopyrightRequest.php
new file mode 100644
index 00000000000..04b57c8321d
--- /dev/null
+++ b/app/Http/Requests/Album/SetAlbumCopyrightRequest.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace App\Http\Requests\Album;
+
+use App\Contracts\Http\Requests\HasBaseAlbum;
+use App\Contracts\Http\Requests\HasCopyright;
+use App\Contracts\Http\Requests\RequestAttribute;
+use App\Http\Requests\BaseApiRequest;
+use App\Http\Requests\Traits\Authorize\AuthorizeCanEditAlbumTrait;
+use App\Http\Requests\Traits\HasBaseAlbumTrait;
+use App\Http\Requests\Traits\HasCopyrightTrait;
+use App\Http\RuleSets\Album\SetAlbumCopyrightRuleSet;
+
+class SetAlbumCopyrightRequest extends BaseApiRequest implements HasBaseAlbum, HasCopyright
+{
+	use HasBaseAlbumTrait;
+	use HasCopyrightTrait;
+	use AuthorizeCanEditAlbumTrait;
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function rules(): array
+	{
+		return SetAlbumCopyrightRuleSet::rules();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	protected function processValidatedValues(array $values, array $files): void
+	{
+		$this->album = $this->albumFactory->findBaseAlbumOrFail(
+			$values[RequestAttribute::ALBUM_ID_ATTRIBUTE], false
+		);
+		$this->copyright = $values[RequestAttribute::COPYRIGHT_ATTRIBUTE];
+	}
+}
diff --git a/app/Http/Requests/Traits/HasCopyrightTrait.php b/app/Http/Requests/Traits/HasCopyrightTrait.php
new file mode 100644
index 00000000000..ab903ed3441
--- /dev/null
+++ b/app/Http/Requests/Traits/HasCopyrightTrait.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Http\Requests\Traits;
+
+trait HasCopyrightTrait
+{
+	protected ?string $copyright = null;
+
+	/**
+	 * @return string|null
+	 */
+	public function copyright(): ?string
+	{
+		return $this->copyright;
+	}
+}
diff --git a/app/Http/Resources/Models/AlbumResource.php b/app/Http/Resources/Models/AlbumResource.php
index 23916d5f2dd..7e48657b272 100644
--- a/app/Http/Resources/Models/AlbumResource.php
+++ b/app/Http/Resources/Models/AlbumResource.php
@@ -37,6 +37,7 @@ public function toArray($request)
 			'id' => $this->resource->id,
 			'title' => $this->resource->title,
 			'owner_name' => $this->when(Auth::check(), $this->resource->owner->name),
+			'copyright' => $this->resource->copyright,
 
 			// attributes
 			'description' => $this->resource->description,
diff --git a/app/Http/RuleSets/Album/SetAlbumCopyrightRuleSet.php b/app/Http/RuleSets/Album/SetAlbumCopyrightRuleSet.php
new file mode 100644
index 00000000000..f0f89ea3956
--- /dev/null
+++ b/app/Http/RuleSets/Album/SetAlbumCopyrightRuleSet.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace App\Http\RuleSets\Album;
+
+use App\Contracts\Http\Requests\RequestAttribute;
+use App\Contracts\Http\RuleSet;
+use App\Rules\CopyrightRule;
+use App\Rules\RandomIDRule;
+
+/**
+ * Rules applied when updating the copyright of an album.
+ */
+class SetAlbumCopyrightRuleSet implements RuleSet
+{
+	/**
+	 * {@inheritDoc}
+	 */
+	public static function rules(): array
+	{
+		return [
+			RequestAttribute::ALBUM_ID_ATTRIBUTE => ['required', new RandomIDRule(false)],
+			RequestAttribute::COPYRIGHT_ATTRIBUTE => ['required', new CopyrightRule()],
+		];
+	}
+}
diff --git a/app/Livewire/Components/Forms/Album/Properties.php b/app/Livewire/Components/Forms/Album/Properties.php
index 6918908e633..a45d2c85b2e 100644
--- a/app/Livewire/Components/Forms/Album/Properties.php
+++ b/app/Livewire/Components/Forms/Album/Properties.php
@@ -20,6 +20,7 @@
 use App\Models\Album as ModelsAlbum;
 use App\Models\Extensions\BaseAlbum;
 use App\Policies\AlbumPolicy;
+use App\Rules\CopyrightRule;
 use App\Rules\TitleRule;
 use Illuminate\Contracts\View\View;
 use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
@@ -44,6 +45,7 @@ class Properties extends Component
 	public string $album_sorting_order = ''; // ! wired
 	public string $album_aspect_ratio = ''; // ! wired
 	public string $license = 'none'; // ! wired
+	public string $copyright = ''; // ! wired
 
 	/**
 	 * This is the equivalent of the constructor for Livewire Components.
@@ -63,6 +65,7 @@ public function mount(BaseAlbum $album): void
 		$this->description = $album->description ?? '';
 		$this->photo_sorting_column = $album->photo_sorting?->column->value ?? '';
 		$this->photo_sorting_order = $album->photo_sorting?->order->value ?? '';
+		$this->copyright = $album->copyright ?? '';
 		if ($this->is_model_album) {
 			/** @var ModelsAlbum $album */
 			$this->license = $album->license->value;
@@ -94,6 +97,7 @@ public function submit(AlbumFactory $albumFactory): void
 			...SetPhotoSortingRuleSet::rules(),
 			...SetAlbumSortingRuleSet::rules(),
 			RequestAttribute::ALBUM_ASPECT_RATIO_ATTRIBUTE => ['present', 'nullable', new Enum(AspectRatioType::class)],
+			RequestAttribute::COPYRIGHT_ATTRIBUTE => ['present', 'nullable', new CopyrightRule()],
 		];
 
 		if (!$this->areValid($rules)) {
@@ -106,12 +110,17 @@ public function submit(AlbumFactory $albumFactory): void
 		$baseAlbum->title = $this->title;
 		$baseAlbum->description = $this->description;
 
+		$this->copyright = trim($this->copyright);
+
 		// Not super pretty but whatever.
 		$column = ColumnSortingPhotoType::tryFrom($this->photo_sorting_column);
 		$order = OrderSortingType::tryFrom($this->photo_sorting_order);
 		$photoSortingCriterion = $column === null ? null : new PhotoSortingCriterion($column->toColumnSortingType(), $order);
 		$baseAlbum->photo_sorting = $photoSortingCriterion;
 
+		// If left empty, we set to null
+		$baseAlbum->copyright = $this->copyright === '' ? null : $this->copyright;
+
 		if ($this->is_model_album) {
 			/** @var ModelsAlbum $baseAlbum */
 			$baseAlbum->license = LicenseType::from($this->license);
diff --git a/app/Livewire/DTO/AlbumFormatted.php b/app/Livewire/DTO/AlbumFormatted.php
index 1a86351177c..c8f7152ed5c 100644
--- a/app/Livewire/DTO/AlbumFormatted.php
+++ b/app/Livewire/DTO/AlbumFormatted.php
@@ -21,6 +21,7 @@ class AlbumFormatted
 	public bool $can_download;
 	public ?string $created_at = null;
 	public ?string $description = null;
+	public ?string $copyright = null;
 
 	public function __construct(AbstractAlbum $album, ?string $url)
 	{
@@ -33,6 +34,7 @@ public function __construct(AbstractAlbum $album, ?string $url)
 			$this->max_taken_at = $album->max_taken_at?->format($min_max_date_format);
 			$this->created_at = $album->created_at->format($create_date_format);
 			$this->description = trim($album->description ?? '');
+			$this->copyright = $album->copyright;
 		}
 		if ($album instanceof Album) {
 			$this->num_children = $album->num_children;
diff --git a/app/Models/BaseAlbumImpl.php b/app/Models/BaseAlbumImpl.php
index 85802373dde..3d1c4ac3a7e 100644
--- a/app/Models/BaseAlbumImpl.php
+++ b/app/Models/BaseAlbumImpl.php
@@ -104,6 +104,7 @@
  * @property string|null                      $sorting_order
  * @property Collection<int,AccessPermission> $access_permissions
  * @property int|null                         $access_permissions_count
+ * @property string|null                      $copyright
  *
  * @method static BaseAlbumImplBuilder|BaseAlbumImpl addSelect($column)
  * @method static BaseAlbumImplBuilder|BaseAlbumImpl join(string $table, string $first, string $operator = null, string $second = null, string $type = 'inner', string $where = false)
@@ -173,6 +174,7 @@ class BaseAlbumImpl extends Model implements HasRandomID
 		'owner_id' => 0,
 		'sorting_col' => null,
 		'sorting_order' => null,
+		'copyright' => null,
 		// Special visibility attributes
 		'is_nsfw' => false,
 	];
diff --git a/app/Models/Extensions/BaseAlbum.php b/app/Models/Extensions/BaseAlbum.php
index bda00620f19..3262432dfc1 100644
--- a/app/Models/Extensions/BaseAlbum.php
+++ b/app/Models/Extensions/BaseAlbum.php
@@ -29,6 +29,7 @@
  * @property Carbon                           $updated_at
  * @property string|null                      $description
  * @property bool                             $is_nsfw
+ * @property string|null                      $copyright
  * @property int                              $owner_id
  * @property User                             $owner
  * @property Collection<int,AccessPermission> $access_permissions
diff --git a/app/Rules/CopyrightRule.php b/app/Rules/CopyrightRule.php
new file mode 100644
index 00000000000..0f5891e488a
--- /dev/null
+++ b/app/Rules/CopyrightRule.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace App\Rules;
+
+class CopyrightRule extends StringRule
+{
+	public function __construct()
+	{
+		parent::__construct(true, 300);
+	}
+}
diff --git a/database/migrations/2024_04_28_172241_add_album_copyright.php b/database/migrations/2024_04_28_172241_add_album_copyright.php
new file mode 100644
index 00000000000..ab283e0362b
--- /dev/null
+++ b/database/migrations/2024_04_28_172241_add_album_copyright.php
@@ -0,0 +1,30 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class() extends Migration {
+	public const TABLE = 'base_albums';
+	public const COLUMN = 'copyright';
+
+	/**
+	 * Run the migrations.
+	 */
+	public function up(): void
+	{
+		Schema::table(self::TABLE, function (Blueprint $table) {
+			$table->string(self::COLUMN, 300)->nullable()->default(null);
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 */
+	public function down(): void
+	{
+		Schema::table(self::TABLE, function (Blueprint $table) {
+			$table->dropColumn(self::COLUMN);
+		});
+	}
+};
diff --git a/lang/cz/lychee.php b/lang/cz/lychee.php
index d4fa2eac1fd..acdf84e82c7 100644
--- a/lang/cz/lychee.php
+++ b/lang/cz/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => 'O albu',
 	'ALBUM_BASICS' => 'Základní informace',
 	'ALBUM_TITLE' => 'Název',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => 'Zadat nový název alba:',
 	'ALBUMS_NEW_TITLE' => 'Zadat nový název pro %d vybraná alba:',
 	'ALBUM_SET_TITLE' => 'Uložit název',
diff --git a/lang/de/lychee.php b/lang/de/lychee.php
index 9ffa6a2596b..8f6789029da 100644
--- a/lang/de/lychee.php
+++ b/lang/de/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => 'Über',
 	'ALBUM_BASICS' => 'Grundlegende Informationen',
 	'ALBUM_TITLE' => 'Titel',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => 'Geben Sie einen neuen Titel für dieses Album ein:',
 	'ALBUMS_NEW_TITLE' => 'Geben Sie einen Titel für alle %d ausgewählten Alben ein:',
 	'ALBUM_SET_TITLE' => 'Titel speichern',
diff --git a/lang/el/lychee.php b/lang/el/lychee.php
index 0935f1b9642..45fa4119c45 100644
--- a/lang/el/lychee.php
+++ b/lang/el/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => 'Περί',
 	'ALBUM_BASICS' => 'Βασικές Πληροφορίες',
 	'ALBUM_TITLE' => 'Τίτλος',
+	'ALBUM_COPYRIGHT' => 'Πνευματική ιδιοκτησία',
+	'ALBUM_SET_COPYRIGHT' => 'Δήλωσε πνευματική ιδιοκτησία',
 	'ALBUM_NEW_TITLE' => 'Εισάγετε έναν νέο τίτλο για αυτό το Λεύκωμα:',
 	'ALBUMS_NEW_TITLE' => 'Εισάγετε νέο τίτλο για όλα %d τα επιλεγμένα λευκώματα:',
 	'ALBUM_SET_TITLE' => 'Ορίστε Τίτλο',
diff --git a/lang/en/lychee.php b/lang/en/lychee.php
index 352e73fd08c..5b4c6c31162 100644
--- a/lang/en/lychee.php
+++ b/lang/en/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => 'About',
 	'ALBUM_BASICS' => 'Basics',
 	'ALBUM_TITLE' => 'Title',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => 'Enter a new title for this album:',
 	'ALBUMS_NEW_TITLE' => 'Enter a title for all %d selected albums:',
 	'ALBUM_SET_TITLE' => 'Set Title',
diff --git a/lang/es/lychee.php b/lang/es/lychee.php
index 0139fd679e0..fedf733f2f7 100644
--- a/lang/es/lychee.php
+++ b/lang/es/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => 'Acerca de',
 	'ALBUM_BASICS' => 'Básico',
 	'ALBUM_TITLE' => 'Título',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => 'Ingrese un nuevo título para este álbum:',
 	'ALBUMS_NEW_TITLE' => 'Ingrese un título para todos %d álbumes seleccionados:',
 	'ALBUM_SET_TITLE' => 'Establecer Título',
diff --git a/lang/fr/lychee.php b/lang/fr/lychee.php
index d2adaf32e61..daf3497076b 100644
--- a/lang/fr/lychee.php
+++ b/lang/fr/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => 'À propos',
 	'ALBUM_BASICS' => 'Informations de base',
 	'ALBUM_TITLE' => 'Titre',
+	'ALBUM_COPYRIGHT' => 'Droits d’auteur',
+	'ALBUM_SET_COPYRIGHT' => 'Définir les droits d’auteur',
 	'ALBUM_NEW_TITLE' => 'Entrez un nouveau titre pour cet album :',
 	'ALBUMS_NEW_TITLE' => 'Entrez un titre pour les %d albums sélectionnés :',
 	'ALBUM_SET_TITLE' => 'Enregistrer le titre',
diff --git a/lang/hu/lychee.php b/lang/hu/lychee.php
index 961ba7256d1..4cac3932cc5 100644
--- a/lang/hu/lychee.php
+++ b/lang/hu/lychee.php
@@ -155,6 +155,8 @@
 	'ALBUM_ABOUT' => 'Névjegy',
 	'ALBUM_BASICS' => 'Alapok',
 	'ALBUM_TITLE' => 'Cím',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => 'Adjon meg egy új címet ennek az albumnak:',
 	'ALBUMS_NEW_TITLE' => 'Adjon meg címet az összes %d kiválasztott albumnak:',
 	'ALBUM_SET_TITLE' => 'Cím beállítása',
diff --git a/lang/it/lychee.php b/lang/it/lychee.php
index ce346ce0671..d120b90e235 100644
--- a/lang/it/lychee.php
+++ b/lang/it/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => 'Informazioni',
 	'ALBUM_BASICS' => 'Base',
 	'ALBUM_TITLE' => 'Titolo',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => 'Inserire un nuovo titolo per questo album:',
 	'ALBUMS_NEW_TITLE' => 'Inserire un nuovo titolo per %d gli album selezionati:',
 	'ALBUM_SET_TITLE' => 'Imposta Titolo',
diff --git a/lang/nl/lychee.php b/lang/nl/lychee.php
index c8b3dd3c91d..769fc6dc79c 100644
--- a/lang/nl/lychee.php
+++ b/lang/nl/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => 'Over',
 	'ALBUM_BASICS' => 'Basics',
 	'ALBUM_TITLE' => 'Titel',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => 'Geef dit album een nieuwe titel:',
 	'ALBUMS_NEW_TITLE' => 'Geef alle geselecteerde %d albums een nieuwe titel:',
 	'ALBUM_SET_TITLE' => 'Sla Titel op',
diff --git a/lang/no/lychee.php b/lang/no/lychee.php
index a340be65f42..8274e62af9d 100644
--- a/lang/no/lychee.php
+++ b/lang/no/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => 'Om',
 	'ALBUM_BASICS' => 'Grunnleggende',
 	'ALBUM_TITLE' => 'Tittel',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => 'Legg inn en ny tittel for Albumet:',
 	'ALBUMS_NEW_TITLE' => 'Legg inn en ny tittel for %d valgte album:',
 	'ALBUM_SET_TITLE' => 'Lagre Tittel',
diff --git a/lang/pl/lychee.php b/lang/pl/lychee.php
index 4cef95d131f..aa3130e9679 100644
--- a/lang/pl/lychee.php
+++ b/lang/pl/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => 'Informacje o albumie',
 	'ALBUM_BASICS' => 'Informacje podstawowe',
 	'ALBUM_TITLE' => 'Tytuł',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => 'Edytuj tytuł albumu:',
 	'ALBUMS_NEW_TITLE' => 'Wpisz tytuł dla %d wybranych albumów:',
 	'ALBUM_SET_TITLE' => 'Zapisz',
diff --git a/lang/pt/lychee.php b/lang/pt/lychee.php
index 305a5733b74..a8523b837e7 100644
--- a/lang/pt/lychee.php
+++ b/lang/pt/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => 'Acerca de',
 	'ALBUM_BASICS' => 'Básicos',
 	'ALBUM_TITLE' => 'Título',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => 'Inserir novo título para este álbum:',
 	'ALBUMS_NEW_TITLE' => 'Inserir um título para todos %d álbums selecionados:',
 	'ALBUM_SET_TITLE' => 'Escolher Título',
diff --git a/lang/ru/lychee.php b/lang/ru/lychee.php
index c8fc8baf6fa..ad41d3876d1 100644
--- a/lang/ru/lychee.php
+++ b/lang/ru/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => 'Об альбоме',
 	'ALBUM_BASICS' => 'Основное',
 	'ALBUM_TITLE' => 'Заголовок',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => 'Новый заголовок альбома:',
 	'ALBUMS_NEW_TITLE' => 'Введите заголовок для всех %d выбранных альбомов:',
 	'ALBUM_SET_TITLE' => 'Сохранить заголовок',
diff --git a/lang/sk/lychee.php b/lang/sk/lychee.php
index 6e4884d426a..ecc55c4b83e 100644
--- a/lang/sk/lychee.php
+++ b/lang/sk/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => 'O albume',
 	'ALBUM_BASICS' => 'Základné informácie',
 	'ALBUM_TITLE' => 'Názov',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => 'Zadajte nový názov pre tento album:',
 	'ALBUMS_NEW_TITLE' => 'Zadajte názov pre všetky %d vybrané albumy:',
 	'ALBUM_SET_TITLE' => 'Názov uložiť',
diff --git a/lang/sv/lychee.php b/lang/sv/lychee.php
index d588d6f4399..1186d57dda1 100644
--- a/lang/sv/lychee.php
+++ b/lang/sv/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => 'Om',
 	'ALBUM_BASICS' => 'Grundläggande',
 	'ALBUM_TITLE' => 'Titel',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => 'Skriv en ny titel för det här albumet:',
 	'ALBUMS_NEW_TITLE' => 'Skriv en ny titel för alla %d valda album:',
 	'ALBUM_SET_TITLE' => 'Spara titel',
diff --git a/lang/vi/lychee.php b/lang/vi/lychee.php
index 35e6b34f3bd..49c2b3c7063 100644
--- a/lang/vi/lychee.php
+++ b/lang/vi/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => 'Giới thiệu',
 	'ALBUM_BASICS' => 'Những phần cơ bản',
 	'ALBUM_TITLE' => 'Tên album',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => 'Đặt tên mới cho album này:',
 	'ALBUMS_NEW_TITLE' => 'Đặt tên cho tất cả %d những album được chọn:',
 	'ALBUM_SET_TITLE' => 'Đặt tên',
diff --git a/lang/zh_CN/lychee.php b/lang/zh_CN/lychee.php
index 6066aa7af2f..cd9e8395b5b 100644
--- a/lang/zh_CN/lychee.php
+++ b/lang/zh_CN/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => '关于',
 	'ALBUM_BASICS' => '基本信息',
 	'ALBUM_TITLE' => '标题',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => '输入新的相册标题：',
 	'ALBUMS_NEW_TITLE' => '设置标题为 %d 已选择的相册：',
 	'ALBUM_SET_TITLE' => '设置标题',
diff --git a/lang/zh_TW/lychee.php b/lang/zh_TW/lychee.php
index 9554bf829bb..366fe503449 100644
--- a/lang/zh_TW/lychee.php
+++ b/lang/zh_TW/lychee.php
@@ -157,6 +157,8 @@
 	'ALBUM_ABOUT' => '關於',
 	'ALBUM_BASICS' => '基本資訊',
 	'ALBUM_TITLE' => '標題',
+	'ALBUM_COPYRIGHT' => 'Copyright',
+	'ALBUM_SET_COPYRIGHT' => 'Set copyright',
 	'ALBUM_NEW_TITLE' => '輸入新的相簿標題：',
 	'ALBUMS_NEW_TITLE' => '設定標題為 %d 已選擇的所有相簿：',
 	'ALBUM_SET_TITLE' => '設定標題',
diff --git a/resources/js/lycheeOrg/backend.d.ts b/resources/js/lycheeOrg/backend.d.ts
index 8dcafadec16..6b6e734feea 100644
--- a/resources/js/lycheeOrg/backend.d.ts
+++ b/resources/js/lycheeOrg/backend.d.ts
@@ -131,6 +131,7 @@ export type Album = {
 	cover_id: string | null;
 	thumb: Thumb | null;
 	owner_name?: string;
+	copyright: string | null;
 	is_nsfw: boolean;
 	rights: AlbumRightsDTO;
 	policy: AlbumProtectionPolicy;
diff --git a/resources/views/components/gallery/album/details.blade.php b/resources/views/components/gallery/album/details.blade.php
index d0ac54dcbd9..8b556986643 100644
--- a/resources/views/components/gallery/album/details.blade.php
+++ b/resources/views/components/gallery/album/details.blade.php
@@ -8,6 +8,11 @@
             {{ __('lychee.ALBUM_CREATED') }} {{ $this->AlbumFormatted->created_at }}
         </span>
         @endif
+        @if($this->AlbumFormatted->copyright !== null && $this->AlbumFormatted->copyright !== '')
+        <span class="block text-text-main-200 text-sm">
+            {{ __('lychee.ALBUM_COPYRIGHT') }} {{ $this->AlbumFormatted->copyright }}
+        </span>
+        @endif
         @if($this->num_albums > 0)
             <span class="block text-text-main-200 text-sm">
                 {{ $this->num_albums }} {{ __('lychee.ALBUM_SUBALBUMS') }}
diff --git a/resources/views/livewire/forms/album/properties.blade.php b/resources/views/livewire/forms/album/properties.blade.php
index d5ea5e21918..a94c1a12c3a 100644
--- a/resources/views/livewire/forms/album/properties.blade.php
+++ b/resources/views/livewire/forms/album/properties.blade.php
@@ -26,6 +26,11 @@
             <span class="font-bold">{{ __('lychee.ALBUM_SET_LICENSE') }}</span>
             <x-forms.dropdown class="mx-2" :options="$this->licenses" id="license_dialog_select" wire:model='license'/>
         </div>
+        <div class="mb-4 flex">
+            <span class="inline-block font-bold shrink-0 pr-2">{{ __('lychee.ALBUM_SET_COPYRIGHT') }}:</span>
+            <x-forms.inputs.text class="-mt-1 w-full" wire:model='copyright' id="copyright" />
+            <x-forms.error-message field='copyright' />
+        </div>
         <div class="h-10">
             <span class="font-bold">Set album thumbs aspect ratio</span>
             <x-forms.dropdown class="mx-2" :options="$this->aspectRatios" id="aspect_ratio_dialog_select" wire:model='album_aspect_ratio'/>
diff --git a/routes/api.php b/routes/api.php
index e4649228ac3..47da326ac19 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -34,6 +34,7 @@
 Route::post('/Album::setTitle', [AlbumController::class, 'setTitle']);
 Route::post('/Album::setNSFW', [AlbumController::class, 'setNSFW']);
 Route::post('/Album::setDescription', [AlbumController::class, 'setDescription']);
+Route::post('/Album::setCopyright', [AlbumController::class, 'setCopyright']);
 Route::post('/Album::setCover', [AlbumController::class, 'setCover']);
 Route::post('/Album::setHeader', [AlbumController::class, 'setHeader']);
 Route::post('/Album::setShowTags', [AlbumController::class, 'setShowTags']);


Test patch:
diff --git a/tests/Feature/AlbumTest.php b/tests/Feature/AlbumTest.php
index f1330324874..c8d1a553e0e 100644
--- a/tests/Feature/AlbumTest.php
+++ b/tests/Feature/AlbumTest.php
@@ -625,6 +625,18 @@ public function testEditAlbumByOwner(): void
 		$this->albums_tests->get($albumID);
 	}
 
+	public function testEditAlbumCopyright(): void
+	{
+		Auth::loginUsingId(1);
+		$this->users_tests->add('Test user', 'Test password 1')->offsetGet('id');
+
+		$albumID = $this->albums_tests->add(null, 'Test Album')->offsetGet('id');
+		$this->albums_tests->set_copyright($albumID, 'Test copyright value');
+
+		Auth::logout();
+		Session::flush();
+	}
+
 	public function testDeleteMultipleAlbumsByAnonUser(): void
 	{
 		Auth::loginUsingId(1);
diff --git a/tests/Feature/LibUnitTests/AlbumsUnitTest.php b/tests/Feature/LibUnitTests/AlbumsUnitTest.php
index 2e6e74c5b8f..723c59d0660 100644
--- a/tests/Feature/LibUnitTests/AlbumsUnitTest.php
+++ b/tests/Feature/LibUnitTests/AlbumsUnitTest.php
@@ -209,6 +209,31 @@ public function set_title(
 		}
 	}
 
+	/**
+	 * Change copyright.
+	 *
+	 * @param string      $id
+	 * @param string      $copyright
+	 * @param int         $expectedStatusCode
+	 * @param string|null $assertSee
+	 */
+	public function set_copyright(
+		string $id,
+		string $copyright,
+		int $expectedStatusCode = 204,
+		?string $assertSee = null
+	): void {
+		$response = $this->testCase->postJson(
+			'/api/Album::setCopyright',
+			['albumID' => $id, 'copyright' => $copyright]
+		);
+
+		$this->assertStatus($response, $expectedStatusCode);
+		if ($assertSee !== null) {
+			$response->assertSee($assertSee, false);
+		}
+	}
+
 	/**
 	 * Change description.
 	 *
diff --git a/tests/Livewire/Forms/Album/PropertiesTest.php b/tests/Livewire/Forms/Album/PropertiesTest.php
index 9bfb7c3ae08..946a2fe0aef 100644
--- a/tests/Livewire/Forms/Album/PropertiesTest.php
+++ b/tests/Livewire/Forms/Album/PropertiesTest.php
@@ -13,6 +13,7 @@
 namespace Tests\Livewire\Forms\Album;
 
 use App\Livewire\Components\Forms\Album\Properties;
+use App\Models\Album;
 use Livewire\Livewire;
 use Tests\Livewire\Base\BaseLivewireTest;
 
@@ -40,4 +41,31 @@ public function testPropertiesLoggedIn(): void
 			->assertOk()
 			->assertNotDispatched('notify', self::notifySuccess());
 	}
+
+	public function testSetCopyright(): void
+	{
+		Livewire::actingAs($this->admin)->test(Properties::class, ['album' => $this->album1])
+			->assertOk()
+			->assertViewIs('livewire.forms.album.properties')
+			->set('copyright', 'something')
+			->call('submit')
+			->assertOk()
+			->assertDispatched('notify', self::notifySuccess());
+
+		/** @var Album $album */
+		$album = Album::findOrFail($this->album1->id);
+		$this->assertEquals('something', $album->copyright);
+
+		Livewire::actingAs($this->admin)->test(Properties::class, ['album' => $this->album1])
+			->assertOk()
+			->assertViewIs('livewire.forms.album.properties')
+			->set('copyright', '')
+			->call('submit')
+			->assertOk()
+			->assertDispatched('notify', self::notifySuccess());
+
+		/** @var Album $album */
+		$album = Album::findOrFail($this->album1->id);
+		$this->assertEquals(null, $album->copyright);
+	}
 }