From b575b449cbaf739092d23bfdfd534e4019ccf1a8 Mon Sep 17 00:00:00 2001 From: AmirmahdiNourkazemi Date: Mon, 1 Jun 2026 14:31:14 +0330 Subject: [PATCH] feat: media play --- app/Http/Controllers/MediaController.php | 71 +++++++++++++++++++ app/Models/Media.php | 5 ++ app/Models/MediaPlay.php | 26 +++++++ ..._06_01_120000_create_media_plays_table.php | 35 +++++++++ routes/api.php | 5 +- 5 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 app/Models/MediaPlay.php create mode 100644 database/migrations/2026_06_01_120000_create_media_plays_table.php diff --git a/app/Http/Controllers/MediaController.php b/app/Http/Controllers/MediaController.php index 0866d81..14418e2 100644 --- a/app/Http/Controllers/MediaController.php +++ b/app/Http/Controllers/MediaController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Models\Media; +use App\Models\MediaPlay; use App\Models\Category; use App\Models\SubCategory; use App\Models\Tag; @@ -567,6 +568,76 @@ public function saved() return auth()->user()->savedMedia()->with(['image','categories', 'subCategories', 'myNote' , 'tags'])->get(); } + // RECORD a play for the current user (feeds popular + recently played). + public function recordPlay($id) + { + $media = Media::where('id', $id) + ->where(function ($q) { + $q->where('visibility', 'public')->orWhere('user_id', auth()->id()); + }) + ->firstOrFail(); + + $play = MediaPlay::firstOrNew([ + 'user_id' => auth()->id(), + 'media_id' => $media->id, + ]); + $play->play_count = ($play->play_count ?? 0) + 1; + $play->last_played_at = now(); + $play->save(); + + return response()->json([ + 'message' => 'Play recorded', + 'play_count' => $play->play_count, + 'last_played_at' => $play->last_played_at, + ]); + } + + // POPULAR media (global), ranked by total play count across all users. + public function popular(Request $request) + { + $limit = (int) $request->input('limit', 20); + + $media = Media::query() + ->where(function ($q) { + $q->where('visibility', 'public')->orWhere('user_id', auth()->id()); + }) + ->withCount('plays as listeners_count') // distinct users who played + ->withSum('plays as plays_count', 'play_count') // total plays + ->with(['image', 'categories', 'subCategories', 'tags']) + ->orderByDesc('plays_count') + ->orderByDesc('listeners_count') + ->orderByDesc('created_at') + ->limit($limit) + ->get(); + + return response()->json($media); + } + + // RECENTLY PLAYED media for the current user, most recent first. + public function recentlyPlayed(Request $request) + { + $limit = (int) $request->input('limit', 20); + + $plays = MediaPlay::where('user_id', auth()->id()) + ->whereNotNull('last_played_at') + ->with(['media' => fn ($q) => $q->with(['image', 'categories', 'subCategories', 'tags'])]) + ->orderByDesc('last_played_at') + ->limit($limit) + ->get(); + + $media = $plays->map(function ($play) { + $media = $play->media; + if (!$media) { + return null; + } + $media->last_played_at = $play->last_played_at; + $media->play_count = $play->play_count; + return $media; + })->filter()->values(); + + return response()->json($media); + } + public function storeNote(Request $request, $mediaId) { $media = Media::findOrFail($mediaId); diff --git a/app/Models/Media.php b/app/Models/Media.php index 613e04e..6b0b976 100644 --- a/app/Models/Media.php +++ b/app/Models/Media.php @@ -56,6 +56,11 @@ public function tags() { return $this->belongsToMany(Tag::class, 'media_tag'); } + + public function plays() + { + return $this->hasMany(MediaPlay::class); + } public function notes() { return $this->morphMany(Note::class, 'noteable'); diff --git a/app/Models/MediaPlay.php b/app/Models/MediaPlay.php new file mode 100644 index 0000000..5be2a14 --- /dev/null +++ b/app/Models/MediaPlay.php @@ -0,0 +1,26 @@ + 'integer', + 'last_played_at' => 'datetime', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function media(): BelongsTo + { + return $this->belongsTo(Media::class); + } +} diff --git a/database/migrations/2026_06_01_120000_create_media_plays_table.php b/database/migrations/2026_06_01_120000_create_media_plays_table.php new file mode 100644 index 0000000..c211770 --- /dev/null +++ b/database/migrations/2026_06_01_120000_create_media_plays_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->foreignId('media_id')->constrained('media')->cascadeOnDelete(); + $table->unsignedInteger('play_count')->default(0); + $table->timestamp('last_played_at')->nullable(); + $table->timestamps(); + + // One row per user + media; updated on each play. + $table->unique(['user_id', 'media_id']); + $table->index('last_played_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('media_plays'); + } +}; diff --git a/routes/api.php b/routes/api.php index ada22f1..ba86ae2 100644 --- a/routes/api.php +++ b/routes/api.php @@ -160,13 +160,16 @@ ///media - Route::get('/media/filters', [MediaController::class, 'filters']); + Route::get('/media/filters', [MediaController::class, 'filters']); + Route::get('/media/popular', [MediaController::class, 'popular']); + Route::get('/media/recently-played', [MediaController::class, 'recentlyPlayed']); Route::post('/media', [MediaController::class, 'store']); Route::get('/media', [MediaController::class, 'index']); Route::post('/media/{id}', [MediaController::class, 'update']); Route::get('/media/saved', [MediaController::class, 'saved']); Route::delete('/media/{id}', [MediaController::class, 'destroy']); Route::get('/media/{id}', [MediaController::class, 'show']); + Route::post('/media/{id}/play', [MediaController::class, 'recordPlay']); Route::post('/media/{id}/save', [MediaController::class, 'toggleSaveMedia']); Route::post('/media/{id}/rate', [MediaController::class, 'rate']); Route::post('/media/{id}/comment', [MediaController::class, 'storeComment']);