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) {
$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);
+60 -37
View File
@@ -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);
@@ -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);
@@ -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);
+6 -5
View File
@@ -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
{
+7 -5
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
@@ -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');
}
}
@@ -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');
});
}
};