feat: add many playlist for music

This commit is contained in:
2026-06-01 00:40:32 +03:30
parent 695efcc452
commit b3a7edf3a5
8 changed files with 162 additions and 51 deletions
@@ -82,7 +82,7 @@ public function show($id)
{ {
$category = MusicCategory::with(['image', 'playlists' => function($q) { $category = MusicCategory::with(['image', 'playlists' => function($q) {
$q->with(['image', 'musics' => function($q2) { $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'); }])->where('is_active', true)->orderBy('order');
}])->findOrFail($id); }])->findOrFail($id);
+46 -23
View File
@@ -15,7 +15,7 @@ public function getAllMusic()
{ {
$userId = auth()->id(); $userId = auth()->id();
$music = Music::with(['image', 'playlist']) $music = Music::with(['image', 'playlists'])
->where('type', 'public') ->where('type', 'public')
->orWhere(function($query) use ($userId) { ->orWhere(function($query) use ($userId) {
$query->where('type', 'private') $query->where('type', 'private')
@@ -33,7 +33,7 @@ public function index()
{ {
$userId = auth()->id(); $userId = auth()->id();
$music = Music::with(['image', 'playlist']) $music = Music::with(['image', 'playlists'])
->where('type', 'public') ->where('type', 'public')
->orWhere(function($query) use ($userId) { ->orWhere(function($query) use ($userId) {
$query->where('type', 'private') $query->where('type', 'private')
@@ -52,10 +52,10 @@ public function getMusicByPlaylist($playlistId)
{ {
$playlist = MusicPlaylist::findOrFail($playlistId); $playlist = MusicPlaylist::findOrFail($playlistId);
$music = Music::where('playlist_id', $playlistId) $music = $playlist->musics()
->where('is_active', true) ->where('music.is_active', true)
->with(['image', 'tags']) ->with(['image', 'tags'])
->orderBy('order') ->orderBy('music_playlist.order')
->get(); ->get();
return response()->json([ return response()->json([
@@ -78,24 +78,30 @@ public function addToPlaylist(Request $request, $musicId)
'order' => 'nullable|integer', 'order' => 'nullable|integer',
]); ]);
$music->update([ $order = $data['order'] ?? $this->getNextOrderInPlaylist($data['playlist_id']);
'playlist_id' => $data['playlist_id'],
'order' => $data['order'] ?? $music->order, // Add (or update its order) without removing the music from other playlists.
$music->playlists()->syncWithoutDetaching([
$data['playlist_id'] => ['order' => $order],
]); ]);
return response()->json([ return response()->json([
'message' => 'Music added to playlist successfully', '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) $music = Music::where('id', $musicId)
->where('user_id', auth()->id()) ->where('user_id', auth()->id())
->firstOrFail(); ->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']); return response()->json(['message' => 'Music removed from playlist']);
} }
@@ -103,15 +109,18 @@ public function removeFromPlaylist($musicId)
public function updateOrder(Request $request) public function updateOrder(Request $request)
{ {
$data = $request->validate([ $data = $request->validate([
'playlist_id' => 'required|exists:music_playlists,id',
'musics' => 'required|array', 'musics' => 'required|array',
'musics.*.id' => 'required|exists:music,id', 'musics.*.id' => 'required|exists:music,id',
'musics.*.order' => 'required|integer', 'musics.*.order' => 'required|integer',
]); ]);
$playlist = MusicPlaylist::findOrFail($data['playlist_id']);
foreach ($data['musics'] as $item) { foreach ($data['musics'] as $item) {
Music::where('id', $item['id']) $playlist->musics()->updateExistingPivot($item['id'], [
->where('user_id', auth()->id()) 'order' => $item['order'],
->update(['order' => $item['order']]); ]);
} }
return response()->json(['message' => 'Order updated successfully']); return response()->json(['message' => 'Order updated successfully']);
@@ -126,7 +135,9 @@ public function store(Request $request)
'file' => 'required|mimes:mp3,wav,ogg,flac|max:20971520', 'file' => 'required|mimes:mp3,wav,ogg,flac|max:20971520',
'type' => 'nullable|in:public,private', 'type' => 'nullable|in:public,private',
'image_id' => 'nullable|exists:images,id', '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 'duration' => 'nullable|integer|min:1', // validates mm:ss or hh:mm:ss
]); ]);
@@ -161,15 +172,26 @@ public function store(Request $request)
'file_path' => $path, 'file_path' => $path,
'type' => $data['type'] ?? 'private', 'type' => $data['type'] ?? 'private',
'image_id' => $data['image_id'] ?? null, 'image_id' => $data['image_id'] ?? null,
'playlist_id' => $data['playlist_id'] ?? null,
'duration' => $data['duration'] ?? null, // Store as string 'duration' => $data['duration'] ?? null, // Store as string
'order' => $this->getNextOrderInPlaylist($data['playlist_id'] ?? null),
'is_active' => true, '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([ return response()->json([
'message' => 'Music uploaded successfully', 'message' => 'Music uploaded successfully',
'music' => $music->load(['image', 'playlist', 'tags']), 'music' => $music->load(['image', 'playlists', 'tags']),
'url' => asset('storage/' . $path), 'url' => asset('storage/' . $path),
], 201); ], 201);
@@ -193,7 +215,10 @@ private function getNextOrderInPlaylist($playlistId)
return 0; 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; return ($maxOrder ?? -1) + 1;
} }
@@ -209,9 +234,7 @@ public function update(Request $request, $id)
'type' => 'nullable|in:public,private', 'type' => 'nullable|in:public,private',
'file' => 'nullable|mimes:mp3,wav,ogg,flac|max:20971520', 'file' => 'nullable|mimes:mp3,wav,ogg,flac|max:20971520',
'image_id' => 'nullable|exists:images,id', 'image_id' => 'nullable|exists:images,id',
'playlist_id' => 'nullable|exists:music_playlists,id',
'duration' => 'nullable|integer|min:1', 'duration' => 'nullable|integer|min:1',
'order' => 'nullable|integer',
'is_active' => 'nullable|boolean', 'is_active' => 'nullable|boolean',
]); ]);
@@ -228,7 +251,7 @@ public function update(Request $request, $id)
return response()->json([ return response()->json([
'message' => 'Music updated successfully', 'message' => 'Music updated successfully',
'music' => $music->load(['image', 'playlist', 'tags']), 'music' => $music->load(['image', 'playlists', 'tags']),
'url' => asset('storage/' . $music->file_path), 'url' => asset('storage/' . $music->file_path),
]); ]);
@@ -248,7 +271,7 @@ public function show($id)
$music = Music::with([ $music = Music::with([
'image', 'image',
'playlist', 'playlists',
'tags', 'tags',
'comments' => function($query) { 'comments' => function($query) {
$query->with('user')->latest()->limit(10); $query->with('user')->latest()->limit(10);
@@ -73,9 +73,9 @@ public function show($id)
'subcategories', 'subcategories',
'image', 'image',
'musics' => function($q) { 'musics' => function($q) {
$q->where('is_active', true) $q->where('music.is_active', true)
->with(['image', 'tags']) ->with(['image', 'tags'])
->orderBy('order'); ->orderBy('music_playlist.order');
}, },
'comments' => function($q) { // Add comments relationship 'comments' => function($q) { // Add comments relationship
$q->with('user')->latest()->limit(10); $q->with('user')->latest()->limit(10);
@@ -74,7 +74,7 @@ public function show($id)
'image', 'image',
'playlists' => function($q) { 'playlists' => function($q) {
$q->with(['image', 'musics' => function($q2) { $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'); }])->where('is_active', true)->orderBy('order');
} }
])->findOrFail($id); ])->findOrFail($id);
+5 -4
View File
@@ -21,11 +21,10 @@ class Music extends Model
protected $fillable = [ protected $fillable = [
'user_id', 'title', 'artist', 'file_path', 'type', 'user_id', 'title', 'artist', 'file_path', 'type',
'playlist_id', 'image_id', 'duration', 'order', 'is_active' 'image_id', 'duration', 'is_active'
]; ];
protected $casts = [ protected $casts = [
'duration' => 'integer', 'duration' => 'integer',
'order' => 'integer',
'is_active' => 'boolean', 'is_active' => 'boolean',
]; ];
protected $attributes = [ protected $attributes = [
@@ -36,9 +35,11 @@ public function user()
{ {
return $this->belongsTo(User::class); 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 public function tags(): BelongsToMany
{ {
+6 -4
View File
@@ -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 public function image(): BelongsTo
@@ -55,11 +57,11 @@ public function image(): BelongsTo
public function getActiveMusicsAttribute() 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() public function getTotalDurationAttribute()
{ {
return $this->musics()->where('is_active', true)->sum('duration'); return $this->musics()->where('music.is_active', true)->sum('music.duration');
} }
} }
@@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('music_playlist', function (Blueprint $table) {
$table->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');
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('music', function (Blueprint $table) {
$table->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');
});
}
};