'boolean', ]; // Relationships public function user() { return $this->belongsTo(User::class); } public function videos() { return $this->belongsToMany(Video::class, 'playlist_videos') ->withPivot('position', 'watched_seconds', 'watched', 'added_at', 'last_watched_at') ->orderBy('position'); } // Get videos with their pivot data public function getVideosWithPivot() { return $this->videos()->orderBy('position')->get(); } // Accessors public function getThumbnailUrlAttribute() { if ($this->thumbnail) { return asset('storage/thumbnails/'.$this->thumbnail); } // Generate a placeholder based on playlist name return 'https://ui-avatars.com/api/?name='.urlencode($this->name).'&background=random&size=200'; } // Get total video count public function getVideoCountAttribute() { return $this->videos()->count(); } // Get total duration of all videos in playlist public function getTotalDurationAttribute() { return $this->videos()->sum('duration'); } // Get formatted total duration (e.g., "2h 30m") public function getFormattedDurationAttribute() { $totalSeconds = $this->total_duration; if (! $totalSeconds) { return '0m'; } $hours = floor($totalSeconds / 3600); $minutes = floor(($totalSeconds % 3600) / 60); if ($hours > 0) { return "{$hours}h {$minutes}m"; } return "{$minutes}m"; } // Get total views of all videos in playlist public function getTotalViewsAttribute() { return $this->videos()->get()->sum('view_count'); } // Check if user owns this playlist public function isOwnedBy($user) { if (! $user) { return false; } return $this->user_id === $user->id; } // Check if video is in this playlist public function hasVideo(Video $video) { return $this->videos()->where('video_id', $video->id)->exists(); } // Get video position in playlist public function getVideoPosition(Video $video) { $pivot = $this->videos()->where('video_id', $video->id)->first(); return $pivot ? $pivot->pivot->position : null; } // Get next video in playlist public function getNextVideo(Video $currentVideo) { $currentPosition = $this->getVideoPosition($currentVideo); if ($currentPosition === null) { return $this->videos()->first(); } return $this->videos() ->wherePivot('position', '>', $currentPosition) ->orderBy('position') ->first(); } // Get previous video in playlist public function getPreviousVideo(Video $currentVideo) { $currentPosition = $this->getVideoPosition($currentVideo); if ($currentPosition === null) { return $this->videos()->last(); } return $this->videos() ->wherePivot('position', '<', $currentPosition) ->orderByDesc('position') ->first(); } // Get shareable URL public function getShareUrlAttribute() { return route('playlists.show', $this->id); } // Scope for public playlists public function scopePublic($query) { return $query->where('visibility', 'public'); } // Scope for user's playlists public function scopeForUser($query, $userId) { return $query->where('user_id', $userId); } // Scope for default playlists (Watch Later) public function scopeDefault($query) { return $query->where('is_default', true); } // Visibility helpers public function isPublic() { return $this->visibility === 'public'; } public function isPrivate() { return $this->visibility === 'private'; } // Check if user can view this playlist public function canView($user = null) { // Owner can always view if ($user && $this->user_id === $user->id) { return true; } // Public playlists can be viewed by anyone return $this->visibility === 'public'; } // Check if user can edit this playlist public function canEdit($user = null) { return $user && $this->user_id === $user->id; } // Update video positions after reordering public function reorderVideos($videoIds) { foreach ($videoIds as $index => $videoId) { $this->videos()->updateExistingPivot($videoId, ['position' => $index]); } } // Add video to playlist public function addVideo(Video $video) { if ($this->hasVideo($video)) { return false; } $maxPosition = $this->videos()->max('position') ?? -1; $this->videos()->attach($video->id, [ 'position' => $maxPosition + 1, 'added_at' => now(), ]); return true; } // Remove video from playlist public function removeVideo(Video $video) { if (! $this->hasVideo($video)) { return false; } $this->videos()->detach($video->id); // Reorder remaining videos $this->reorderPositions(); return true; } // Reorder positions after removal protected function reorderPositions() { $videos = $this->videos()->orderBy('position')->get(); $position = 0; foreach ($videos as $video) { $this->videos()->updateExistingPivot($video->id, ['position' => $position]); $position++; } } // Update watch progress for a video in playlist public function updateWatchProgress(Video $video, $seconds) { $pivot = $this->videos()->where('video_id', $video->id)->first()?->pivot; if ($pivot) { $pivot->watched_seconds = $seconds; $pivot->last_watched_at = now(); // Mark as watched if 90% complete if ($video->duration && $seconds >= ($video->duration * 0.9)) { $pivot->watched = true; } $pivot->save(); } } // Get watch progress for a video in playlist public function getWatchProgress(Video $video) { $pivot = $this->videos()->where('video_id', $video->id)->first()?->pivot; return $pivot ? [ 'watched_seconds' => $pivot->watched_seconds, 'watched' => $pivot->watched, 'last_watched_at' => $pivot->last_watched_at, ] : null; } // Get first unwatched video public function getFirstUnwatchedVideo() { return $this->videos() ->wherePivot('watched', false) ->orderBy('position') ->first(); } // Get random video (for shuffle) public function getRandomVideo() { return $this->videos()->inRandomOrder()->first(); } // Static method to create default "Watch Later" playlist public static function createWatchLater($userId) { return self::create([ 'user_id' => $userId, 'name' => 'Watch Later', 'description' => 'Save videos to watch later', 'visibility' => 'private', 'is_default' => true, ]); } // Get or create watch later playlist for user public static function getWatchLater($userId) { $playlist = self::where('user_id', $userId) ->where('is_default', true) ->first(); if (! $playlist) { $playlist = self::createWatchLater($userId); } return $playlist; } }