feat: add many playlist for music
This commit is contained in:
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
@@ -51,19 +51,19 @@ public function index()
|
|||||||
public function getMusicByPlaylist($playlistId)
|
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([
|
||||||
'playlist' => $playlist->load('image'),
|
'playlist' => $playlist->load('image'),
|
||||||
'musics' => $music
|
'musics' => $music
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addToPlaylist(Request $request, $musicId)
|
public function addToPlaylist(Request $request, $musicId)
|
||||||
{
|
{
|
||||||
$music = Music::where('id', $musicId)
|
$music = Music::where('id', $musicId)
|
||||||
@@ -72,48 +72,57 @@ public function addToPlaylist(Request $request, $musicId)
|
|||||||
->orWhere('user_id', auth()->id());
|
->orWhere('user_id', auth()->id());
|
||||||
})
|
})
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'playlist_id' => 'required|exists:music_playlists,id',
|
'playlist_id' => 'required|exists:music_playlists,id',
|
||||||
'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']);
|
||||||
}
|
}
|
||||||
|
|
||||||
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']);
|
||||||
}
|
}
|
||||||
// ✅ Upload music
|
// ✅ Upload music
|
||||||
@@ -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);
|
||||||
|
|
||||||
@@ -192,8 +214,11 @@ private function getNextOrderInPlaylist($playlistId)
|
|||||||
if (!$playlistId) {
|
if (!$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),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -247,8 +270,8 @@ public function show($id)
|
|||||||
$userId = auth()->id();
|
$userId = auth()->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);
|
||||||
|
|||||||
@@ -20,12 +20,11 @@ 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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user