SongTitleCast::class, 'lyrics' => SongLyricsCast::class, 'length' => 'float', 'mtime' => 'int', 'track' => 'int', 'disc' => 'int', 'is_public' => 'bool', 'storage' => SongStorageCast::class, 'episode_metadata' => EpisodeMetadataCast::class, ]; protected $with = ['album', 'artist', 'podcast']; protected $keyType = 'string'; protected static function booted(): void { static::creating(static fn (Song $song) => $song->id ??= Str::uuid()->toString()); } public static function query(?PlayableType $type = null, ?User $user = null): SongBuilder { return parent::query() ->when($type, static fn (Builder $query) => match ($type) { // @phpstan-ignore-line phpcs:ignore PlayableType::SONG => $query->whereNull('songs.podcast_id'), PlayableType::PODCAST_EPISODE => $query->whereNotNull('songs.podcast_id'), default => $query, }) ->when($user, static fn (SongBuilder $query) => $query->forUser($user)); // @phpstan-ignore-line } 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 type(): Attribute { return Attribute::get(fn () => $this->podcast_id ? PlayableType::PODCAST_EPISODE : PlayableType::SONG); } 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 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"; } /** @inheritdoc */ 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 === PlayableType::PODCAST_EPISODE; } public function __toString(): string { return $this->id; } }