'float', 'mtime' => 'int', 'track' => 'int', 'disc' => 'int', 'is_public' => 'bool', 'type' => MediaType::class, 'episode_metadata' => EpisodeMetadataCast::class, ]; protected $with = ['album', 'artist', 'podcast']; protected $keyType = 'string'; protected static function booted(): void { static::creating(static function (self $song): void { $song->type ??= MediaType::SONG; $song->id ??= Str::uuid()->toString(); }); } public static function query(): SongBuilder { return parent::query(); } public function owner(): BelongsTo { return $this->belongsTo(User::class); } public function newEloquentBuilder($query): SongBuilder { return new SongBuilder($query); } public function artist(): BelongsTo { return $this->belongsTo(Artist::class); } public function album(): BelongsTo { return $this->belongsTo(Album::class); } protected function albumArtist(): Attribute { return Attribute::get(fn () => $this->album?->artist); } public function podcast(): BelongsTo { return $this->belongsTo(Podcast::class); } public function playlists(): BelongsToMany { return $this->belongsToMany(Playlist::class); } public function interactions(): HasMany { return $this->hasMany(Interaction::class); } protected function title(): Attribute { return new Attribute( get: fn (?string $value) => $value ?: pathinfo($this->path, PATHINFO_FILENAME), set: static fn (string $value) => html_entity_decode($value) ); } public function accessibleBy(User $user): bool { if ($this->isEpisode()) { return $user->subscribedToPodcast($this->podcast); } return $this->is_public || $this->ownedBy($user); } public function ownedBy(User $user): bool { return $this->owner_id === $user->id; } protected function lyrics(): Attribute { $normalizer = static function (?string $value): string { // Since we're displaying the lyrics using
, replace breaks with newlines and strip all tags.
            $value = strip_tags(preg_replace('##i', PHP_EOL, $value));

            // also remove the timestamps that often come with LRC files
            return preg_replace('/\[\d{2}:\d{2}.\d{2}]\s*/m', '', $value);
        };

        return new Attribute(get: $normalizer, set: $normalizer);
    }

    protected function storage(): Attribute
    {
        return new Attribute(
            get: static fn (?string $raw) => SongStorageType::tryFrom($raw) ?? SongStorageType::LOCAL,
            set: static function (SongStorageType|string|null $type) {
                $type = $type instanceof SongStorageType ? $type : SongStorageType::tryFrom($type);

                return $type->value;
            }
        );
    }

    protected function storageMetadata(): Attribute
    {
        return new Attribute(
            get: function (): SongStorageMetadata {
                try {
                    switch ($this->storage) {
                        case SongStorageType::SFTP:
                            preg_match('/^sftp:\\/\\/(.*)/', $this->path, $matches);
                            return SftpMetadata::make($matches[1]);

                        case SongStorageType::S3:
                            preg_match('/^s3:\\/\\/(.*)\\/(.*)/', $this->path, $matches);
                            return S3CompatibleMetadata::make($matches[1], $matches[2]);

                        case SongStorageType::S3_LAMBDA:
                            preg_match('/^s3:\\/\\/(.*)\\/(.*)/', $this->path, $matches);
                            return S3LambdaMetadata::make($matches[1], $matches[2]);

                        case SongStorageType::DROPBOX:
                            preg_match('/^dropbox:\\/\\/(.*)/', $this->path, $matches);
                            return DropboxMetadata::make($matches[1]);

                        default:
                            return LocalMetadata::make($this->path);
                    }
                } catch (Throwable) {
                    return LocalMetadata::make($this->path);
                }
            }
        );
    }

    public static function getPathFromS3BucketAndKey(string $bucket, string $key): string
    {
        return "s3://$bucket/$key";
    }

    /** @return array */
    public function toSearchableArray(): array
    {
        $array = [
            'id' => $this->id,
            'title' => $this->title,
            'type' => $this->type->value,
        ];

        if ($this->episode_metadata?->description) {
            $array['episode_description'] = $this->episode_metadata->description;
        }

        if ($this->artist && !$this->artist->is_unknown && !$this->artist->is_various) {
            $array['artist'] = $this->artist->name;
        }

        return $array;
    }

    public function isEpisode(): bool
    {
        return $this->type === MediaType::PODCAST_EPISODE;
    }

    public function __toString(): string
    {
        return $this->id;
    }
}