From d90e7641f200895e33fc3ccc255f825f111a1390 Mon Sep 17 00:00:00 2001 From: Phan An Date: Mon, 4 Jul 2022 12:39:02 +0200 Subject: [PATCH] feat: better playlist handling --- .../V6/API/PlaylistSongController.php | 23 +++++++++++++++++++ .../V6/Requests/AddSongsToPlaylistRequest.php | 20 ++++++++++++++++ .../RemoveSongsFromPlaylistRequest.php | 20 ++++++++++++++++ app/Services/PlaylistService.php | 10 ++++++++ .../playlist/PlaylistSidebarItem.vue | 2 +- .../js/components/screens/PlaylistScreen.vue | 2 +- .../js/composables/useSongMenuMethods.ts | 4 ++-- resources/assets/js/stores/playlistStore.ts | 23 +++++++++++-------- routes/api.v6.php | 6 +++++ 9 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 app/Http/Controllers/V6/Requests/AddSongsToPlaylistRequest.php create mode 100644 app/Http/Controllers/V6/Requests/RemoveSongsFromPlaylistRequest.php diff --git a/app/Http/Controllers/V6/API/PlaylistSongController.php b/app/Http/Controllers/V6/API/PlaylistSongController.php index bbb3e771..987cd9e0 100644 --- a/app/Http/Controllers/V6/API/PlaylistSongController.php +++ b/app/Http/Controllers/V6/API/PlaylistSongController.php @@ -3,18 +3,23 @@ namespace App\Http\Controllers\V6\API; use App\Http\Controllers\API\Controller; +use App\Http\Controllers\V6\Requests\AddSongsToPlaylistRequest; +use App\Http\Controllers\V6\Requests\RemoveSongsFromPlaylistRequest; use App\Http\Resources\SongResource; use App\Models\Playlist; use App\Models\User; use App\Repositories\SongRepository; +use App\Services\PlaylistService; use App\Services\SmartPlaylistService; use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Http\Response; class PlaylistSongController extends Controller { /** @param User $user */ public function __construct( private SongRepository $songRepository, + private PlaylistService $playlistService, private SmartPlaylistService $smartPlaylistService, private ?Authenticatable $user ) { @@ -30,4 +35,22 @@ class PlaylistSongController extends Controller : $this->songRepository->getByStandardPlaylist($playlist, $this->user) ); } + + public function add(Playlist $playlist, AddSongsToPlaylistRequest $request) + { + $this->authorize('owner', $playlist); + + abort_if($playlist->is_smart, Response::HTTP_FORBIDDEN); + + $this->playlistService->addSongsToPlaylist($playlist, $request->songs); + } + + public function remove(Playlist $playlist, RemoveSongsFromPlaylistRequest $request) + { + $this->authorize('owner', $playlist); + + abort_if($playlist->is_smart, Response::HTTP_FORBIDDEN); + + $this->playlistService->removeSongsFromPlaylist($playlist, $request->songs); + } } diff --git a/app/Http/Controllers/V6/Requests/AddSongsToPlaylistRequest.php b/app/Http/Controllers/V6/Requests/AddSongsToPlaylistRequest.php new file mode 100644 index 00000000..b93a95b4 --- /dev/null +++ b/app/Http/Controllers/V6/Requests/AddSongsToPlaylistRequest.php @@ -0,0 +1,20 @@ + songs + */ +class AddSongsToPlaylistRequest extends Request +{ + /** @return array */ + public function rules(): array + { + return [ + 'songs' => 'required|array', + 'songs.*' => 'exists:songs,id', + ]; + } +} diff --git a/app/Http/Controllers/V6/Requests/RemoveSongsFromPlaylistRequest.php b/app/Http/Controllers/V6/Requests/RemoveSongsFromPlaylistRequest.php new file mode 100644 index 00000000..2ce3ad41 --- /dev/null +++ b/app/Http/Controllers/V6/Requests/RemoveSongsFromPlaylistRequest.php @@ -0,0 +1,20 @@ + songs + */ +class RemoveSongsFromPlaylistRequest extends Request +{ + /** @return array */ + public function rules(): array + { + return [ + 'songs' => 'required|array', + 'songs.*' => 'exists:songs,id', + ]; + } +} diff --git a/app/Services/PlaylistService.php b/app/Services/PlaylistService.php index 6a7c5d98..220f6a91 100644 --- a/app/Services/PlaylistService.php +++ b/app/Services/PlaylistService.php @@ -21,4 +21,14 @@ class PlaylistService return $playlist; } + + public function addSongsToPlaylist(Playlist $playlist, array $songIds): void + { + $playlist->songs()->syncWithoutDetaching($songIds); + } + + public function removeSongsFromPlaylist(Playlist $playlist, array $songIds): void + { + $playlist->songs()->detach($songIds); + } } diff --git a/resources/assets/js/components/playlist/PlaylistSidebarItem.vue b/resources/assets/js/components/playlist/PlaylistSidebarItem.vue index 3e91b405..c514fe02 100644 --- a/resources/assets/js/components/playlist/PlaylistSidebarItem.vue +++ b/resources/assets/js/components/playlist/PlaylistSidebarItem.vue @@ -94,7 +94,7 @@ const handleDrop = async (event: DragEvent) => { if (type.value === 'favorites') { await favoriteStore.like(songs) } else if (type.value === 'playlist') { - await playlistStore.addSongs(playlist, songs) + await playlistStore.addSongs(playlist.value, songs) alerts.success(`Added ${pluralize(songs.length, 'song')} into "${playlist.value.name}."`) } diff --git a/resources/assets/js/components/screens/PlaylistScreen.vue b/resources/assets/js/components/screens/PlaylistScreen.vue index e9849b84..9901c7e5 100644 --- a/resources/assets/js/components/screens/PlaylistScreen.vue +++ b/resources/assets/js/components/screens/PlaylistScreen.vue @@ -96,7 +96,7 @@ const download = () => downloadService.fromPlaylist(playlist.value!) const editSmartPlaylist = () => eventBus.emit('MODAL_SHOW_EDIT_SMART_PLAYLIST_FORM', playlist.value) const removeSelected = () => { - if (!selectedSongs.value.length) return + if (!selectedSongs.value.length || playlist.value.is_smart) return playlistStore.removeSongs(playlist.value!, selectedSongs.value) songs.value = difference(songs.value, selectedSongs.value) diff --git a/resources/assets/js/composables/useSongMenuMethods.ts b/resources/assets/js/composables/useSongMenuMethods.ts index f08b5795..3c58df4e 100644 --- a/resources/assets/js/composables/useSongMenuMethods.ts +++ b/resources/assets/js/composables/useSongMenuMethods.ts @@ -1,4 +1,4 @@ -import { ref, Ref } from 'vue' +import { Ref } from 'vue' import { favoriteStore, playlistStore, queueStore } from '@/stores' import { alerts, pluralize } from '@/utils' @@ -25,7 +25,7 @@ export const useSongMenuMethods = (songs: Ref, close: Closure) => { const addSongsToExistingPlaylist = async (playlist: Playlist) => { close() - await playlistStore.addSongs(ref(playlist), songs.value) + await playlistStore.addSongs(playlist, songs.value) alerts.success(`Added ${pluralize(songs.value.length, 'song')} into "${playlist.name}."`) } diff --git a/resources/assets/js/stores/playlistStore.ts b/resources/assets/js/stores/playlistStore.ts index 8a06ccb9..54365665 100644 --- a/resources/assets/js/stores/playlistStore.ts +++ b/resources/assets/js/stores/playlistStore.ts @@ -1,9 +1,10 @@ import { difference, orderBy, union } from 'lodash' -import { reactive, Ref } from 'vue' +import { reactive } from 'vue' import { httpService } from '@/services' import models from '@/config/smart-playlist/models' import operators from '@/config/smart-playlist/operators' +import { Cache } from '@/services/cache' export const playlistStore = { state: reactive({ @@ -54,23 +55,24 @@ export const playlistStore = { }, async delete (playlist: Playlist) { - await httpService.delete(`playlist/${playlist.id}`) + await httpService.delete(`playlists/${playlist.id}`) this.state.playlists = difference(this.state.playlists, [playlist]) }, - async addSongs (playlist: Ref, songs: Song[]) { - if (playlist.value.is_smart) { + async addSongs (playlist: Playlist, songs: Song[]) { + if (playlist.is_smart) { return playlist } - const count = playlist.value.songs.length - playlist.value.songs = union(playlist.value.songs, songs) + const count = playlist.songs.length + playlist.songs = union(playlist.songs, songs) - if (count === playlist.value.songs.length) { + if (count === playlist.songs.length) { return playlist } - await httpService.put(`playlist/${playlist.value.id}/sync`, { songs: playlist.value.songs.map(song => song.id) }) + await httpService.post(`playlists/${playlist.id}/songs`, { songs: songs.map(song => song.id) }) + Cache.invalidate(['playlist.songs', playlist.id]) return playlist }, @@ -81,14 +83,15 @@ export const playlistStore = { } playlist.songs = difference(playlist.songs, songs) - await httpService.put(`playlist/${playlist.id}/sync`, { songs: playlist.songs.map(song => song.id) }) + await httpService.delete(`playlists/${playlist.id}/songs`, { songs: songs.map(song => song.id) }) + Cache.invalidate(['playlist.songs', playlist.id]) return playlist }, async update (playlist: Playlist) { const serializedRules = this.serializeSmartPlaylistRulesForStorage(playlist.rules) - await httpService.put(`playlist/${playlist.id}`, { name: playlist.name, rules: serializedRules }) + await httpService.put(`playlists/${playlist.id}`, { name: playlist.name, rules: serializedRules }) return playlist }, diff --git a/routes/api.v6.php b/routes/api.v6.php index 1e23ac91..94e4e271 100644 --- a/routes/api.v6.php +++ b/routes/api.v6.php @@ -1,6 +1,7 @@ middleware('api')->group(static function (): void { Route::apiResource('albums.songs', AlbumSongController::class); Route::apiResource('artists', ArtistController::class); Route::apiResource('artists.songs', ArtistSongController::class); + + Route::apiResource('playlists', PlaylistController::class); Route::apiResource('playlists.songs', PlaylistSongController::class); + Route::post('playlists/{playlist}/songs', [PlaylistSongController::class, 'add']); + Route::delete('playlists/{playlist}/songs', [PlaylistSongController::class, 'remove']); + Route::apiResource('songs', SongController::class); Route::get('users', [UserController::class, 'index']);