From 65db1bd5da0d4db59f35a1db827ec4e40bb175c5 Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Tue, 19 May 2026 08:20:06 +0330 Subject: [PATCH] feat: add music feature --- .../Controllers/MusicCategoryController.php | 83 +++++++++++++++++ app/Http/Controllers/MusicController.php | 70 +++++++++++++- .../Controllers/MusicPlaylistController.php | 92 +++++++++++++++++++ app/Models/Music.php | 27 ++++-- app/Models/MusicCategory.php | 34 +++++++ app/Models/MusicPlaylist.php | 44 +++++++++ ...6_05_19_040553_create_music_categories.php | 33 +++++++ ...19_040751_create_music_playlists_table.php | 34 +++++++ ...05_19_040905_add_colums_to_music_table.php | 32 +++++++ ...6_05_19_041144_create_music_tags_table.php | 31 +++++++ routes/api.php | 19 +++- 11 files changed, 489 insertions(+), 10 deletions(-) create mode 100644 app/Http/Controllers/MusicCategoryController.php create mode 100644 app/Http/Controllers/MusicPlaylistController.php create mode 100644 app/Models/MusicCategory.php create mode 100644 app/Models/MusicPlaylist.php create mode 100644 database/migrations/2026_05_19_040553_create_music_categories.php create mode 100644 database/migrations/2026_05_19_040751_create_music_playlists_table.php create mode 100644 database/migrations/2026_05_19_040905_add_colums_to_music_table.php create mode 100644 database/migrations/2026_05_19_041144_create_music_tags_table.php diff --git a/app/Http/Controllers/MusicCategoryController.php b/app/Http/Controllers/MusicCategoryController.php new file mode 100644 index 0000000..9bfe53f --- /dev/null +++ b/app/Http/Controllers/MusicCategoryController.php @@ -0,0 +1,83 @@ + function($q) { + $q->where('is_active', true)->orderBy('order'); + }])->where('is_active', true)->orderBy('order')->get(); + + return response()->json($categories); + } + + public function store(Request $request) + { + $data = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'image_id' => 'nullable|exists:images,id', + 'order' => 'nullable|integer', + 'is_active' => 'nullable|boolean', + ]); + + $data['slug'] = Str::slug($data['name']); + + $category = MusicCategory::create($data); + + return response()->json([ + 'message' => 'Category created successfully', + 'category' => $category->load('image') + ], 201); + } + + 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'); + }])->where('is_active', true)->orderBy('order'); + }])->findOrFail($id); + + return response()->json($category); + } + + public function update(Request $request, $id) + { + $category = MusicCategory::findOrFail($id); + + $data = $request->validate([ + 'name' => 'sometimes|string|max:255', + 'description' => 'nullable|string', + 'image_id' => 'nullable|exists:images,id', + 'order' => 'nullable|integer', + 'is_active' => 'nullable|boolean', + ]); + + if (isset($data['name'])) { + $data['slug'] = Str::slug($data['name']); + } + + $category->update($data); + + return response()->json([ + 'message' => 'Category updated successfully', + 'category' => $category->load('image') + ]); + } + + public function destroy($id) + { + $category = MusicCategory::findOrFail($id); + $category->delete(); + + return response()->json(['message' => 'Category deleted successfully']); + } + + +} diff --git a/app/Http/Controllers/MusicController.php b/app/Http/Controllers/MusicController.php index 1223669..2a1bbe7 100644 --- a/app/Http/Controllers/MusicController.php +++ b/app/Http/Controllers/MusicController.php @@ -3,11 +3,79 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; +use App\Models\MusicPlaylist; use App\Models\Music; use Illuminate\Support\Facades\Storage; class MusicController extends Controller -{ +{ public function getMusicByPlaylist($playlistId) + { + $playlist = MusicPlaylist::findOrFail($playlistId); + + $music = Music::where('playlist_id', $playlistId) + ->where('is_active', true) + ->with(['image', 'tags']) + ->orderBy('order') + ->get(); + + return response()->json([ + 'playlist' => $playlist->load('image'), + 'musics' => $music + ]); + } + + public function addToPlaylist(Request $request, $musicId) + { + $music = Music::where('id', $musicId) + ->where(function($q) { + $q->where('type', 'public') + ->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, + ]); + + return response()->json([ + 'message' => 'Music added to playlist successfully', + 'music' => $music->load(['image', 'playlist']) + ]); + } + + public function removeFromPlaylist($musicId) + { + $music = Music::where('id', $musicId) + ->where('user_id', auth()->id()) + ->firstOrFail(); + + $music->update(['playlist_id' => null]); + + return response()->json(['message' => 'Music removed from playlist']); + } + + public function updateOrder(Request $request) + { + $data = $request->validate([ + 'musics' => 'required|array', + 'musics.*.id' => 'required|exists:music,id', + 'musics.*.order' => 'required|integer', + ]); + + foreach ($data['musics'] as $item) { + Music::where('id', $item['id']) + ->where('user_id', auth()->id()) + ->update(['order' => $item['order']]); + } + + return response()->json(['message' => 'Order updated successfully']); + } // ✅ Upload music public function store(Request $request) { diff --git a/app/Http/Controllers/MusicPlaylistController.php b/app/Http/Controllers/MusicPlaylistController.php new file mode 100644 index 0000000..5e6de30 --- /dev/null +++ b/app/Http/Controllers/MusicPlaylistController.php @@ -0,0 +1,92 @@ +has('category_id')) { + $query->where('category_id', $request->category_id); + } + + $playlists = $query->where('is_active', true)->orderBy('order')->get(); + + return response()->json($playlists); + } + + public function store(Request $request) + { + $data = $request->validate([ + 'category_id' => 'required|exists:music_categories,id', + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'image_id' => 'nullable|exists:images,id', + 'order' => 'nullable|integer', + 'is_active' => 'nullable|boolean', + ]); + + $data['slug'] = Str::slug($data['name']); + + $playlist = MusicPlaylist::create($data); + + return response()->json([ + 'message' => 'Playlist created successfully', + 'playlist' => $playlist->load(['category', 'image']) + ], 201); + } + + public function show($id) + { + $playlist = MusicPlaylist::with([ + 'category', + 'image', + 'musics' => function($q) { + $q->where('is_active', true) + ->with(['image', 'tags']) + ->orderBy('order'); + } + ])->findOrFail($id); + + return response()->json($playlist); + } + + public function update(Request $request, $id) + { + $playlist = MusicPlaylist::findOrFail($id); + + $data = $request->validate([ + 'category_id' => 'sometimes|exists:music_categories,id', + 'name' => 'sometimes|string|max:255', + 'description' => 'nullable|string', + 'image_id' => 'nullable|exists:images,id', + 'order' => 'nullable|integer', + 'is_active' => 'nullable|boolean', + ]); + + if (isset($data['name'])) { + $data['slug'] = Str::slug($data['name']); + } + + $playlist->update($data); + + return response()->json([ + 'message' => 'Playlist updated successfully', + 'playlist' => $playlist->load(['category', 'image']) + ]); + } + + public function destroy($id) + { + $playlist = MusicPlaylist::findOrFail($id); + $playlist->delete(); + + return response()->json(['message' => 'Playlist deleted successfully']); + } +} diff --git a/app/Models/Music.php b/app/Models/Music.php index f866dac..11a07ca 100644 --- a/app/Models/Music.php +++ b/app/Models/Music.php @@ -5,23 +5,34 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; class Music extends Model -{ +{ use HasFactory; + protected $table = 'music'; + protected $fillable = [ - 'user_id', - 'title', - 'artist', - 'file_path', - 'type', // public or private - 'image_id', + 'user_id', 'title', 'artist', 'file_path', 'type', + 'playlist_id', 'image_id', 'duration', 'order', 'is_active' + ]; + protected $casts = [ + 'duration' => 'string', + 'order' => 'integer', + 'is_active' => 'boolean', ]; - // Relation to user (optional) public function user() { return $this->belongsTo(User::class); } + public function playlist(): BelongsTo + { + return $this->belongsTo(MusicPlaylist::class, 'playlist_id'); + } + public function tags(): BelongsToMany + { + return $this->belongsToMany(Tag::class, 'music_tag'); + } + protected $appends = ['url', 'image_url']; // Accessor for full URL public function getUrlAttribute() diff --git a/app/Models/MusicCategory.php b/app/Models/MusicCategory.php new file mode 100644 index 0000000..3e76707 --- /dev/null +++ b/app/Models/MusicCategory.php @@ -0,0 +1,34 @@ + 'boolean', + 'order' => 'integer', + ]; + + public function playlists(): HasMany + { + return $this->hasMany(MusicPlaylist::class, 'category_id'); + } + + public function image(): BelongsTo + { + return $this->belongsTo(Image::class); + } + + public function getActivePlaylistsAttribute() + { + return $this->playlists()->where('is_active', true)->get(); + } +} diff --git a/app/Models/MusicPlaylist.php b/app/Models/MusicPlaylist.php new file mode 100644 index 0000000..5473680 --- /dev/null +++ b/app/Models/MusicPlaylist.php @@ -0,0 +1,44 @@ + 'boolean', + 'order' => 'integer', + ]; + + public function category(): BelongsTo + { + return $this->belongsTo(MusicCategory::class, 'category_id'); + } + + public function musics(): HasMany + { + return $this->hasMany(Music::class, 'playlist_id'); + } + + public function image(): BelongsTo + { + return $this->belongsTo(Image::class); + } + + public function getActiveMusicsAttribute() + { + return $this->musics()->where('is_active', true)->orderBy('order')->get(); + } + + public function getTotalDurationAttribute() + { + return $this->musics()->where('is_active', true)->sum('duration'); + } +} diff --git a/database/migrations/2026_05_19_040553_create_music_categories.php b/database/migrations/2026_05_19_040553_create_music_categories.php new file mode 100644 index 0000000..9d34d60 --- /dev/null +++ b/database/migrations/2026_05_19_040553_create_music_categories.php @@ -0,0 +1,33 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->foreignId('image_id')->nullable()->constrained('images')->onDelete('set null'); + $table->integer('order')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('music_categories'); + } +}; diff --git a/database/migrations/2026_05_19_040751_create_music_playlists_table.php b/database/migrations/2026_05_19_040751_create_music_playlists_table.php new file mode 100644 index 0000000..735c9ef --- /dev/null +++ b/database/migrations/2026_05_19_040751_create_music_playlists_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('category_id')->constrained('music_categories')->onDelete('cascade'); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->foreignId('image_id')->nullable()->constrained('images')->onDelete('set null'); + $table->integer('order')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('music_playlists'); + } +}; diff --git a/database/migrations/2026_05_19_040905_add_colums_to_music_table.php b/database/migrations/2026_05_19_040905_add_colums_to_music_table.php new file mode 100644 index 0000000..5aa9312 --- /dev/null +++ b/database/migrations/2026_05_19_040905_add_colums_to_music_table.php @@ -0,0 +1,32 @@ +foreignId('playlist_id')->nullable()->constrained('music_playlists')->onDelete('cascade'); + $table->string('duration')->nullable(); + $table->integer('order')->default(0); + $table->boolean('is_active')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('music', function (Blueprint $table) { + $table->dropForeign(['playlist_id']); + $table->dropColumn(['playlist_id', 'image_id', 'duration', 'order', 'is_active']); + }); + } +}; diff --git a/database/migrations/2026_05_19_041144_create_music_tags_table.php b/database/migrations/2026_05_19_041144_create_music_tags_table.php new file mode 100644 index 0000000..c1c92e1 --- /dev/null +++ b/database/migrations/2026_05_19_041144_create_music_tags_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('music_id')->constrained()->onDelete('cascade'); + $table->foreignId('tag_id')->constrained()->onDelete('cascade'); + $table->timestamps(); + + $table->unique(['music_id', 'tag_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('music_tags'); + } +}; diff --git a/routes/api.php b/routes/api.php index cd82291..af59e18 100644 --- a/routes/api.php +++ b/routes/api.php @@ -148,6 +148,23 @@ Route::get('/media/{id}/comments', [MediaController::class, 'getComments']); Route::post('/media/{id}/feedback', [MediaController::class, 'submitFeedback']); - + //add note to media Route::post('/media/{id}/note', [MediaController::class, 'storeNote']); + + + + // Music Categories + Route::apiResource('music-categories', MusicCategoryController::class); + Route::get('public/music-categories', [MusicCategoryController::class, 'index']); + + // Music Playlists + Route::apiResource('music-playlists', MusicPlaylistController::class); + Route::get('playlists/by-category/{categoryId}', [MusicPlaylistController::class, 'index']); + + // Music + Route::get('music/playlist/{playlistId}', [MusicController::class, 'getMusicByPlaylist']); + Route::post('music/{musicId}/add-to-playlist', [MusicController::class, 'addToPlaylist']); + Route::delete('music/{musicId}/remove-from-playlist', [MusicController::class, 'removeFromPlaylist']); + Route::post('music/update-order', [MusicController::class, 'updateOrder']); + Route::apiResource('music', MusicController::class); }); \ No newline at end of file