feat: add sub category

This commit is contained in:
2026-05-21 15:18:11 +03:30
parent 2c40ce1e46
commit f3685d4ca9
9 changed files with 358 additions and 34 deletions
+2 -2
View File
@@ -123,7 +123,7 @@ public function store(Request $request)
$data = $request->validate([ $data = $request->validate([
'title' => 'required|string|max:255', 'title' => 'required|string|max:255',
'artist' => 'nullable|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', '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',
@@ -207,7 +207,7 @@ public function update(Request $request, $id)
'title' => 'nullable|string|max:255', 'title' => 'nullable|string|max:255',
'artist' => 'nullable|string|max:255', 'artist' => 'nullable|string|max:255',
'type' => 'nullable|in:public,private', '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', 'image_id' => 'nullable|exists:images,id',
'playlist_id' => 'nullable|exists:music_playlists,id', 'playlist_id' => 'nullable|exists:music_playlists,id',
'duration' => 'nullable|string|regex:/^(?:\d+:)?[0-5]?\d:[0-5]\d$/', 'duration' => 'nullable|string|regex:/^(?:\d+:)?[0-5]?\d:[0-5]\d$/',
@@ -10,10 +10,14 @@ class MusicPlaylistController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
$query = MusicPlaylist::with(['category', 'image']); $query = MusicPlaylist::with(['category', 'subcategory', 'image']);
if ($request->has('category_id')) { if ($request->has('category_id')) {
$query->where('category_id', $request->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(); $playlists = $query->where('is_active', true)->orderBy('order')->get();
@@ -24,7 +28,8 @@ public function index(Request $request)
public function store(Request $request) public function store(Request $request)
{ {
$data = $request->validate([ $data = $request->validate([
'category_id' => 'required|exists:music_categories,id', 'category_id' => 'nullable|exists:music_categories,id',
'subcategory_id' => 'nullable|exists:music_subcategories,id',
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'description' => 'nullable|string', 'description' => 'nullable|string',
'image_id' => 'nullable|exists:images,id', 'image_id' => 'nullable|exists:images,id',
@@ -32,13 +37,20 @@ public function store(Request $request)
'is_active' => 'nullable|boolean', '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);
}
$data['slug'] = Str::slug($data['name']); $data['slug'] = Str::slug($data['name']);
$playlist = MusicPlaylist::create($data); $playlist = MusicPlaylist::create($data);
return response()->json([ return response()->json([
'message' => 'Playlist created successfully', 'message' => 'Playlist created successfully',
'playlist' => $playlist->load(['category', 'image']) 'playlist' => $playlist->load(['category', 'subcategory', 'image'])
], 201); ], 201);
} }
@@ -0,0 +1,117 @@
<?php
namespace App\Http\Controllers;
use App\Models\MusicSubcategory;
use App\Models\MusicCategory;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class MusicSubcategoryController extends Controller
{
public function index(Request $request)
{
$query = MusicSubcategory::with(['category', 'image', 'playlists' => 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']);
}
}
+29
View File
@@ -23,6 +23,21 @@ public function playlists(): HasMany
return $this->hasMany(MusicPlaylist::class, 'category_id'); 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 public function image(): BelongsTo
{ {
return $this->belongsTo(Image::class); return $this->belongsTo(Image::class);
@@ -32,4 +47,18 @@ public function getActivePlaylistsAttribute()
{ {
return $this->playlists()->where('is_active', true)->get(); 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;
}
} }
+7 -1
View File
@@ -10,7 +10,7 @@ class MusicPlaylist extends Model
protected $table = 'music_playlists'; protected $table = 'music_playlists';
protected $fillable = [ 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 = [ protected $casts = [
@@ -23,6 +23,12 @@ public function category(): BelongsTo
return $this->belongsTo(MusicCategory::class, 'category_id'); return $this->belongsTo(MusicCategory::class, 'category_id');
} }
public function subcategory(): BelongsTo
{
return $this->belongsTo(MusicSubcategory::class, 'subcategory_id');
}
public function musics(): HasMany public function musics(): HasMany
{ {
return $this->hasMany(Music::class, 'playlist_id'); return $this->hasMany(Music::class, 'playlist_id');
+85
View File
@@ -0,0 +1,85 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class MusicSubcategory extends Model
{
protected $table = 'music_subcategories';
protected $fillable = [
'category_id', 'name', 'slug', 'description', 'image_id', 'order', 'is_active'
];
protected $casts = [
'is_active' => '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);
}
}
@@ -0,0 +1,36 @@
<?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::create('music_subcategories', function (Blueprint $table) {
$table->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');
}
};
@@ -0,0 +1,33 @@
<?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_playlists', function (Blueprint $table) {
$table->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();
});
}
};
+6
View File
@@ -19,6 +19,7 @@
use App\Http\Controllers\MusicPlaylistController; use App\Http\Controllers\MusicPlaylistController;
use App\Http\Controllers\RatingController; use App\Http\Controllers\RatingController;
use App\Http\Controllers\CommentController; use App\Http\Controllers\CommentController;
use App\Http\Controllers\MusicSubcategoryController;
Route::get('/test-hash', function() { Route::get('/test-hash', function() {
@@ -166,6 +167,10 @@
Route::apiResource('music-playlists', MusicPlaylistController::class); Route::apiResource('music-playlists', MusicPlaylistController::class);
Route::get('playlists/by-category/{categoryId}', [MusicPlaylistController::class, 'index']); 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 // Music Routes - Add this line before your other routes
Route::get('music/all', [MusicController::class, 'getAllMusic']); // For old app compatibility Route::get('music/all', [MusicController::class, 'getAllMusic']); // For old app compatibility
@@ -178,6 +183,7 @@
Route::apiResource('music', MusicController::class); Route::apiResource('music', MusicController::class);
// Generic Rating Routes (works for both music and media) // Generic Rating Routes (works for both music and media)
Route::prefix('ratings')->group(function () { Route::prefix('ratings')->group(function () {
Route::post('{type}/{id}', [RatingController::class, 'rate']); Route::post('{type}/{id}', [RatingController::class, 'rate']);