From de88d23b9564d2f7b496b9cd141fc9e648b7feeb Mon Sep 17 00:00:00 2001 From: Phan An Date: Mon, 5 Jun 2023 23:46:41 +0200 Subject: [PATCH] chore: merge v6 into base API (#1685) --- .../{V6 => }/API/AlbumController.php | 2 +- .../{V6 => }/API/AlbumSongController.php | 2 +- .../{V6 => }/API/ArtistAlbumController.php | 2 +- .../{V6 => }/API/ArtistController.php | 2 +- .../{V6 => }/API/ArtistSongController.php | 2 +- app/Http/Controllers/API/DataController.php | 53 +++---- .../{V6 => }/API/ExcerptSearchController.php | 6 +- .../{V6 => }/API/FavoriteSongController.php | 2 +- .../API/FetchAlbumInformationController.php | 2 +- .../API/FetchArtistInformationController.php | 2 +- .../API/FetchRandomSongsInGenreController.php | 4 +- .../{V6 => }/API/GenreController.php | 2 +- .../{V6 => }/API/GenreSongController.php | 4 +- .../API/Interaction/PlayCountController.php | 3 +- .../API/MediaInformation/AlbumController.php | 19 --- .../API/MediaInformation/ArtistController.php | 19 --- .../API/MediaInformation/SongController.php | 27 ---- .../{V6 => }/API/OverviewController.php | 2 +- .../Controllers/API/PlaylistController.php | 39 +++-- .../{V6 => }/API/PlaylistFolderController.php | 6 +- .../API/PlaylistFolderPlaylistController.php | 6 +- .../API/PlaylistSongController.php | 33 ++-- .../{V6 => }/API/QueueController.php | 4 +- .../API/RecentlyPlayedSongController.php | 2 +- .../API/Search/ExcerptSearchController.php | 27 ---- .../API/Search/SongSearchController.php | 24 --- app/Http/Controllers/API/SongController.php | 36 ++++- .../{V6 => }/API/SongSearchController.php | 6 +- .../Controllers/V6/API/DataController.php | 52 ------ .../V6/API/DeleteSongsController.php | 20 --- .../V6/API/PlayCountController.php | 27 ---- .../Controllers/V6/API/PlaylistController.php | 90 ----------- .../V6/API/PlaylistSongController.php | 60 ------- .../Controllers/V6/API/SongController.php | 35 ---- .../API/AddSongsToPlaylistRequest.php | 3 +- .../{V6 => }/API/DeleteSongsRequest.php | 4 +- .../API/FetchRandomSongsInGenreRequest.php | 4 +- .../{V6 => }/API/GenreFetchSongRequest.php | 4 +- .../PlaylistFolderPlaylistDestroyRequest.php | 3 +- .../PlaylistFolderPlaylistStoreRequest.php | 3 +- .../API/PlaylistFolderStoreRequest.php | 4 +- .../API/PlaylistFolderUpdateRequest.php | 4 +- .../{V6 => }/API/QueueFetchSongRequest.php | 3 +- .../API/RemoveSongsFromPlaylistRequest.php | 4 +- .../Requests/{V6 => }/API/SearchRequest.php | 4 +- .../Requests/{V6 => }/API/SongListRequest.php | 4 +- .../Requests/API/YouTubeSearchRequest.php | 2 +- .../Requests/V6/API/YouTubeSearchRequest.php | 12 -- app/Services/SearchService.php | 56 ++++--- app/Services/V6/SearchService.php | 57 ------- routes/api.base.php | 86 ++++++---- routes/api.v6.php | 74 --------- .../Feature/{V6 => }/AlbumInformationTest.php | 2 +- tests/Feature/{V6 => }/AlbumSongTest.php | 2 +- tests/Feature/{V6 => }/AlbumTest.php | 2 +- tests/Feature/{V6 => }/ArtistAlbumTest.php | 2 +- .../{V6 => }/ArtistInformationTest.php | 2 +- tests/Feature/{V6 => }/ArtistSongTest.php | 2 +- tests/Feature/{V6 => }/ArtistTest.php | 2 +- tests/Feature/{V6 => }/DataTest.php | 2 +- tests/Feature/DownloadTest.php | 8 +- tests/Feature/{V6 => }/ExcerptSearchTest.php | 2 +- tests/Feature/{V6 => }/FavoriteSongTest.php | 2 +- tests/Feature/{V6 => }/GenreTest.php | 4 +- tests/Feature/InteractionTest.php | 2 +- tests/Feature/{V6 => }/OverviewTest.php | 2 +- tests/Feature/{V6 => }/PlayCountTest.php | 2 +- tests/Feature/{V6 => }/PlaylistFolderTest.php | 2 +- tests/Feature/PlaylistSongTest.php | 134 +++++++++++++--- tests/Feature/PlaylistTest.php | 55 ++++--- tests/Feature/{V6 => }/QueueTest.php | 2 +- .../{V6 => }/RecentlyPlayedSongTest.php | 2 +- tests/Feature/ScrobbleTest.php | 2 +- tests/Feature/{V6 => }/SongSearchTest.php | 2 +- tests/Feature/SongTest.php | 101 +++++++++++- tests/Feature/V6/PlaylistSongTest.php | 145 ----------------- tests/Feature/V6/PlaylistTest.php | 149 ------------------ tests/Feature/V6/SongTest.php | 92 ----------- tests/Feature/V6/TestCase.php | 15 -- .../Services/InteractionServiceTest.php | 5 +- tests/TestCase.php | 11 +- .../Services/PlaylistFolderServiceTest.php | 6 +- tests/Unit/Services/SpotifyServiceTest.php | 5 +- 83 files changed, 512 insertions(+), 1207 deletions(-) rename app/Http/Controllers/{V6 => }/API/AlbumController.php (92%) rename app/Http/Controllers/{V6 => }/API/AlbumSongController.php (93%) rename app/Http/Controllers/{V6 => }/API/ArtistAlbumController.php (91%) rename app/Http/Controllers/{V6 => }/API/ArtistController.php (93%) rename app/Http/Controllers/{V6 => }/API/ArtistSongController.php (93%) rename app/Http/Controllers/{V6 => }/API/ExcerptSearchController.php (79%) rename app/Http/Controllers/{V6 => }/API/FavoriteSongController.php (92%) rename app/Http/Controllers/{V6 => }/API/FetchAlbumInformationController.php (90%) rename app/Http/Controllers/{V6 => }/API/FetchArtistInformationController.php (90%) rename app/Http/Controllers/{V6 => }/API/FetchRandomSongsInGenreController.php (84%) rename app/Http/Controllers/{V6 => }/API/GenreController.php (93%) rename app/Http/Controllers/{V6 => }/API/GenreSongController.php (89%) delete mode 100644 app/Http/Controllers/API/MediaInformation/AlbumController.php delete mode 100644 app/Http/Controllers/API/MediaInformation/ArtistController.php delete mode 100644 app/Http/Controllers/API/MediaInformation/SongController.php rename app/Http/Controllers/{V6 => }/API/OverviewController.php (96%) rename app/Http/Controllers/{V6 => }/API/PlaylistFolderController.php (87%) rename app/Http/Controllers/{V6 => }/API/PlaylistFolderPlaylistController.php (83%) rename app/Http/Controllers/{V6 => }/API/QueueController.php (90%) rename app/Http/Controllers/{V6 => }/API/RecentlyPlayedSongController.php (93%) delete mode 100644 app/Http/Controllers/API/Search/ExcerptSearchController.php delete mode 100644 app/Http/Controllers/API/Search/SongSearchController.php rename app/Http/Controllers/{V6 => }/API/SongSearchController.php (78%) delete mode 100644 app/Http/Controllers/V6/API/DataController.php delete mode 100644 app/Http/Controllers/V6/API/DeleteSongsController.php delete mode 100644 app/Http/Controllers/V6/API/PlayCountController.php delete mode 100644 app/Http/Controllers/V6/API/PlaylistController.php delete mode 100644 app/Http/Controllers/V6/API/PlaylistSongController.php delete mode 100644 app/Http/Controllers/V6/API/SongController.php rename app/Http/Requests/{V6 => }/API/AddSongsToPlaylistRequest.php (83%) rename app/Http/Requests/{V6 => }/API/DeleteSongsRequest.php (78%) rename app/Http/Requests/{V6 => }/API/FetchRandomSongsInGenreRequest.php (65%) rename app/Http/Requests/{V6 => }/API/GenreFetchSongRequest.php (63%) rename app/Http/Requests/{V6 => }/API/PlaylistFolderPlaylistDestroyRequest.php (87%) rename app/Http/Requests/{V6 => }/API/PlaylistFolderPlaylistStoreRequest.php (87%) rename app/Http/Requests/{V6 => }/API/PlaylistFolderStoreRequest.php (78%) rename app/Http/Requests/{V6 => }/API/PlaylistFolderUpdateRequest.php (78%) rename app/Http/Requests/{V6 => }/API/QueueFetchSongRequest.php (89%) rename app/Http/Requests/{V6 => }/API/RemoveSongsFromPlaylistRequest.php (80%) rename app/Http/Requests/{V6 => }/API/SearchRequest.php (73%) rename app/Http/Requests/{V6 => }/API/SongListRequest.php (62%) delete mode 100644 app/Http/Requests/V6/API/YouTubeSearchRequest.php delete mode 100644 app/Services/V6/SearchService.php delete mode 100644 routes/api.v6.php rename tests/Feature/{V6 => }/AlbumInformationTest.php (98%) rename tests/Feature/{V6 => }/AlbumSongTest.php (93%) rename tests/Feature/{V6 => }/AlbumTest.php (97%) rename tests/Feature/{V6 => }/ArtistAlbumTest.php (93%) rename tests/Feature/{V6 => }/ArtistInformationTest.php (98%) rename tests/Feature/{V6 => }/ArtistSongTest.php (93%) rename tests/Feature/{V6 => }/ArtistTest.php (97%) rename tests/Feature/{V6 => }/DataTest.php (95%) rename tests/Feature/{V6 => }/ExcerptSearchTest.php (96%) rename tests/Feature/{V6 => }/FavoriteSongTest.php (93%) rename tests/Feature/{V6 => }/GenreTest.php (98%) rename tests/Feature/{V6 => }/OverviewTest.php (96%) rename tests/Feature/{V6 => }/PlayCountTest.php (98%) rename tests/Feature/{V6 => }/PlaylistFolderTest.php (98%) rename tests/Feature/{V6 => }/QueueTest.php (94%) rename tests/Feature/{V6 => }/RecentlyPlayedSongTest.php (93%) rename tests/Feature/{V6 => }/SongSearchTest.php (91%) delete mode 100644 tests/Feature/V6/PlaylistSongTest.php delete mode 100644 tests/Feature/V6/PlaylistTest.php delete mode 100644 tests/Feature/V6/SongTest.php delete mode 100644 tests/Feature/V6/TestCase.php diff --git a/app/Http/Controllers/V6/API/AlbumController.php b/app/Http/Controllers/API/AlbumController.php similarity index 92% rename from app/Http/Controllers/V6/API/AlbumController.php rename to app/Http/Controllers/API/AlbumController.php index 1f2ffff3..d409fbcf 100644 --- a/app/Http/Controllers/V6/API/AlbumController.php +++ b/app/Http/Controllers/API/AlbumController.php @@ -1,6 +1,6 @@ json($this->mediaCacheService->get() + [ - 'settings' => $this->currentUser->is_admin ? $this->settingRepository->getAllAsKeyValueArray() : [], - 'playlists' => $this->playlistRepository->getAllByCurrentUser(), - 'interactions' => $this->interactionRepository->getAllByCurrentUser(), - 'recentlyPlayed' => $this->interactionRepository->getRecentlyPlayed( - $this->currentUser, - self::RECENTLY_PLAYED_EXCERPT_COUNT - ), - 'users' => $this->currentUser->is_admin ? $this->userRepository->getAll() : [], - 'currentUser' => $this->currentUser, - 'useLastfm' => LastfmService::used(), - 'useYouTube' => YouTubeService::enabled(), - 'useiTunes' => ITunesService::used(), - 'allowDownload' => config('koel.download.allow'), - 'supportsTranscoding' => config('koel.streaming.ffmpeg_path') + return response()->json([ + 'settings' => $this->user->is_admin ? $this->settingRepository->getAllAsKeyValueArray() : [], + 'playlists' => PlaylistResource::collection($this->user->playlists), + 'playlist_folders' => PlaylistFolderResource::collection($this->user->playlist_folders), + 'current_user' => UserResource::make($this->user, true), + 'use_last_fm' => LastfmService::used(), + 'use_you_tube' => YouTubeService::enabled(), + 'use_i_tunes' => $this->iTunesService->used(), + 'allow_download' => config('koel.download.allow'), + 'supports_transcoding' => config('koel.streaming.ffmpeg_path') && is_executable(config('koel.streaming.ffmpeg_path')), - 'cdnUrl' => static_url(), - 'currentVersion' => koel_version(), - 'latestVersion' => $this->currentUser->is_admin + 'cdn_url' => static_url(), + 'current_version' => koel_version(), + 'latest_version' => $this->user->is_admin ? $this->applicationInformationService->getLatestVersionNumber() : koel_version(), + 'song_count' => $this->songRepository->count(), + 'song_length' => $this->songRepository->getTotalLength(), ]); } } diff --git a/app/Http/Controllers/V6/API/ExcerptSearchController.php b/app/Http/Controllers/API/ExcerptSearchController.php similarity index 79% rename from app/Http/Controllers/V6/API/ExcerptSearchController.php rename to app/Http/Controllers/API/ExcerptSearchController.php index dd77d1b3..515838b9 100644 --- a/app/Http/Controllers/V6/API/ExcerptSearchController.php +++ b/app/Http/Controllers/API/ExcerptSearchController.php @@ -1,12 +1,12 @@ interactionService->increasePlayCount($request->song, $this->user); event(new SongStartedPlaying($interaction->song, $interaction->user)); - return response()->json($interaction); + return InteractionResource::make($interaction); } } diff --git a/app/Http/Controllers/API/MediaInformation/AlbumController.php b/app/Http/Controllers/API/MediaInformation/AlbumController.php deleted file mode 100644 index e7b4eacf..00000000 --- a/app/Http/Controllers/API/MediaInformation/AlbumController.php +++ /dev/null @@ -1,19 +0,0 @@ -json($this->mediaInformationService->getAlbumInformation($album)?->toArray() ?: []); - } -} diff --git a/app/Http/Controllers/API/MediaInformation/ArtistController.php b/app/Http/Controllers/API/MediaInformation/ArtistController.php deleted file mode 100644 index 26126728..00000000 --- a/app/Http/Controllers/API/MediaInformation/ArtistController.php +++ /dev/null @@ -1,19 +0,0 @@ -json($this->mediaInformationService->getArtistInformation($artist)?->toArray() ?: []); - } -} diff --git a/app/Http/Controllers/API/MediaInformation/SongController.php b/app/Http/Controllers/API/MediaInformation/SongController.php deleted file mode 100644 index a8406845..00000000 --- a/app/Http/Controllers/API/MediaInformation/SongController.php +++ /dev/null @@ -1,27 +0,0 @@ -json([ - 'lyrics' => nl2br($song->lyrics), // backward compat - 'album_info' => $this->mediaInformationService->getAlbumInformation($song->album)?->toArray() ?: [], - 'artist_info' => $this->mediaInformationService->getArtistInformation($song->artist)?->toArray() ?: [], - 'youtube' => $this->youTubeService->searchVideosRelatedToSong($song), - ]); - } -} diff --git a/app/Http/Controllers/V6/API/OverviewController.php b/app/Http/Controllers/API/OverviewController.php similarity index 96% rename from app/Http/Controllers/V6/API/OverviewController.php rename to app/Http/Controllers/API/OverviewController.php index 09967660..0dd6f93b 100644 --- a/app/Http/Controllers/V6/API/OverviewController.php +++ b/app/Http/Controllers/API/OverviewController.php @@ -1,6 +1,6 @@ json($this->playlistRepository->getAllByCurrentUser()); + return PlaylistResource::collection($this->user->playlists); } public function store(PlaylistStoreRequest $request) { + $folder = null; + + if ($request->folder_id) { + /** @var PlaylistFolder $folder */ + $folder = $this->folderRepository->getOneById($request->folder_id); + $this->authorize('own', $folder); + } + try { $playlist = $this->playlistService->createPlaylist( $request->name, $this->user, - null, + $folder, Arr::wrap($request->songs), $request->rules ? SmartPlaylistRuleGroupCollection::create(Arr::wrap($request->rules)) : null ); - $playlist->songs = $playlist->songs->pluck('id')->toArray(); - - return response()->json($playlist); + return PlaylistResource::make($playlist); } catch (PlaylistBothSongsAndRulesProvidedException $e) { throw ValidationException::withMessages(['songs' => [$e->getMessage()]]); } @@ -53,9 +61,22 @@ class PlaylistController extends Controller { $this->authorize('own', $playlist); - $playlist->update($request->only('name', 'rules')); + $folder = null; - return response()->json($playlist); + if ($request->folder_id) { + /** @var PlaylistFolder $folder */ + $folder = $this->folderRepository->getOneById($request->folder_id); + $this->authorize('own', $folder); + } + + return PlaylistResource::make( + $this->playlistService->updatePlaylist( + $playlist, + $request->name, + $folder, + $request->rules ? SmartPlaylistRuleGroupCollection::create(Arr::wrap($request->rules)) : null + ) + ); } public function destroy(Playlist $playlist) diff --git a/app/Http/Controllers/V6/API/PlaylistFolderController.php b/app/Http/Controllers/API/PlaylistFolderController.php similarity index 87% rename from app/Http/Controllers/V6/API/PlaylistFolderController.php rename to app/Http/Controllers/API/PlaylistFolderController.php index fa3adc8a..12e1cd08 100644 --- a/app/Http/Controllers/V6/API/PlaylistFolderController.php +++ b/app/Http/Controllers/API/PlaylistFolderController.php @@ -1,10 +1,10 @@ authorize('own', $playlist); - return response()->json( + return SongResource::collection( $playlist->is_smart - ? $this->smartPlaylistService->getSongs($playlist, $this->user)->pluck('id') - : $playlist->songs->pluck('id') + ? $this->smartPlaylistService->getSongs($playlist, $this->user) + : $this->songRepository->getByStandardPlaylist($playlist, $this->user) ); } - /** @deprecated */ - public function update(PlaylistSongUpdateRequest $request, Playlist $playlist) + public function store(Playlist $playlist, AddSongsToPlaylistRequest $request) { $this->authorize('own', $playlist); - abort_if($playlist->is_smart, Response::HTTP_FORBIDDEN, 'A smart playlist cannot be populated manually.'); + abort_if($playlist->is_smart, Response::HTTP_FORBIDDEN); - $this->playlistService->populatePlaylist($playlist, Arr::wrap($request->songs)); + $this->playlistService->addSongsToPlaylist($playlist, $request->songs); + + return response()->noContent(); + } + + public function destroy(Playlist $playlist, RemoveSongsFromPlaylistRequest $request) + { + $this->authorize('own', $playlist); + + abort_if($playlist->is_smart, Response::HTTP_FORBIDDEN); + + $this->playlistService->removeSongsFromPlaylist($playlist, $request->songs); return response()->noContent(); } diff --git a/app/Http/Controllers/V6/API/QueueController.php b/app/Http/Controllers/API/QueueController.php similarity index 90% rename from app/Http/Controllers/V6/API/QueueController.php rename to app/Http/Controllers/API/QueueController.php index 1b727340..686e4cd8 100644 --- a/app/Http/Controllers/V6/API/QueueController.php +++ b/app/Http/Controllers/API/QueueController.php @@ -1,9 +1,9 @@ get('q'), new InvalidArgumentException('A search query is required.')); - - $count = (int) $request->get('count', SearchService::DEFAULT_EXCERPT_RESULT_COUNT); - throw_if($count < 0, new InvalidArgumentException('Invalid count parameter.')); - - return [ - 'results' => $this->searchService->excerptSearch($request->get('q'), $count), - ]; - } -} diff --git a/app/Http/Controllers/API/Search/SongSearchController.php b/app/Http/Controllers/API/Search/SongSearchController.php deleted file mode 100644 index 0c7f767b..00000000 --- a/app/Http/Controllers/API/Search/SongSearchController.php +++ /dev/null @@ -1,24 +0,0 @@ -get('q'), new InvalidArgumentException('A search query is required.')); - - return [ - 'songs' => $this->searchService->searchSongs($request->get('q')), - ]; - } -} diff --git a/app/Http/Controllers/API/SongController.php b/app/Http/Controllers/API/SongController.php index d69a4e9a..9fe23e10 100644 --- a/app/Http/Controllers/API/SongController.php +++ b/app/Http/Controllers/API/SongController.php @@ -3,26 +3,51 @@ namespace App\Http\Controllers\API; use App\Http\Controllers\Controller; +use App\Http\Requests\API\DeleteSongsRequest; +use App\Http\Requests\API\SongListRequest; use App\Http\Requests\API\SongUpdateRequest; use App\Http\Resources\AlbumResource; use App\Http\Resources\ArtistResource; use App\Http\Resources\SongResource; +use App\Models\Song; +use App\Models\User; use App\Repositories\AlbumRepository; use App\Repositories\ArtistRepository; +use App\Repositories\SongRepository; use App\Services\LibraryManager; use App\Services\SongService; use App\Values\SongUpdateData; +use Illuminate\Contracts\Auth\Authenticatable; class SongController extends Controller { + /** @param User $user */ public function __construct( private SongService $songService, + private SongRepository $songRepository, private AlbumRepository $albumRepository, private ArtistRepository $artistRepository, - private LibraryManager $libraryManager + private LibraryManager $libraryManager, + private ?Authenticatable $user ) { } + public function index(SongListRequest $request) + { + return SongResource::collection( + $this->songRepository->getForListing( + $request->sort ?: 'songs.title', + $request->order ?: 'asc', + $this->user + ) + ); + } + + public function show(Song $song) + { + return SongResource::make($this->songRepository->getOne($song->id)); + } + public function update(SongUpdateRequest $request) { $updatedSongs = $this->songService->updateSongs($request->songs, SongUpdateData::fromRequest($request)); @@ -42,4 +67,13 @@ class SongController extends Controller 'removed' => $this->libraryManager->prune(), ]); } + + public function destroy(DeleteSongsRequest $request) + { + $this->authorize('admin', $this->user); + + $this->songService->deleteSongs($request->songs); + + return response()->noContent(); + } } diff --git a/app/Http/Controllers/V6/API/SongSearchController.php b/app/Http/Controllers/API/SongSearchController.php similarity index 78% rename from app/Http/Controllers/V6/API/SongSearchController.php rename to app/Http/Controllers/API/SongSearchController.php index 91f700c3..dabe5799 100644 --- a/app/Http/Controllers/V6/API/SongSearchController.php +++ b/app/Http/Controllers/API/SongSearchController.php @@ -1,12 +1,12 @@ json([ - 'settings' => $this->user->is_admin ? $this->settingRepository->getAllAsKeyValueArray() : [], - 'playlists' => PlaylistResource::collection($this->user->playlists), - 'playlist_folders' => PlaylistFolderResource::collection($this->user->playlist_folders), - 'current_user' => UserResource::make($this->user, true), - 'use_last_fm' => LastfmService::used(), - 'use_you_tube' => YouTubeService::enabled(), - 'use_i_tunes' => $this->iTunesService->used(), - 'allow_download' => config('koel.download.allow'), - 'supports_transcoding' => config('koel.streaming.ffmpeg_path') - && is_executable(config('koel.streaming.ffmpeg_path')), - 'cdn_url' => static_url(), - 'current_version' => koel_version(), - 'latest_version' => $this->user->is_admin - ? $this->applicationInformationService->getLatestVersionNumber() - : koel_version(), - 'song_count' => $this->songRepository->count(), - 'song_length' => $this->songRepository->getTotalLength(), - ]); - } -} diff --git a/app/Http/Controllers/V6/API/DeleteSongsController.php b/app/Http/Controllers/V6/API/DeleteSongsController.php deleted file mode 100644 index bd161e54..00000000 --- a/app/Http/Controllers/V6/API/DeleteSongsController.php +++ /dev/null @@ -1,20 +0,0 @@ -authorize('admin', $user); - - $service->deleteSongs($request->songs); - - return response()->noContent(); - } -} diff --git a/app/Http/Controllers/V6/API/PlayCountController.php b/app/Http/Controllers/V6/API/PlayCountController.php deleted file mode 100644 index d05900a9..00000000 --- a/app/Http/Controllers/V6/API/PlayCountController.php +++ /dev/null @@ -1,27 +0,0 @@ -interactionService->increasePlayCount($request->song, $this->user); - event(new SongStartedPlaying($interaction->song, $interaction->user)); - - return InteractionResource::make($interaction); - } -} diff --git a/app/Http/Controllers/V6/API/PlaylistController.php b/app/Http/Controllers/V6/API/PlaylistController.php deleted file mode 100644 index b8168486..00000000 --- a/app/Http/Controllers/V6/API/PlaylistController.php +++ /dev/null @@ -1,90 +0,0 @@ -user->playlists); - } - - public function store(PlaylistStoreRequest $request) - { - $folder = null; - - if ($request->folder_id) { - /** @var PlaylistFolder $folder */ - $folder = $this->folderRepository->getOneById($request->folder_id); - $this->authorize('own', $folder); - } - - try { - $playlist = $this->playlistService->createPlaylist( - $request->name, - $this->user, - $folder, - Arr::wrap($request->songs), - $request->rules ? SmartPlaylistRuleGroupCollection::create(Arr::wrap($request->rules)) : null - ); - - return PlaylistResource::make($playlist); - } catch (PlaylistBothSongsAndRulesProvidedException $e) { - throw ValidationException::withMessages(['songs' => [$e->getMessage()]]); - } - } - - public function update(PlaylistUpdateRequest $request, Playlist $playlist) - { - $this->authorize('own', $playlist); - - $folder = null; - - if ($request->folder_id) { - /** @var PlaylistFolder $folder */ - $folder = $this->folderRepository->getOneById($request->folder_id); - $this->authorize('own', $folder); - } - - return PlaylistResource::make( - $this->playlistService->updatePlaylist( - $playlist, - $request->name, - $folder, - $request->rules ? SmartPlaylistRuleGroupCollection::create(Arr::wrap($request->rules)) : null - ) - ); - } - - public function destroy(Playlist $playlist) - { - $this->authorize('own', $playlist); - - $playlist->delete(); - - return response()->noContent(); - } -} diff --git a/app/Http/Controllers/V6/API/PlaylistSongController.php b/app/Http/Controllers/V6/API/PlaylistSongController.php deleted file mode 100644 index a30af9a6..00000000 --- a/app/Http/Controllers/V6/API/PlaylistSongController.php +++ /dev/null @@ -1,60 +0,0 @@ -authorize('own', $playlist); - - return SongResource::collection( - $playlist->is_smart - ? $this->smartPlaylistService->getSongs($playlist, $this->user) - : $this->songRepository->getByStandardPlaylist($playlist, $this->user) - ); - } - - public function store(Playlist $playlist, AddSongsToPlaylistRequest $request) - { - $this->authorize('own', $playlist); - - abort_if($playlist->is_smart, Response::HTTP_FORBIDDEN); - - $this->playlistService->addSongsToPlaylist($playlist, $request->songs); - - return response()->noContent(); - } - - public function destroy(Playlist $playlist, RemoveSongsFromPlaylistRequest $request) - { - $this->authorize('own', $playlist); - - abort_if($playlist->is_smart, Response::HTTP_FORBIDDEN); - - $this->playlistService->removeSongsFromPlaylist($playlist, $request->songs); - - return response()->noContent(); - } -} diff --git a/app/Http/Controllers/V6/API/SongController.php b/app/Http/Controllers/V6/API/SongController.php deleted file mode 100644 index 099d3e22..00000000 --- a/app/Http/Controllers/V6/API/SongController.php +++ /dev/null @@ -1,35 +0,0 @@ -songRepository->getOne($song->id)); - } - - public function index(SongListRequest $request) - { - return SongResource::collection( - $this->songRepository->getForListing( - $request->sort ?: 'songs.title', - $request->order ?: 'asc', - $this->user - ) - ); - } -} diff --git a/app/Http/Requests/V6/API/AddSongsToPlaylistRequest.php b/app/Http/Requests/API/AddSongsToPlaylistRequest.php similarity index 83% rename from app/Http/Requests/V6/API/AddSongsToPlaylistRequest.php rename to app/Http/Requests/API/AddSongsToPlaylistRequest.php index 1c54c102..26167a7d 100644 --- a/app/Http/Requests/V6/API/AddSongsToPlaylistRequest.php +++ b/app/Http/Requests/API/AddSongsToPlaylistRequest.php @@ -1,8 +1,7 @@ $songs */ class DeleteSongsRequest extends Request diff --git a/app/Http/Requests/V6/API/FetchRandomSongsInGenreRequest.php b/app/Http/Requests/API/FetchRandomSongsInGenreRequest.php similarity index 65% rename from app/Http/Requests/V6/API/FetchRandomSongsInGenreRequest.php rename to app/Http/Requests/API/FetchRandomSongsInGenreRequest.php index 4f4575db..0291f44f 100644 --- a/app/Http/Requests/V6/API/FetchRandomSongsInGenreRequest.php +++ b/app/Http/Requests/API/FetchRandomSongsInGenreRequest.php @@ -1,8 +1,6 @@ $songs diff --git a/app/Http/Requests/V6/API/SearchRequest.php b/app/Http/Requests/API/SearchRequest.php similarity index 73% rename from app/Http/Requests/V6/API/SearchRequest.php rename to app/Http/Requests/API/SearchRequest.php index 97fddc3f..71f2a911 100644 --- a/app/Http/Requests/V6/API/SearchRequest.php +++ b/app/Http/Requests/API/SearchRequest.php @@ -1,8 +1,6 @@ */ - public function excerptSearch(string $keywords, int $count): array - { - return [ - 'songs' => self::getTopResults($this->songRepository->search($keywords), $count) - ->map(static fn (Song $song): string => $song->id), - 'artists' => self::getTopResults($this->artistRepository->search($keywords), $count) - ->map(static fn (Artist $artist): int => $artist->id), - 'albums' => self::getTopResults($this->albumRepository->search($keywords), $count) - ->map(static fn (Album $album): int => $album->id), - ]; + public function excerptSearch( + string $keywords, + ?User $scopedUser = null, + int $count = self::DEFAULT_EXCERPT_RESULT_COUNT + ): ExcerptSearchResult { + $scopedUser ??= auth()->user(); + + return ExcerptSearchResult::make( + $this->songRepository->getByIds( + Song::search($keywords)->get()->take($count)->pluck('id')->all(), + $scopedUser + ), + $this->artistRepository->getByIds(Artist::search($keywords)->get()->take($count)->pluck('id')->all()), + $this->albumRepository->getByIds(Album::search($keywords)->get()->take($count)->pluck('id')->all()), + ); } - /** @return Collection|array */ - private static function getTopResults(Builder $query, int $count): Collection - { - return $query->take($count)->get(); - } - - /** @return Collection|array */ - public function searchSongs(string $keywords): Collection - { - return $this->songRepository - ->search($keywords) - ->get() - ->map(static fn (Song $song): string => $song->id); // @phpstan-ignore-line + /** @return Collection|array */ + public function searchSongs( + string $keywords, + ?User $scopedUser = null, + int $limit = self::DEFAULT_MAX_SONG_RESULT_COUNT + ): Collection { + return Song::search($keywords) + ->query(static function (SongBuilder $builder) use ($scopedUser, $limit): void { + $builder->withMeta($scopedUser ?? auth()->user())->limit($limit); + }) + ->get(); } } diff --git a/app/Services/V6/SearchService.php b/app/Services/V6/SearchService.php deleted file mode 100644 index 035672cc..00000000 --- a/app/Services/V6/SearchService.php +++ /dev/null @@ -1,57 +0,0 @@ -user(); - - return ExcerptSearchResult::make( - $this->songRepository->getByIds( - Song::search($keywords)->get()->take($count)->pluck('id')->all(), - $scopedUser - ), - $this->artistRepository->getByIds(Artist::search($keywords)->get()->take($count)->pluck('id')->all()), - $this->albumRepository->getByIds(Album::search($keywords)->get()->take($count)->pluck('id')->all()), - ); - } - - /** @return Collection|array */ - public function searchSongs( - string $keywords, - ?User $scopedUser = null, - int $limit = self::DEFAULT_MAX_SONG_RESULT_COUNT - ): Collection { - return Song::search($keywords) - ->query(static function (SongBuilder $builder) use ($scopedUser, $limit): void { - $builder->withMeta($scopedUser ?? auth()->user())->limit($limit); - }) - ->get(); - } -} diff --git a/routes/api.base.php b/routes/api.base.php index cf5dd15a..ce3bcf7f 100644 --- a/routes/api.base.php +++ b/routes/api.base.php @@ -1,32 +1,45 @@ middleware('api')->group(static function (): void { return $pusher->socket_auth($request->channel_name, $request->socket_id); })->name('broadcasting.auth'); + Route::get('overview', [OverviewController::class, 'index']); Route::get('data', [DataController::class, 'index']); + Route::get('queue/fetch', [QueueController::class, 'fetchSongs']); + Route::put('settings', [SettingController::class, 'update']); - /** - * @deprecated Use songs/{song}/scrobble instead - */ - Route::post('{song}/scrobble', [ScrobbleController::class, 'store']); - Route::post('songs/{song}/scrobble', [ScrobbleController::class, 'store']); + Route::apiResource('albums', AlbumController::class); + Route::apiResource('albums.songs', AlbumSongController::class); + + Route::apiResource('artists', ArtistController::class); + Route::apiResource('artists.albums', ArtistAlbumController::class); + Route::apiResource('artists.songs', ArtistSongController::class); + + Route::post('songs/{song}/scrobble', [ScrobbleController::class, 'store'])->where(['song' => Song::ID_REGEX]); + + Route::apiResource('songs', SongController::class) + ->except('update', 'destroy') + ->where(['song' => Song::ID_REGEX]); + Route::put('songs', [SongController::class, 'update']); + Route::delete('songs', [SongController::class, 'destroy']); Route::post('upload', UploadController::class); @@ -70,19 +95,30 @@ Route::prefix('api')->middleware('api')->group(static function (): void { Route::post('interaction/like', [LikeController::class, 'store']); Route::post('interaction/batch/like', [BatchLikeController::class, 'store']); Route::post('interaction/batch/unlike', [BatchLikeController::class, 'destroy']); - Route::get('interaction/recently-played/{count?}', [RecentlyPlayedController::class, 'index'])->where([ - 'count' => '\d+', - ]); + + Route::get('songs/recently-played', [RecentlyPlayedSongController::class, 'index']); + Route::get('songs/favorite', [FavoriteSongController::class, 'index']); + + Route::apiResource('playlist-folders', PlaylistFolderController::class); + Route::apiResource('playlist-folders.playlists', PlaylistFolderPlaylistController::class)->except('destroy'); + Route::delete( + 'playlist-folders/{playlistFolder}/playlists', + [PlaylistFolderPlaylistController::class, 'destroy'] + ); // Playlist routes - Route::apiResource('playlist', PlaylistController::class); + Route::apiResource('playlists', PlaylistController::class); + Route::apiResource('playlists.songs', PlaylistSongController::class)->except('destroy'); + Route::delete('playlists/{playlist}/songs', [PlaylistSongController::class, 'destroy']); - Route::put('playlist/{playlist}/sync', [PlaylistSongController::class, 'update']); // @deprecated - Route::put('playlist/{playlist}/songs', [PlaylistSongController::class, 'update']); - Route::get('playlist/{playlist}/songs', [PlaylistSongController::class, 'index']); + Route::get('genres/{genre}/songs', GenreSongController::class)->where('genre', '.*'); + Route::get('genres/{genre}/songs/random', FetchRandomSongsInGenreController::class)->where('genre', '.*'); + Route::apiResource('genres', GenreController::class)->where(['genre' => '.*']); + + Route::apiResource('users', UserController::class); // User and user profile routes - Route::apiResource('user', UserController::class)->only('store', 'update', 'destroy'); + Route::apiResource('user', UserController::class); Route::get('me', [ProfileController::class, 'show']); Route::put('me', [ProfileController::class, 'update']); @@ -96,20 +132,16 @@ Route::prefix('api')->middleware('api')->group(static function (): void { } // Media information routes - Route::get('album/{album}/info', [AlbumInformationController::class, 'show']); - Route::get('artist/{artist}/info', [ArtistInformationController::class, 'show']); - Route::get('song/{song}/info', [SongInformationController::class, 'show']); + Route::get('albums/{album}/information', FetchAlbumInformationController::class); + Route::get('artists/{artist}/information', FetchArtistInformationController::class); // Cover/image upload routes Route::put('album/{album}/cover', [AlbumCoverController::class, 'update']); Route::put('artist/{artist}/image', [ArtistImageController::class, 'update']); Route::get('album/{album}/thumbnail', [AlbumThumbnailController::class, 'show']); - // Search routes - Route::prefix('search')->group(static function (): void { - Route::get('/', [ExcerptSearchController::class, 'index']); - Route::get('songs', [SongSearchController::class, 'index']); - }); + Route::get('search', ExcerptSearchController::class); + Route::get('search/songs', SongSearchController::class); }); // Object-storage (S3) routes diff --git a/routes/api.v6.php b/routes/api.v6.php deleted file mode 100644 index c45630cd..00000000 --- a/routes/api.v6.php +++ /dev/null @@ -1,74 +0,0 @@ -middleware('api')->group(static function (): void { - Route::middleware('auth')->group(static function (): void { - Route::get('overview', [OverviewController::class, 'index']); - Route::get('data', [DataController::class, 'index']); - - Route::apiResource('albums', AlbumController::class); - Route::apiResource('albums.songs', AlbumSongController::class); - Route::apiResource('artists', ArtistController::class); - Route::apiResource('artists.songs', ArtistSongController::class); - Route::apiResource('artists.albums', ArtistAlbumController::class); - - Route::get('albums/{album}/information', FetchAlbumInformationController::class); - Route::get('artists/{artist}/information', FetchArtistInformationController::class); - - Route::apiResource('playlist-folders', PlaylistFolderController::class); - Route::apiResource('playlist-folders.playlists', PlaylistFolderPlaylistController::class)->except('destroy'); - Route::delete( - 'playlist-folders/{playlistFolder}/playlists', - [PlaylistFolderPlaylistController::class, 'destroy'] - ); - - Route::apiResource('playlists', PlaylistController::class); - Route::apiResource('playlists.songs', PlaylistSongController::class)->except('destroy'); - Route::delete('playlists/{playlist}/songs', [PlaylistSongController::class, 'destroy']); - - Route::apiResource('songs', SongController::class)->where(['song' => Song::ID_REGEX]); - Route::get('songs/recently-played', [RecentlyPlayedSongController::class, 'index']); - Route::get('songs/favorite', [FavoriteSongController::class, 'index']); - Route::delete('songs', DeleteSongsController::class); - - Route::get('genres/{genre}/songs', GenreSongController::class)->where('genre', '.*'); - Route::get('genres/{genre}/songs/random', FetchRandomSongsInGenreController::class)->where('genre', '.*'); - Route::apiResource('genres', GenreController::class)->where(['genre' => '.*']); - - Route::apiResource('users', UserController::class); - - Route::get('search', ExcerptSearchController::class); - Route::get('search/songs', SongSearchController::class); - - Route::get('queue/fetch', [QueueController::class, 'fetchSongs']); - - Route::post('interaction/play', [PlayCountController::class, 'store']); - }); -}); diff --git a/tests/Feature/V6/AlbumInformationTest.php b/tests/Feature/AlbumInformationTest.php similarity index 98% rename from tests/Feature/V6/AlbumInformationTest.php rename to tests/Feature/AlbumInformationTest.php index 3e5b87dd..f08bb729 100644 --- a/tests/Feature/V6/AlbumInformationTest.php +++ b/tests/Feature/AlbumInformationTest.php @@ -1,6 +1,6 @@ shouldReceive('from') ->once() ->with(Mockery::on(static function (Collection $retrievedSongs) use ($songs): bool { - $retrievedIds = $retrievedSongs->pluck('id')->toArray(); - $requestedIds = $songs->pluck('id')->toArray(); + $retrievedIds = $retrievedSongs->pluck('id')->all(); + $requestedIds = $songs->pluck('id')->all(); self::assertEqualsCanonicalizing($requestedIds, $retrievedIds); return true; @@ -131,9 +131,7 @@ class DownloadTest extends TestCase $user = User::factory()->create(); /** @var Playlist $playlist */ - $playlist = Playlist::factory()->create([ - 'user_id' => $user->id, - ]); + $playlist = Playlist::factory()->for($user)->create(); $this->downloadService ->shouldReceive('from') diff --git a/tests/Feature/V6/ExcerptSearchTest.php b/tests/Feature/ExcerptSearchTest.php similarity index 96% rename from tests/Feature/V6/ExcerptSearchTest.php rename to tests/Feature/ExcerptSearchTest.php index 781263c8..ef321eb5 100644 --- a/tests/Feature/V6/ExcerptSearchTest.php +++ b/tests/Feature/ExcerptSearchTest.php @@ -1,6 +1,6 @@ assertJsonStructure(self::JSON_STRUCTURE) ->assertJsonFragment(['name' => 'Rock', 'song_count' => 5]); } - + public function testGetNonExistingGenreThrowsNotFound(): void { $this->getAs('api/genres/NonExistingGenre')->assertNotFound(); diff --git a/tests/Feature/InteractionTest.php b/tests/Feature/InteractionTest.php index f0ff85c8..3f519893 100644 --- a/tests/Feature/InteractionTest.php +++ b/tests/Feature/InteractionTest.php @@ -80,7 +80,7 @@ class InteractionTest extends TestCase /** @var Collection|array $songs */ $songs = Song::query()->orderBy('id')->take(2)->get(); - $songIds = array_pluck($songs->toArray(), 'id'); + $songIds = $songs->pluck('id')->all(); $this->postAs('api/interaction/batch/like', ['songs' => $songIds], $user); diff --git a/tests/Feature/V6/OverviewTest.php b/tests/Feature/OverviewTest.php similarity index 96% rename from tests/Feature/V6/OverviewTest.php rename to tests/Feature/OverviewTest.php index d937c25c..6dbf5fb6 100644 --- a/tests/Feature/V6/OverviewTest.php +++ b/tests/Feature/OverviewTest.php @@ -1,6 +1,6 @@ doTestUpdatePlaylistSongs(); + /** @var Playlist $playlist */ + $playlist = Playlist::factory()->create(); + $playlist->songs()->attach(Song::factory(5)->create()); + + $this->getAs('api/playlists/' . $playlist->id . '/songs', $playlist->user) + ->assertJsonStructure(['*' => SongTest::JSON_STRUCTURE]); } - /** @deprecated */ - public function testSyncPlaylist(): void + public function testGetSmartPlaylist(): void { - $this->doTestUpdatePlaylistSongs(true); + Song::factory()->create(['title' => 'A foo song']); + + /** @var Playlist $playlist */ + $playlist = Playlist::factory()->create([ + 'rules' => [ + [ + 'id' => '45368b8f-fec8-4b72-b826-6b295af0da65', + 'rules' => [ + [ + 'id' => '2a4548cd-c67f-44d4-8fec-34ff75c8a026', + 'model' => 'title', + 'operator' => 'contains', + 'value' => ['foo'], + ], + ], + ], + ], + ]); + + $this->getAs("api/playlists/$playlist->id/songs", $playlist->user) + ->assertJsonStructure(['*' => SongTest::JSON_STRUCTURE]); } - private function doTestUpdatePlaylistSongs(bool $useDeprecatedRoute = false): void + public function testNonOwnerCannotAccessPlaylist(): void + { + $user = User::factory()->create(); + + /** @var Playlist $playlist */ + $playlist = Playlist::factory()->for($user)->create(); + $playlist->songs()->attach(Song::factory(5)->create()); + + $this->getAs('api/playlists/' . $playlist->id . '/songs') + ->assertForbidden(); + } + + public function testAddSongsToPlaylist(): void + { + /** @var Playlist $playlist */ + $playlist = Playlist::factory()->create(); + + /** @var Collection|array $songs */ + $songs = Song::factory(2)->create(); + + $this->postAs('api/playlists/' . $playlist->id . '/songs', [ + 'songs' => $songs->map(static fn (Song $song) => $song->id)->all(), + ], $playlist->user) + ->assertNoContent(); + + self::assertEqualsCanonicalizing($songs->pluck('id')->all(), $playlist->songs->pluck('id')->all()); + } + + public function testRemoveSongsFromPlaylist(): void + { + /** @var Playlist $playlist */ + $playlist = Playlist::factory()->create(); + + $toRemainSongs = Song::factory(5)->create(); + + /** @var Collection|array $toBeRemovedSongs */ + $toBeRemovedSongs = Song::factory(2)->create(); + + $playlist->songs()->attach($toRemainSongs->merge($toBeRemovedSongs)); + + self::assertCount(7, $playlist->songs); + + $this->deleteAs('api/playlists/' . $playlist->id . '/songs', [ + 'songs' => $toBeRemovedSongs->map(static fn (Song $song) => $song->id)->all(), + ], $playlist->user) + ->assertNoContent(); + + $playlist->refresh(); + + self::assertEqualsCanonicalizing($toRemainSongs->pluck('id')->all(), $playlist->songs->pluck('id')->all()); + } + + public function testNonOwnerCannotModifyPlaylist(): void { - /** @var User $user */ $user = User::factory()->create(); /** @var Playlist $playlist */ $playlist = Playlist::factory()->for($user)->create(); - $toRemainSongs = Song::factory(3)->create(); - $toBeRemovedSongs = Song::factory(2)->create(); - $playlist->songs()->attach($toRemainSongs->merge($toBeRemovedSongs)); + /** @var Song $song */ + $song = Song::factory()->create(); - $path = $useDeprecatedRoute ? "api/playlist/$playlist->id/sync" : "api/playlist/$playlist->id/songs"; + $this->postAs('api/playlists/' . $playlist->id . '/songs', ['songs' => [$song->id]]) + ->assertForbidden(); - $this->putAs($path, ['songs' => $toRemainSongs->pluck('id')->all()], $user)->assertNoContent(); - - self::assertEqualsCanonicalizing( - $toRemainSongs->pluck('id')->all(), - $playlist->refresh()->songs->pluck('id')->all() - ); + $this->deleteAs('api/playlists/' . $playlist->id . '/songs', ['songs' => [$song->id]]) + ->assertForbidden(); } - public function testGetPlaylistSongs(): void + public function testSmartPlaylistContentCannotBeModified(): void { /** @var Playlist $playlist */ - $playlist = Playlist::factory()->create(); + $playlist = Playlist::factory()->create([ + 'rules' => [ + [ + 'id' => '45368b8f-fec8-4b72-b826-6b295af0da65', + 'rules' => [ + [ + 'id' => '2a4548cd-c67f-44d4-8fec-34ff75c8a026', + 'model' => 'title', + 'operator' => 'contains', + 'value' => ['foo'], + ], + ], + ], + ], + ]); + /** @var Collection|array $songs */ $songs = Song::factory(2)->create(); - $playlist->songs()->saveMany($songs); + $songIds = $songs->map(static fn (Song $song) => $song->id)->all(); - $responseIds = $this->getAs("api/playlist/$playlist->id/songs", $playlist->user) - ->json(); + $this->postAs('api/playlists/' . $playlist->id . '/songs', ['songs' => $songIds], $playlist->user) + ->assertForbidden(); - self::assertEqualsCanonicalizing($responseIds, $songs->pluck('id')->all()); + $this->deleteAs('api/playlists/' . $playlist->id . '/songs', ['songs' => $songIds], $playlist->user) + ->assertForbidden(); } } diff --git a/tests/Feature/PlaylistTest.php b/tests/Feature/PlaylistTest.php index b9c8b7a7..7379022a 100644 --- a/tests/Feature/PlaylistTest.php +++ b/tests/Feature/PlaylistTest.php @@ -10,12 +10,16 @@ use Illuminate\Support\Collection; class PlaylistTest extends TestCase { - public function setUp(): void - { - parent::setUp(); - - static::createSampleMediaSet(); - } + private const JSON_STRUCTURE = [ + 'type', + 'id', + 'name', + 'folder_id', + 'user_id', + 'is_smart', + 'rules', + 'created_at', + ]; public function testCreatingPlaylist(): void { @@ -23,21 +27,21 @@ class PlaylistTest extends TestCase $user = User::factory()->create(); /** @var array|Collection $songs */ - $songs = Song::query()->orderBy('id')->take(3)->get(); + $songs = Song::factory(4)->create(); - $response = $this->postAs('api/playlist', [ + $this->postAs('api/playlists', [ 'name' => 'Foo Bar', - 'songs' => $songs->pluck('id')->toArray(), + 'songs' => $songs->pluck('id')->all(), 'rules' => [], - ], $user); - - $response->assertOk(); + ], $user) + ->assertJsonStructure(self::JSON_STRUCTURE); /** @var Playlist $playlist */ $playlist = Playlist::query()->orderByDesc('id')->first(); self::assertSame('Foo Bar', $playlist->name); self::assertTrue($playlist->user->is($user)); + self::assertNull($playlist->folder_id); self::assertEqualsCanonicalizing($songs->pluck('id')->all(), $playlist->songs->pluck('id')->all()); } @@ -52,15 +56,15 @@ class PlaylistTest extends TestCase 'value' => ['Bob Dylan'], ]); - $this->postAs('api/playlist', [ + $this->postAs('api/playlists', [ 'name' => 'Smart Foo Bar', 'rules' => [ [ - 'id' => '45368b8f-fec8-4b72-b826-6b295af0da65', + 'id' => '2a4548cd-c67f-44d4-8fec-34ff75c8a026', 'rules' => [$rule->toArray()], ], ], - ], $user); + ], $user)->assertJsonStructure(self::JSON_STRUCTURE); /** @var Playlist $playlist */ $playlist = Playlist::query()->orderByDesc('id')->first(); @@ -69,16 +73,17 @@ class PlaylistTest extends TestCase self::assertTrue($playlist->user->is($user)); self::assertTrue($playlist->is_smart); self::assertCount(1, $playlist->rule_groups); + self::assertNull($playlist->folder_id); self::assertTrue($rule->equals($playlist->rule_groups[0]->rules[0])); } - public function testCreatingPlaylistCannotHaveBothSongsAndRules(): void + public function testCreatingSmartPlaylistFailsIfSongsProvided(): void { - $this->postAs('api/playlist', [ + $this->postAs('api/playlists', [ 'name' => 'Smart Foo Bar', 'rules' => [ [ - 'id' => '45368b8f-fec8-4b72-b826-6b295af0da65', + 'id' => '2a4548cd-c67f-44d4-8fec-34ff75c8a026', 'rules' => [ SmartPlaylistRule::create([ 'model' => 'artist.name', @@ -88,13 +93,13 @@ class PlaylistTest extends TestCase ], ], ], - 'songs' => Song::query()->orderBy('id')->take(3)->get()->pluck('id')->all(), + 'songs' => Song::factory(3)->create()->pluck('id')->all(), ])->assertUnprocessable(); } public function testCreatingPlaylistWithNonExistentSongsFails(): void { - $this->postAs('api/playlist', [ + $this->postAs('api/playlists', [ 'name' => 'Foo Bar', 'rules' => [], 'songs' => ['foo'], @@ -107,7 +112,8 @@ class PlaylistTest extends TestCase /** @var Playlist $playlist */ $playlist = Playlist::factory()->create(['name' => 'Foo']); - $this->putAs("api/playlist/$playlist->id", ['name' => 'Bar'], $playlist->user); + $this->putAs("api/playlists/$playlist->id", ['name' => 'Bar'], $playlist->user) + ->assertJsonStructure(self::JSON_STRUCTURE); self::assertSame('Bar', $playlist->refresh()->name); } @@ -117,7 +123,8 @@ class PlaylistTest extends TestCase /** @var Playlist $playlist */ $playlist = Playlist::factory()->create(['name' => 'Foo']); - $this->putAs("api/playlist/$playlist->id", ['name' => 'Qux'])->assertForbidden(); + $this->putAs("api/playlists/$playlist->id", ['name' => 'Qux'])->assertForbidden(); + self::assertSame('Foo', $playlist->refresh()->name); } public function testDeletePlaylist(): void @@ -125,7 +132,7 @@ class PlaylistTest extends TestCase /** @var Playlist $playlist */ $playlist = Playlist::factory()->create(); - $this->deleteAs("api/playlist/$playlist->id", [], $playlist->user); + $this->deleteAs("api/playlists/$playlist->id", [], $playlist->user); self::assertModelMissing($playlist); } @@ -135,7 +142,7 @@ class PlaylistTest extends TestCase /** @var Playlist $playlist */ $playlist = Playlist::factory()->create(); - $this->deleteAs("api/playlist/$playlist->id")->assertForbidden(); + $this->deleteAs("api/playlists/$playlist->id")->assertForbidden(); self::assertModelExists($playlist); } diff --git a/tests/Feature/V6/QueueTest.php b/tests/Feature/QueueTest.php similarity index 94% rename from tests/Feature/V6/QueueTest.php rename to tests/Feature/QueueTest.php index 7287c040..96f8e198 100644 --- a/tests/Feature/V6/QueueTest.php +++ b/tests/Feature/QueueTest.php @@ -1,6 +1,6 @@ once(); - $this->postAs("/api/$song->id/scrobble", ['timestamp' => 100], $user) + $this->postAs("/api/songs/$song->id/scrobble", ['timestamp' => 100], $user) ->assertNoContent(); } } diff --git a/tests/Feature/V6/SongSearchTest.php b/tests/Feature/SongSearchTest.php similarity index 91% rename from tests/Feature/V6/SongSearchTest.php rename to tests/Feature/SongSearchTest.php index 988f848f..88675005 100644 --- a/tests/Feature/V6/SongSearchTest.php +++ b/tests/Feature/SongSearchTest.php @@ -1,6 +1,6 @@ [ + '*' => self::JSON_STRUCTURE, + ], + 'links' => [ + 'first', + 'last', + 'prev', + 'next', + ], + 'meta' => [ + 'current_page', + 'from', + 'path', + 'per_page', + 'to', + ], + ]; + + public function testIndex(): void + { + Song::factory(10)->create(); + + $this->getAs('api/songs')->assertJsonStructure(self::JSON_COLLECTION_STRUCTURE); + $this->getAs('api/songs?sort=title&order=desc')->assertJsonStructure(self::JSON_COLLECTION_STRUCTURE); + } + + public function testShow(): void + { + /** @var Song $song */ + $song = Song::factory()->create(); + + $this->getAs('api/songs/' . $song->id)->assertJsonStructure(self::JSON_STRUCTURE); + } + + public function testDelete(): void + { + /** @var Collection|array $songs */ + $songs = Song::factory(3)->create(); + + /** @var User $admin */ + $admin = User::factory()->admin()->create(); + + $this->deleteAs('api/songs', ['songs' => $songs->pluck('id')->all()], $admin) + ->assertNoContent(); + + $songs->each(fn (Song $song) => $this->assertModelMissing($song)); + } + + public function testUnauthorizedDelete(): void + { + /** @var Collection|array $songs */ + $songs = Song::factory(3)->create(); + + $this->deleteAs('api/songs', ['songs' => $songs->pluck('id')->all()]) + ->assertForbidden(); + + $songs->each(fn (Song $song) => $this->assertModelExists($song)); } public function testSingleUpdateAllInfoNoCompilation(): void { + static::createSampleMediaSet(); + /** @var User $user */ $user = User::factory()->admin()->create(); @@ -57,6 +134,8 @@ class SongTest extends TestCase public function testSingleUpdateSomeInfoNoCompilation(): void { + static::createSampleMediaSet(); + /** @var User $user */ $user = User::factory()->admin()->create(); @@ -86,9 +165,11 @@ class SongTest extends TestCase public function testMultipleUpdateNoCompilation(): void { + static::createSampleMediaSet(); + /** @var User $user */ $user = User::factory()->admin()->create(); - $songIds = Song::query()->latest()->take(3)->pluck('id')->toArray(); + $songIds = Song::query()->latest()->take(3)->pluck('id')->all(); $this->putAs('/api/songs', [ 'songs' => $songIds, @@ -124,6 +205,8 @@ class SongTest extends TestCase public function testMultipleUpdateCreatingNewAlbumsAndArtists(): void { + static::createSampleMediaSet(); + /** @var User $user */ $user = User::factory()->admin()->create(); @@ -131,7 +214,7 @@ class SongTest extends TestCase $originalSongs = Song::query()->latest()->take(3)->get(); $this->putAs('/api/songs', [ - 'songs' => $originalSongs->pluck('id')->toArray(), + 'songs' => $originalSongs->pluck('id')->all(), 'data' => [ 'title' => 'Foo Bar', 'artist_name' => 'John Cena', @@ -162,6 +245,8 @@ class SongTest extends TestCase public function testSingleUpdateAllInfoWithCompilation(): void { + static::createSampleMediaSet(); + /** @var User $user */ $user = User::factory()->admin()->create(); @@ -205,6 +290,8 @@ class SongTest extends TestCase public function testUpdateSingleSongWithEmptyTrackAndDisc(): void { + static::createSampleMediaSet(); + /** @var User $user */ $user = User::factory()->admin()->create(); @@ -231,6 +318,8 @@ class SongTest extends TestCase public function testDeletingByChunk(): void { + Song::factory(5)->create(); + self::assertNotSame(0, Song::query()->count()); $ids = Song::query()->select('id')->get()->pluck('id')->all(); diff --git a/tests/Feature/V6/PlaylistSongTest.php b/tests/Feature/V6/PlaylistSongTest.php deleted file mode 100644 index 727d8168..00000000 --- a/tests/Feature/V6/PlaylistSongTest.php +++ /dev/null @@ -1,145 +0,0 @@ -create(); - $playlist->songs()->attach(Song::factory(5)->create()); - - $this->getAs('api/playlists/' . $playlist->id . '/songs', $playlist->user) - ->assertJsonStructure(['*' => SongTest::JSON_STRUCTURE]); - } - - public function testGetSmartPlaylist(): void - { - Song::factory()->create(['title' => 'A foo song']); - - /** @var Playlist $playlist */ - $playlist = Playlist::factory()->create([ - 'rules' => [ - [ - 'id' => '45368b8f-fec8-4b72-b826-6b295af0da65', - 'rules' => [ - [ - 'id' => '2a4548cd-c67f-44d4-8fec-34ff75c8a026', - 'model' => 'title', - 'operator' => 'contains', - 'value' => ['foo'], - ], - ], - ], - ], - ]); - - $this->getAs('api/playlists/' . $playlist->id . '/songs', $playlist->user) - ->assertJsonStructure(['*' => SongTest::JSON_STRUCTURE]); - } - - public function testNonOwnerCannotAccessPlaylist(): void - { - $user = User::factory()->create(); - - /** @var Playlist $playlist */ - $playlist = Playlist::factory()->for($user)->create(); - $playlist->songs()->attach(Song::factory(5)->create()); - - $this->getAs('api/playlists/' . $playlist->id . '/songs') - ->assertForbidden(); - } - - public function testAddSongsToPlaylist(): void - { - /** @var Playlist $playlist */ - $playlist = Playlist::factory()->create(); - - /** @var Collection|array $songs */ - $songs = Song::factory(2)->create(); - - $this->postAs('api/playlists/' . $playlist->id . '/songs', [ - 'songs' => $songs->map(static fn (Song $song) => $song->id)->all(), - ], $playlist->user) - ->assertNoContent(); - - self::assertEqualsCanonicalizing($songs->pluck('id')->all(), $playlist->songs->pluck('id')->all()); - } - - public function testRemoveSongsFromPlaylist(): void - { - /** @var Playlist $playlist */ - $playlist = Playlist::factory()->create(); - - $toRemainSongs = Song::factory(5)->create(); - - /** @var Collection|array $toBeRemovedSongs */ - $toBeRemovedSongs = Song::factory(2)->create(); - - $playlist->songs()->attach($toRemainSongs->merge($toBeRemovedSongs)); - - self::assertCount(7, $playlist->songs); - - $this->deleteAs('api/playlists/' . $playlist->id . '/songs', [ - 'songs' => $toBeRemovedSongs->map(static fn (Song $song) => $song->id)->all(), - ], $playlist->user) - ->assertNoContent(); - - $playlist->refresh(); - - self::assertEqualsCanonicalizing($toRemainSongs->pluck('id')->all(), $playlist->songs->pluck('id')->all()); - } - - public function testNonOwnerCannotModifyPlaylist(): void - { - $user = User::factory()->create(); - - /** @var Playlist $playlist */ - $playlist = Playlist::factory()->for($user)->create(); - - /** @var Song $song */ - $song = Song::factory()->create(); - - $this->postAs('api/playlists/' . $playlist->id . '/songs', ['songs' => [$song->id]]) - ->assertForbidden(); - - $this->deleteAs('api/playlists/' . $playlist->id . '/songs', ['songs' => [$song->id]]) - ->assertForbidden(); - } - - public function testSmartPlaylistContentCannotBeModified(): void - { - /** @var Playlist $playlist */ - $playlist = Playlist::factory()->create([ - 'rules' => [ - [ - 'id' => '45368b8f-fec8-4b72-b826-6b295af0da65', - 'rules' => [ - [ - 'id' => '2a4548cd-c67f-44d4-8fec-34ff75c8a026', - 'model' => 'title', - 'operator' => 'contains', - 'value' => ['foo'], - ], - ], - ], - ], - ]); - - /** @var Collection|array $songs */ - $songs = Song::factory(2)->create(); - $songIds = $songs->map(static fn (Song $song) => $song->id)->all(); - - $this->postAs('api/playlists/' . $playlist->id . '/songs', ['songs' => $songIds], $playlist->user) - ->assertForbidden(); - - $this->deleteAs('api/playlists/' . $playlist->id . '/songs', ['songs' => $songIds], $playlist->user) - ->assertForbidden(); - } -} diff --git a/tests/Feature/V6/PlaylistTest.php b/tests/Feature/V6/PlaylistTest.php deleted file mode 100644 index 0cc771cf..00000000 --- a/tests/Feature/V6/PlaylistTest.php +++ /dev/null @@ -1,149 +0,0 @@ -create(); - - /** @var array|Collection $songs */ - $songs = Song::factory(4)->create(); - - $this->postAs('api/playlists', [ - 'name' => 'Foo Bar', - 'songs' => $songs->pluck('id')->all(), - 'rules' => [], - ], $user) - ->assertJsonStructure(self::JSON_STRUCTURE); - - /** @var Playlist $playlist */ - $playlist = Playlist::query()->orderByDesc('id')->first(); - - self::assertSame('Foo Bar', $playlist->name); - self::assertTrue($playlist->user->is($user)); - self::assertNull($playlist->folder_id); - self::assertEqualsCanonicalizing($songs->pluck('id')->all(), $playlist->songs->pluck('id')->all()); - } - - public function testCreatingSmartPlaylist(): void - { - /** @var User $user */ - $user = User::factory()->create(); - - $rule = SmartPlaylistRule::create([ - 'model' => 'artist.name', - 'operator' => SmartPlaylistRule::OPERATOR_IS, - 'value' => ['Bob Dylan'], - ]); - - $this->postAs('api/playlists', [ - 'name' => 'Smart Foo Bar', - 'rules' => [ - [ - 'id' => '2a4548cd-c67f-44d4-8fec-34ff75c8a026', - 'rules' => [$rule->toArray()], - ], - ], - ], $user)->assertJsonStructure(self::JSON_STRUCTURE); - - /** @var Playlist $playlist */ - $playlist = Playlist::query()->orderByDesc('id')->first(); - - self::assertSame('Smart Foo Bar', $playlist->name); - self::assertTrue($playlist->user->is($user)); - self::assertTrue($playlist->is_smart); - self::assertCount(1, $playlist->rule_groups); - self::assertNull($playlist->folder_id); - self::assertTrue($rule->equals($playlist->rule_groups[0]->rules[0])); - } - - public function testCreatingSmartPlaylistFailsIfSongsProvided(): void - { - $this->postAs('api/playlists', [ - 'name' => 'Smart Foo Bar', - 'rules' => [ - [ - 'id' => '2a4548cd-c67f-44d4-8fec-34ff75c8a026', - 'rules' => [ - SmartPlaylistRule::create([ - 'model' => 'artist.name', - 'operator' => SmartPlaylistRule::OPERATOR_IS, - 'value' => ['Bob Dylan'], - ])->toArray(), - ], - ], - ], - 'songs' => Song::factory(3)->create()->pluck('id')->all(), - ])->assertUnprocessable(); - } - - public function testCreatingPlaylistWithNonExistentSongsFails(): void - { - $this->postAs('api/playlists', [ - 'name' => 'Foo Bar', - 'rules' => [], - 'songs' => ['foo'], - ]) - ->assertUnprocessable(); - } - - public function testUpdatePlaylistName(): void - { - /** @var Playlist $playlist */ - $playlist = Playlist::factory()->create(['name' => 'Foo']); - - $this->putAs("api/playlists/$playlist->id", ['name' => 'Bar'], $playlist->user) - ->assertJsonStructure(self::JSON_STRUCTURE); - - self::assertSame('Bar', $playlist->refresh()->name); - } - - public function testNonOwnerCannotUpdatePlaylist(): void - { - /** @var Playlist $playlist */ - $playlist = Playlist::factory()->create(['name' => 'Foo']); - - $this->putAs("api/playlists/$playlist->id", ['name' => 'Qux'])->assertForbidden(); - self::assertSame('Foo', $playlist->refresh()->name); - } - - public function testDeletePlaylist(): void - { - /** @var Playlist $playlist */ - $playlist = Playlist::factory()->create(); - - $this->deleteAs("api/playlists/$playlist->id", [], $playlist->user); - - self::assertModelMissing($playlist); - } - - public function testNonOwnerCannotDeletePlaylist(): void - { - /** @var Playlist $playlist */ - $playlist = Playlist::factory()->create(); - - $this->deleteAs("api/playlists/$playlist->id")->assertForbidden(); - - self::assertModelExists($playlist); - } -} diff --git a/tests/Feature/V6/SongTest.php b/tests/Feature/V6/SongTest.php deleted file mode 100644 index 3f216dd6..00000000 --- a/tests/Feature/V6/SongTest.php +++ /dev/null @@ -1,92 +0,0 @@ - [ - '*' => self::JSON_STRUCTURE, - ], - 'links' => [ - 'first', - 'last', - 'prev', - 'next', - ], - 'meta' => [ - 'current_page', - 'from', - 'path', - 'per_page', - 'to', - ], - ]; - - public function testIndex(): void - { - Song::factory(10)->create(); - - $this->getAs('api/songs')->assertJsonStructure(self::JSON_COLLECTION_STRUCTURE); - $this->getAs('api/songs?sort=title&order=desc')->assertJsonStructure(self::JSON_COLLECTION_STRUCTURE); - } - - public function testShow(): void - { - /** @var Song $song */ - $song = Song::factory()->create(); - - $this->getAs('api/songs/' . $song->id)->assertJsonStructure(self::JSON_STRUCTURE); - } - - public function testDelete(): void - { - /** @var Collection|array $songs */ - $songs = Song::factory(3)->create(); - - /** @var User $admin */ - $admin = User::factory()->admin()->create(); - - $this->deleteAs('api/songs', ['songs' => $songs->pluck('id')->toArray()], $admin) - ->assertNoContent(); - - $songs->each(fn (Song $song) => $this->assertModelMissing($song)); - } - - public function testUnauthorizedDelete(): void - { - /** @var Collection|array $songs */ - $songs = Song::factory(3)->create(); - - $this->deleteAs('api/songs', ['songs' => $songs->pluck('id')->toArray()]) - ->assertForbidden(); - - $songs->each(fn (Song $song) => $this->assertModelExists($song)); - } -} diff --git a/tests/Feature/V6/TestCase.php b/tests/Feature/V6/TestCase.php deleted file mode 100644 index 1c9a9e3e..00000000 --- a/tests/Feature/V6/TestCase.php +++ /dev/null @@ -1,15 +0,0 @@ -create(); /** @var Collection $interactions */ - $interactions = Interaction::factory(3)->create([ - 'user_id' => $user->id, - 'liked' => true, - ]); + $interactions = Interaction::factory(3)->for($user)->create(['liked' => true]); $this->interactionService->batchUnlike($interactions->pluck('song.id')->all(), $user); diff --git a/tests/TestCase.php b/tests/TestCase.php index 4b0fb9d3..5f70fa1f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -39,20 +39,15 @@ abstract class TestCase extends BaseTestCase $artist = Artist::factory()->create(); /** @var array $albums */ - $albums = Album::factory(3)->create([ - 'artist_id' => $artist->id, - ]); + $albums = Album::factory(3)->for($artist)->create(); // 7-15 songs per albums foreach ($albums as $album) { - Song::factory(random_int(7, 15))->create([ - 'album_id' => $album->id, - 'artist_id' => $artist->id, - ]); + Song::factory(random_int(7, 15))->for($artist)->for($album)->create(); } } - protected static function getNonPublicProperty($object, string $property) // @phpcs:ignore + protected static function getNonPublicProperty($object, string $property): mixed { $reflection = new ReflectionClass($object); $property = $reflection->getProperty($property); diff --git a/tests/Unit/Services/PlaylistFolderServiceTest.php b/tests/Unit/Services/PlaylistFolderServiceTest.php index 03cf514b..13895fce 100644 --- a/tests/Unit/Services/PlaylistFolderServiceTest.php +++ b/tests/Unit/Services/PlaylistFolderServiceTest.php @@ -51,7 +51,7 @@ class PlaylistFolderServiceTest extends TestCase /** @var PlaylistFolder $folder */ $folder = PlaylistFolder::factory()->create(); - $this->service->addPlaylistsToFolder($folder, $playlists->pluck('id')->toArray()); + $this->service->addPlaylistsToFolder($folder, $playlists->pluck('id')->all()); self::assertCount(3, $folder->playlists); } @@ -62,9 +62,9 @@ class PlaylistFolderServiceTest extends TestCase $folder = PlaylistFolder::factory()->create(); /** @var Collection|array $playlists */ - $playlists = Playlist::factory()->count(3)->create(['folder_id' => $folder->id]); + $playlists = Playlist::factory()->count(3)->for($folder, 'folder')->create(); - $this->service->movePlaylistsToRootLevel($playlists->pluck('id')->toArray()); + $this->service->movePlaylistsToRootLevel($playlists->pluck('id')->all()); self::assertCount(0, $folder->playlists); diff --git a/tests/Unit/Services/SpotifyServiceTest.php b/tests/Unit/Services/SpotifyServiceTest.php index 19a9f97a..56c04856 100644 --- a/tests/Unit/Services/SpotifyServiceTest.php +++ b/tests/Unit/Services/SpotifyServiceTest.php @@ -53,11 +53,8 @@ class SpotifyServiceTest extends TestCase public function testTryGetAlbumImage(): void { - /** @var Artist $artist */ - $artist = Artist::factory(['name' => 'Foo'])->create(); - /** @var Album $album */ - $album = Album::factory(['name' => 'Bar', 'artist_id' => $artist->id])->create(); + $album = Album::factory(['name' => 'Bar'])->for(Artist::factory(['name' => 'Foo']))->create(); $this->client ->shouldReceive('search')