diff --git a/app/Http/Controllers/MusicController.php b/app/Http/Controllers/MusicController.php index c3554c3..1c7ff2d 100644 --- a/app/Http/Controllers/MusicController.php +++ b/app/Http/Controllers/MusicController.php @@ -123,7 +123,7 @@ public function store(Request $request) $data = $request->validate([ 'title' => 'required|string|max:255', 'artist' => 'nullable|string|max:255', - 'file' => 'required|mimes:mp3,wav,ogg|max:20971520', + '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', @@ -207,7 +207,7 @@ public function update(Request $request, $id) 'title' => 'nullable|string|max:255', 'artist' => 'nullable|string|max:255', 'type' => 'nullable|in:public,private', - 'file' => 'nullable|mimes:mp3,wav,ogg|max:10240', + 'file' => 'nullable|mimes:mp3,wav,ogg,flac|max:20971520', 'image_id' => 'nullable|exists:images,id', 'playlist_id' => 'nullable|exists:music_playlists,id', 'duration' => 'nullable|string|regex:/^(?:\d+:)?[0-5]?\d:[0-5]\d$/', diff --git a/app/Http/Controllers/MusicPlaylistController.php b/app/Http/Controllers/MusicPlaylistController.php index 5e6de30..ee10c48 100644 --- a/app/Http/Controllers/MusicPlaylistController.php +++ b/app/Http/Controllers/MusicPlaylistController.php @@ -8,39 +8,51 @@ class MusicPlaylistController extends Controller { - public function index(Request $request) - { - $query = MusicPlaylist::with(['category', 'image']); - - if ($request->has('category_id')) { - $query->where('category_id', $request->category_id); + public function index(Request $request) + { + $query = MusicPlaylist::with(['category', 'subcategory', 'image']); + + if ($request->has('category_id')) { + $query->where('category_id', $request->category_id)->whereNull('subcategory_id'); + } + + if ($request->has('subcategory_id')) { + $query->where('subcategory_id', $request->subcategory_id); + } + + $playlists = $query->where('is_active', true)->orderBy('order')->get(); + + return response()->json($playlists); } - - $playlists = $query->where('is_active', true)->orderBy('order')->get(); - - return response()->json($playlists); + + public function store(Request $request) +{ + $data = $request->validate([ + 'category_id' => 'nullable|exists:music_categories,id', + 'subcategory_id' => 'nullable|exists:music_subcategories,id', + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'image_id' => 'nullable|exists:images,id', + 'order' => 'nullable|integer', + 'is_active' => 'nullable|boolean', + ]); + + // Ensure either category_id or subcategory_id is provided + if (!$data['category_id'] && !$data['subcategory_id']) { + return response()->json([ + 'message' => 'Either category_id or subcategory_id is required' + ], 422); } - 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); - } + $data['slug'] = Str::slug($data['name']); + + $playlist = MusicPlaylist::create($data); + + return response()->json([ + 'message' => 'Playlist created successfully', + 'playlist' => $playlist->load(['category', 'subcategory', 'image']) + ], 201); +} public function show($id) { diff --git a/app/Http/Controllers/MusicSubcategoryController.php b/app/Http/Controllers/MusicSubcategoryController.php new file mode 100644 index 0000000..9d3f5b6 --- /dev/null +++ b/app/Http/Controllers/MusicSubcategoryController.php @@ -0,0 +1,117 @@ + function($q) { + $q->where('is_active', true)->orderBy('order'); + }]); + + if ($request->has('category_id')) { + $query->where('category_id', $request->category_id); + } + + $subcategories = $query->where('is_active', true)->orderBy('order')->get(); + + return response()->json($subcategories); + } + + public function store(Request $request) + { + try { + $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', + ]); + + $slug = Str::slug($data['name']); + + // Check for duplicate in same category + $existing = MusicSubcategory::where('category_id', $data['category_id']) + ->where('slug', $slug) + ->first(); + + if ($existing) { + return response()->json([ + 'message' => 'A subcategory with this name already exists in this category', + 'errors' => ['name' => ['The subcategory name must be unique within this category.']] + ], 422); + } + + $data['slug'] = $slug; + $subcategory = MusicSubcategory::create($data); + + return response()->json([ + 'message' => 'Subcategory created successfully', + 'subcategory' => $subcategory->load(['category', 'image']) + ], 201); + + } catch (\Exception $e) { + return response()->json([ + 'message' => 'An error occurred while creating the subcategory', + 'error' => $e->getMessage() + ], 500); + } + } + + public function show($id) + { + $subcategory = MusicSubcategory::with([ + 'category', + '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($subcategory); + } + + public function update(Request $request, $id) + { + $subcategory = MusicSubcategory::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']); + } + + $subcategory->update($data); + + return response()->json([ + 'message' => 'Subcategory updated successfully', + 'subcategory' => $subcategory->load(['category', 'image']) + ]); + } + + public function destroy($id) + { + $subcategory = MusicSubcategory::findOrFail($id); + $subcategory->delete(); + + return response()->json(['message' => 'Subcategory deleted successfully']); + } +} diff --git a/app/Models/MusicCategory.php b/app/Models/MusicCategory.php index aefa7ba..5387cf9 100644 --- a/app/Models/MusicCategory.php +++ b/app/Models/MusicCategory.php @@ -22,7 +22,22 @@ public function playlists(): HasMany { return $this->hasMany(MusicPlaylist::class, 'category_id'); } - + + public function subcategories(): HasMany + { + return $this->hasMany(MusicSubcategory::class, 'category_id'); + } + // All playlists (including those in subcategories) + public function allPlaylists() + { + $playlists = collect($this->playlists); + + foreach ($this->subcategories as $subcategory) { + $playlists = $playlists->merge($subcategory->playlists); + } + + return $playlists; + } public function image(): BelongsTo { return $this->belongsTo(Image::class); @@ -32,4 +47,18 @@ public function getActivePlaylistsAttribute() { return $this->playlists()->where('is_active', true)->get(); } + + // Total music count across all playlists and subcategories + public function getTotalMusicCountAttribute() + { + $count = $this->playlists->sum(function($playlist) { + return $playlist->musics->count(); + }); + + foreach ($this->subcategories as $subcategory) { + $count += $subcategory->total_music_count; + } + + return $count; + } } diff --git a/app/Models/MusicPlaylist.php b/app/Models/MusicPlaylist.php index f2491be..7e60325 100644 --- a/app/Models/MusicPlaylist.php +++ b/app/Models/MusicPlaylist.php @@ -10,7 +10,7 @@ class MusicPlaylist extends Model protected $table = 'music_playlists'; protected $fillable = [ - 'category_id', 'name', 'slug', 'description', 'image_id', 'order', 'is_active' + 'category_id', 'subcategory_id','name', 'slug', 'description', 'image_id', 'order', 'is_active' ]; protected $casts = [ @@ -23,6 +23,12 @@ public function category(): BelongsTo return $this->belongsTo(MusicCategory::class, 'category_id'); } + public function subcategory(): BelongsTo + { + return $this->belongsTo(MusicSubcategory::class, 'subcategory_id'); + } + + public function musics(): HasMany { return $this->hasMany(Music::class, 'playlist_id'); diff --git a/app/Models/MusicSubcategory.php b/app/Models/MusicSubcategory.php new file mode 100644 index 0000000..41103ad --- /dev/null +++ b/app/Models/MusicSubcategory.php @@ -0,0 +1,85 @@ + 'boolean', + 'order' => 'integer', + ]; + + public function category(): BelongsTo + { + return $this->belongsTo(MusicCategory::class, 'category_id'); + } + + public function playlists(): HasMany + { + return $this->hasMany(MusicPlaylist::class, 'subcategory_id'); + } + + public function image(): BelongsTo + { + return $this->belongsTo(Image::class); + } + + public function getActivePlaylistsAttribute() + { + return $this->playlists()->where('is_active', true)->orderBy('order')->get(); + } + + // Get total music count across all playlists in this subcategory + public function getTotalMusicCountAttribute() + { + return $this->playlists() + ->withCount('musics') + ->get() + ->sum('musics_count'); + } + + // Get total duration across all music in this subcategory + public function getTotalDurationAttribute() + { + $totalSeconds = 0; + foreach ($this->playlists as $playlist) { + foreach ($playlist->musics as $music) { + $totalSeconds += $this->durationToSeconds($music->duration); + } + } + return $this->secondsToDuration($totalSeconds); + } + + private function durationToSeconds($duration) + { + if (!$duration) return 0; + $parts = explode(':', $duration); + if (count($parts) === 2) { + return (int)$parts[0] * 60 + (int)$parts[1]; + } elseif (count($parts) === 3) { + return (int)$parts[0] * 3600 + (int)$parts[1] * 60 + (int)$parts[2]; + } + return 0; + } + + private function secondsToDuration($seconds) + { + $hours = floor($seconds / 3600); + $minutes = floor(($seconds % 3600) / 60); + $secs = $seconds % 60; + + if ($hours > 0) { + return sprintf("%d:%02d:%02d", $hours, $minutes, $secs); + } + return sprintf("%d:%02d", $minutes, $secs); + } +} diff --git a/database/migrations/2026_05_21_105404_create_music_subcategories_table.php b/database/migrations/2026_05_21_105404_create_music_subcategories_table.php new file mode 100644 index 0000000..06581ec --- /dev/null +++ b/database/migrations/2026_05_21_105404_create_music_subcategories_table.php @@ -0,0 +1,36 @@ +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(); + + $table->index(['category_id', 'order']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('music_subcategories'); + } +}; diff --git a/database/migrations/2026_05_21_105523_add_subcategory_to_playlists.php b/database/migrations/2026_05_21_105523_add_subcategory_to_playlists.php new file mode 100644 index 0000000..74fcac8 --- /dev/null +++ b/database/migrations/2026_05_21_105523_add_subcategory_to_playlists.php @@ -0,0 +1,33 @@ +foreignId('subcategory_id')->nullable()->after('category_id') + ->constrained('music_subcategories')->onDelete('cascade'); + // Make category_id nullable since playlist can belong to subcategory + $table->foreignId('category_id')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('music_playlists', function (Blueprint $table) { + $table->dropForeign(['subcategory_id']); + $table->dropColumn('subcategory_id'); + $table->foreignId('category_id')->nullable(false)->change(); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index 7ccb2d7..3794d20 100644 --- a/routes/api.php +++ b/routes/api.php @@ -19,6 +19,7 @@ use App\Http\Controllers\MusicPlaylistController; use App\Http\Controllers\RatingController; use App\Http\Controllers\CommentController; +use App\Http\Controllers\MusicSubcategoryController; Route::get('/test-hash', function() { @@ -166,6 +167,10 @@ Route::apiResource('music-playlists', MusicPlaylistController::class); Route::get('playlists/by-category/{categoryId}', [MusicPlaylistController::class, 'index']); + // Subcategory routes + Route::apiResource('music-subcategories', MusicSubcategoryController::class); + Route::get('subcategories/by-category/{categoryId}', [MusicSubcategoryController::class, 'index']); + // Music Routes - Add this line before your other routes Route::get('music/all', [MusicController::class, 'getAllMusic']); // For old app compatibility @@ -177,6 +182,7 @@ Route::post('music/update-order', [MusicController::class, 'updateOrder']); Route::apiResource('music', MusicController::class); + // Generic Rating Routes (works for both music and media) Route::prefix('ratings')->group(function () {