322 lines
8.0 KiB
PHP
322 lines
8.0 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
class Playlist extends Model
|
|
{
|
|
use HasFactory;
|
|
|
|
protected $fillable = [
|
|
'user_id',
|
|
'name',
|
|
'description',
|
|
'thumbnail',
|
|
'visibility',
|
|
'is_default',
|
|
];
|
|
|
|
protected $casts = [
|
|
'is_default' => '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;
|
|
}
|
|
}
|