id(); $music = Music::with(['image', 'playlists']) ->where('type', 'public') ->orWhere(function($query) use ($userId) { $query->where('type', 'private') ->where('user_id', $userId); }) ->orderBy('created_at', 'desc') ->get(); // Return as array directly (not wrapped in 'data' object) // to match what your old Flutter app expects return response()->json($music); } public function index() { $userId = auth()->id(); $music = Music::with(['image', 'playlists']) ->where('type', 'public') ->orWhere(function($query) use ($userId) { $query->where('type', 'private') ->where('user_id', $userId); }) ->orderBy('created_at', 'desc') ->get(); return response()->json([ 'data' => $music, 'total' => $music->count() ]); } public function getMusicByPlaylist($playlistId) { $playlist = MusicPlaylist::findOrFail($playlistId); $music = $playlist->musics() ->where('music.is_active', true) ->with(['image', 'tags']) ->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) ->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', ]); $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', 'playlists']) ]); } public function removeFromPlaylist(Request $request, $musicId) { $music = Music::where('id', $musicId) ->where('user_id', auth()->id()) ->firstOrFail(); $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) { $playlist->musics()->updateExistingPivot($item['id'], [ 'order' => $item['order'], ]); } return response()->json(['message' => 'Order updated successfully']); } // ✅ Upload music public function store(Request $request) { try { $data = $request->validate([ 'title' => 'required|string|max:255', 'artist' => 'nullable|string|max:255', 'file' => 'required|file|extensions:mp3,wav,ogg,flac,aac,m4a,opus,mpga,wma|max:20971520', 'type' => 'nullable|in:public,private', 'image_id' => 'nullable|exists:images,id', 'image' => 'nullable|image|max:8192', '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 ]); // Handle file upload if (!$request->hasFile('file')) { return response()->json([ 'message' => 'No file was uploaded' ], 400); } $file = $request->file('file'); // Validate file size if ($file->getSize() > 20971520) { return response()->json([ 'message' => 'File size exceeds 10MB limit' ], 422); } $path = $this->storeUpload($file, 'music'); if (!$path) { return response()->json([ 'message' => 'Failed to store the file' ], 500); } // An uploaded image file takes precedence over a provided image_id. $imageId = $this->uploadedImageId($request) ?? ($data['image_id'] ?? null); $music = Music::create([ 'user_id' => auth()->id(), 'title' => $data['title'], 'artist' => $data['artist'] ?? null, 'file_path' => $path, 'type' => $data['type'] ?? 'private', 'image_id' => $imageId, 'duration' => $data['duration'] ?? null, // Store as string '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', 'playlists', 'tags']), 'url' => asset('storage/' . $path), ], 201); } catch (\Illuminate\Validation\ValidationException $e) { return response()->json([ 'message' => 'Validation failed', 'errors' => $e->errors() ], 422); } catch (\Exception $e) { return response()->json([ 'message' => 'An error occurred while uploading the music', 'error' => $e->getMessage() ], 500); } } // Helper method to get next order number in playlist private function getNextOrderInPlaylist($playlistId) { if (!$playlistId) { return 0; } $maxOrder = \DB::table('music_playlist') ->where('playlist_id', $playlistId) ->max('order'); return ($maxOrder ?? -1) + 1; } // ✅ Update music with string duration public function update(Request $request, $id) { try { $music = Music::where('id', $id)->where('user_id', auth()->id())->firstOrFail(); $data = $request->validate([ 'title' => 'nullable|string|max:255', 'artist' => 'nullable|string|max:255', 'type' => 'nullable|in:public,private', 'file' => 'nullable|file|extensions:mp3,wav,ogg,flac,aac,m4a,opus,mpga,wma|max:20971520', 'image_id' => 'nullable|exists:images,id', 'image' => 'nullable|image|max:8192', 'duration' => 'nullable|integer|min:1', 'is_active' => 'nullable|boolean', ]); if ($request->hasFile('file')) { // Delete old file Storage::disk('public')->delete($music->file_path); $path = $this->storeUpload($request->file('file'), 'music'); $music->file_path = $path; } // Update only provided fields (drop the raw file input from mass-assign). $music->fill(collect($data)->except('image')->toArray()); // An uploaded image file takes precedence over a provided image_id. $uploadedImageId = $this->uploadedImageId($request); if ($uploadedImageId !== null) { $music->image_id = $uploadedImageId; } $music->save(); return response()->json([ 'message' => 'Music updated successfully', 'music' => $music->load(['image', 'playlists', 'tags']), 'url' => asset('storage/' . $music->file_path), ]); } catch (\Exception $e) { return response()->json([ 'message' => 'An error occurred while updating the music', 'error' => $e->getMessage() ], 500); } } // ✅ Get all public + user private music public function show($id) { $userId = auth()->id(); $music = Music::with([ 'image', 'playlists', 'tags', 'comments' => function($query) { $query->with('user')->latest()->limit(10); }, 'ratings' ]) ->where(function($query) use ($userId) { $query->where('type', 'public') ->orWhere(function($q) use ($userId) { $q->where('type', 'private') ->where('user_id', $userId); }); }) ->findOrFail($id); // Get user's specific comment $userComment = $music->userComment(); // Get paginated comments for the response $comments = $music->comments() ->with('user') ->latest() ->paginate(15); return response()->json([ 'music' => $music, 'statistics' => [ 'average_rating' => $music->average_rating, 'total_ratings' => $music->ratings_count, 'total_comments' => $music->comments_count, 'rating_distribution' => $music->rating_distribution, ], 'user_interaction' => [ 'has_rated' => $music->has_user_rated, 'user_rating' => $music->user_rating, 'has_commented' => $music->has_user_commented, 'user_comment' => $music->user_comment, 'user_comment_id' => $music->user_comment_id, ], 'comments' => $comments, ]); } // ✅ Delete music public function destroy($id) { $music = Music::where('id', $id)->where('user_id', auth()->id())->firstOrFail(); Storage::disk('public')->delete($music->file_path); $music->delete(); return response()->json(['message' => 'Music deleted successfully']); } }