From b3a7edf3a55ccca56146baac33a3ceda2c7b99d6 Mon Sep 17 00:00:00 2001 From: AmirmahdiNourkazemi Date: Mon, 1 Jun 2026 00:40:32 +0330 Subject: [PATCH] feat: add many playlist for music --- .../Controllers/MusicCategoryController.php | 2 +- app/Http/Controllers/MusicController.php | 97 ++++++++++++------- .../Controllers/MusicPlaylistController.php | 4 +- .../MusicSubcategoryController.php | 2 +- app/Models/Music.php | 11 ++- app/Models/MusicPlaylist.php | 12 ++- ...0000_create_music_playlist_pivot_table.php | 53 ++++++++++ ...drop_playlist_columns_from_music_table.php | 32 ++++++ 8 files changed, 162 insertions(+), 51 deletions(-) create mode 100644 database/migrations/2026_05_31_120000_create_music_playlist_pivot_table.php create mode 100644 database/migrations/2026_05_31_120100_drop_playlist_columns_from_music_table.php diff --git a/app/Http/Controllers/MusicCategoryController.php b/app/Http/Controllers/MusicCategoryController.php index f914e03..273e427 100644 --- a/app/Http/Controllers/MusicCategoryController.php +++ b/app/Http/Controllers/MusicCategoryController.php @@ -82,7 +82,7 @@ public function show($id) { $category = MusicCategory::with(['image', 'playlists' => function($q) { $q->with(['image', 'musics' => function($q2) { - $q2->where('is_active', true)->with('image')->orderBy('order'); + $q2->where('music.is_active', true)->with('image')->orderBy('music_playlist.order'); }])->where('is_active', true)->orderBy('order'); }])->findOrFail($id); diff --git a/app/Http/Controllers/MusicController.php b/app/Http/Controllers/MusicController.php index 245876f..bc1fe3b 100644 --- a/app/Http/Controllers/MusicController.php +++ b/app/Http/Controllers/MusicController.php @@ -15,7 +15,7 @@ public function getAllMusic() { $userId = auth()->id(); - $music = Music::with(['image', 'playlist']) + $music = Music::with(['image', 'playlists']) ->where('type', 'public') ->orWhere(function($query) use ($userId) { $query->where('type', 'private') @@ -33,7 +33,7 @@ public function index() { $userId = auth()->id(); - $music = Music::with(['image', 'playlist']) + $music = Music::with(['image', 'playlists']) ->where('type', 'public') ->orWhere(function($query) use ($userId) { $query->where('type', 'private') @@ -51,19 +51,19 @@ public function index() public function getMusicByPlaylist($playlistId) { $playlist = MusicPlaylist::findOrFail($playlistId); - - $music = Music::where('playlist_id', $playlistId) - ->where('is_active', true) + + $music = $playlist->musics() + ->where('music.is_active', true) ->with(['image', 'tags']) - ->orderBy('order') + ->orderBy('music_playlist.order') ->get(); - + return response()->json([ 'playlist' => $playlist->load('image'), 'musics' => $music ]); } - + public function addToPlaylist(Request $request, $musicId) { $music = Music::where('id', $musicId) @@ -72,48 +72,57 @@ public function addToPlaylist(Request $request, $musicId) ->orWhere('user_id', auth()->id()); }) ->firstOrFail(); - + $data = $request->validate([ 'playlist_id' => 'required|exists:music_playlists,id', 'order' => 'nullable|integer', ]); - - $music->update([ - 'playlist_id' => $data['playlist_id'], - 'order' => $data['order'] ?? $music->order, + + $order = $data['order'] ?? $this->getNextOrderInPlaylist($data['playlist_id']); + + // Add (or update its order) without removing the music from other playlists. + $music->playlists()->syncWithoutDetaching([ + $data['playlist_id'] => ['order' => $order], ]); - + return response()->json([ 'message' => 'Music added to playlist successfully', - 'music' => $music->load(['image', 'playlist']) + 'music' => $music->load(['image', 'playlists']) ]); } - - public function removeFromPlaylist($musicId) + + public function removeFromPlaylist(Request $request, $musicId) { $music = Music::where('id', $musicId) ->where('user_id', auth()->id()) ->firstOrFail(); - - $music->update(['playlist_id' => null]); - + + $data = $request->validate([ + 'playlist_id' => 'required|exists:music_playlists,id', + ]); + + $music->playlists()->detach($data['playlist_id']); + return response()->json(['message' => 'Music removed from playlist']); } - + public function updateOrder(Request $request) { $data = $request->validate([ + 'playlist_id' => 'required|exists:music_playlists,id', 'musics' => 'required|array', 'musics.*.id' => 'required|exists:music,id', 'musics.*.order' => 'required|integer', ]); - + + $playlist = MusicPlaylist::findOrFail($data['playlist_id']); + foreach ($data['musics'] as $item) { - Music::where('id', $item['id']) - ->where('user_id', auth()->id()) - ->update(['order' => $item['order']]); + $playlist->musics()->updateExistingPivot($item['id'], [ + 'order' => $item['order'], + ]); } - + return response()->json(['message' => 'Order updated successfully']); } // ✅ Upload music @@ -126,7 +135,9 @@ public function store(Request $request) 'file' => 'required|mimes:mp3,wav,ogg,flac|max:20971520', 'type' => 'nullable|in:public,private', 'image_id' => 'nullable|exists:images,id', - 'playlist_id' => 'nullable|exists:music_playlists,id', + 'playlist_id' => 'nullable|exists:music_playlists,id', // single (backward compatible) + 'playlist_ids' => 'nullable|array', // multiple + 'playlist_ids.*' => 'integer|exists:music_playlists,id', 'duration' => 'nullable|integer|min:1', // validates mm:ss or hh:mm:ss ]); @@ -161,15 +172,26 @@ public function store(Request $request) 'file_path' => $path, 'type' => $data['type'] ?? 'private', 'image_id' => $data['image_id'] ?? null, - 'playlist_id' => $data['playlist_id'] ?? null, 'duration' => $data['duration'] ?? null, // Store as string - 'order' => $this->getNextOrderInPlaylist($data['playlist_id'] ?? null), 'is_active' => true, ]); + // Merge single + multiple playlist inputs into a unique list. + $playlistIds = collect($data['playlist_ids'] ?? []) + ->push($data['playlist_id'] ?? null) + ->filter() + ->unique() + ->values(); + + foreach ($playlistIds as $playlistId) { + $music->playlists()->syncWithoutDetaching([ + $playlistId => ['order' => $this->getNextOrderInPlaylist($playlistId)], + ]); + } + return response()->json([ 'message' => 'Music uploaded successfully', - 'music' => $music->load(['image', 'playlist', 'tags']), + 'music' => $music->load(['image', 'playlists', 'tags']), 'url' => asset('storage/' . $path), ], 201); @@ -192,8 +214,11 @@ private function getNextOrderInPlaylist($playlistId) if (!$playlistId) { return 0; } - - $maxOrder = Music::where('playlist_id', $playlistId)->max('order'); + + $maxOrder = \DB::table('music_playlist') + ->where('playlist_id', $playlistId) + ->max('order'); + return ($maxOrder ?? -1) + 1; } @@ -209,9 +234,7 @@ public function update(Request $request, $id) 'type' => 'nullable|in:public,private', 'file' => 'nullable|mimes:mp3,wav,ogg,flac|max:20971520', 'image_id' => 'nullable|exists:images,id', - 'playlist_id' => 'nullable|exists:music_playlists,id', 'duration' => 'nullable|integer|min:1', - 'order' => 'nullable|integer', 'is_active' => 'nullable|boolean', ]); @@ -228,7 +251,7 @@ public function update(Request $request, $id) return response()->json([ 'message' => 'Music updated successfully', - 'music' => $music->load(['image', 'playlist', 'tags']), + 'music' => $music->load(['image', 'playlists', 'tags']), 'url' => asset('storage/' . $music->file_path), ]); @@ -247,8 +270,8 @@ public function show($id) $userId = auth()->id(); $music = Music::with([ - 'image', - 'playlist', + 'image', + 'playlists', 'tags', 'comments' => function($query) { $query->with('user')->latest()->limit(10); diff --git a/app/Http/Controllers/MusicPlaylistController.php b/app/Http/Controllers/MusicPlaylistController.php index ca34f44..81fcbc3 100644 --- a/app/Http/Controllers/MusicPlaylistController.php +++ b/app/Http/Controllers/MusicPlaylistController.php @@ -73,9 +73,9 @@ public function show($id) 'subcategories', 'image', 'musics' => function($q) { - $q->where('is_active', true) + $q->where('music.is_active', true) ->with(['image', 'tags']) - ->orderBy('order'); + ->orderBy('music_playlist.order'); }, 'comments' => function($q) { // Add comments relationship $q->with('user')->latest()->limit(10); diff --git a/app/Http/Controllers/MusicSubcategoryController.php b/app/Http/Controllers/MusicSubcategoryController.php index 9d3f5b6..533d7f5 100644 --- a/app/Http/Controllers/MusicSubcategoryController.php +++ b/app/Http/Controllers/MusicSubcategoryController.php @@ -74,7 +74,7 @@ public function show($id) 'image', 'playlists' => function($q) { $q->with(['image', 'musics' => function($q2) { - $q2->where('is_active', true)->with('image')->orderBy('order'); + $q2->where('music.is_active', true)->with('image')->orderBy('music_playlist.order'); }])->where('is_active', true)->orderBy('order'); } ])->findOrFail($id); diff --git a/app/Models/Music.php b/app/Models/Music.php index fc9465d..65ad268 100644 --- a/app/Models/Music.php +++ b/app/Models/Music.php @@ -20,12 +20,11 @@ class Music extends Model protected $fillable = [ - 'user_id', 'title', 'artist', 'file_path', 'type', - 'playlist_id', 'image_id', 'duration', 'order', 'is_active' + 'user_id', 'title', 'artist', 'file_path', 'type', + 'image_id', 'duration', 'is_active' ]; protected $casts = [ 'duration' => 'integer', - 'order' => 'integer', 'is_active' => 'boolean', ]; protected $attributes = [ @@ -36,9 +35,11 @@ public function user() { return $this->belongsTo(User::class); } - public function playlist(): BelongsTo + public function playlists(): BelongsToMany { - return $this->belongsTo(MusicPlaylist::class, 'playlist_id'); + return $this->belongsToMany(MusicPlaylist::class, 'music_playlist', 'music_id', 'playlist_id') + ->withPivot('order') + ->withTimestamps(); } public function tags(): BelongsToMany { diff --git a/app/Models/MusicPlaylist.php b/app/Models/MusicPlaylist.php index c701e9b..4e2c490 100644 --- a/app/Models/MusicPlaylist.php +++ b/app/Models/MusicPlaylist.php @@ -43,9 +43,11 @@ public function subcategories(): BelongsToMany } - public function musics(): HasMany + public function musics(): BelongsToMany { - return $this->hasMany(Music::class, 'playlist_id'); + return $this->belongsToMany(Music::class, 'music_playlist', 'playlist_id', 'music_id') + ->withPivot('order') + ->withTimestamps(); } public function image(): BelongsTo @@ -55,11 +57,11 @@ public function image(): BelongsTo public function getActiveMusicsAttribute() { - return $this->musics()->where('is_active', true)->orderBy('order')->get(); + return $this->musics()->where('music.is_active', true)->orderBy('music_playlist.order')->get(); } - + public function getTotalDurationAttribute() { - return $this->musics()->where('is_active', true)->sum('duration'); + return $this->musics()->where('music.is_active', true)->sum('music.duration'); } } diff --git a/database/migrations/2026_05_31_120000_create_music_playlist_pivot_table.php b/database/migrations/2026_05_31_120000_create_music_playlist_pivot_table.php new file mode 100644 index 0000000..ced0f5c --- /dev/null +++ b/database/migrations/2026_05_31_120000_create_music_playlist_pivot_table.php @@ -0,0 +1,53 @@ +id(); + $table->foreignId('music_id')->constrained('music')->cascadeOnDelete(); + $table->foreignId('playlist_id')->constrained('music_playlists')->cascadeOnDelete(); + $table->integer('order')->default(0); + $table->timestamps(); + + $table->unique(['music_id', 'playlist_id']); + }); + + // Backfill the pivot from the existing single playlist_id column. + if (Schema::hasColumn('music', 'playlist_id')) { + DB::table('music') + ->whereNotNull('playlist_id') + ->orderBy('id') + ->select('id', 'playlist_id', 'order') + ->chunk(200, function ($rows) { + $now = now(); + $insert = $rows->map(fn ($row) => [ + 'music_id' => $row->id, + 'playlist_id' => $row->playlist_id, + 'order' => $row->order ?? 0, + 'created_at' => $now, + 'updated_at' => $now, + ])->all(); + + DB::table('music_playlist')->insertOrIgnore($insert); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('music_playlist'); + } +}; diff --git a/database/migrations/2026_05_31_120100_drop_playlist_columns_from_music_table.php b/database/migrations/2026_05_31_120100_drop_playlist_columns_from_music_table.php new file mode 100644 index 0000000..bddc789 --- /dev/null +++ b/database/migrations/2026_05_31_120100_drop_playlist_columns_from_music_table.php @@ -0,0 +1,32 @@ +dropForeign(['playlist_id']); + $table->dropColumn('playlist_id'); + $table->dropColumn('order'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('music', function (Blueprint $table) { + $table->foreignId('playlist_id')->nullable()->after('type') + ->constrained('music_playlists')->nullOnDelete(); + $table->integer('order')->default(0)->after('duration'); + }); + } +};