mirror of
https://github.com/koel/koel
synced 2024-11-23 20:53:05 +00:00
feat(plus): song visibility behaviors for collaborative playlists
This commit is contained in:
parent
e874c80b26
commit
5c5c538478
75 changed files with 892 additions and 375 deletions
16
app/Events/NewPlaylistCollaboratorJoined.php
Normal file
16
app/Events/NewPlaylistCollaboratorJoined.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\PlaylistCollaborationToken;
|
||||
use App\Models\User;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class NewPlaylistCollaboratorJoined extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(public User $collaborator, public PlaylistCollaborationToken $token)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class CannotRemoveOwnerFromPlaylistException extends Exception
|
||||
{
|
||||
}
|
9
app/Exceptions/KoelPlusRequiredException.php
Normal file
9
app/Exceptions/KoelPlusRequiredException.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class KoelPlusRequiredException extends Exception
|
||||
{
|
||||
}
|
9
app/Exceptions/NotAPlaylistCollaboratorException.php
Normal file
9
app/Exceptions/NotAPlaylistCollaboratorException.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class NotAPlaylistCollaboratorException extends Exception
|
||||
{
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PlaylistCollaborationTokenExpiredException extends Exception
|
||||
{
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class SmartPlaylistsAreNotCollaborativeException extends Exception
|
||||
{
|
||||
}
|
|
@ -2,24 +2,22 @@
|
|||
|
||||
namespace App\Http\Controllers\API\PlaylistCollaboration;
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Exceptions\CannotRemoveOwnerFromPlaylistException;
|
||||
use App\Exceptions\KoelPlusRequiredException;
|
||||
use App\Exceptions\NotAPlaylistCollaboratorException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\PlaylistCollaboration\PlaylistCollaboratorDestroyRequest;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\User;
|
||||
use App\Repositories\UserRepository;
|
||||
use App\Services\PlaylistCollaborationService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class PlaylistCollaboratorController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __construct(
|
||||
private PlaylistCollaborationService $service,
|
||||
private UserRepository $userRepository,
|
||||
private ?Authenticatable $user
|
||||
) {
|
||||
public function __construct(private PlaylistCollaborationService $service, private UserRepository $userRepository)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(Playlist $playlist)
|
||||
|
@ -36,20 +34,16 @@ class PlaylistCollaboratorController extends Controller
|
|||
/** @var User $collaborator */
|
||||
$collaborator = $this->userRepository->getOne($request->collaborator);
|
||||
|
||||
abort_unless(License::isPlus(), Response::HTTP_FORBIDDEN, 'This feature is only available for Plus users.');
|
||||
try {
|
||||
$this->service->removeCollaborator($playlist, $collaborator);
|
||||
|
||||
abort_if(
|
||||
$collaborator->is($this->user),
|
||||
Response::HTTP_FORBIDDEN,
|
||||
'You cannot remove yourself from your own playlist.'
|
||||
);
|
||||
|
||||
abort_unless(
|
||||
$playlist->hasCollaborator($collaborator),
|
||||
Response::HTTP_NOT_FOUND,
|
||||
'This user is not a collaborator of this playlist.'
|
||||
);
|
||||
|
||||
$this->service->removeCollaborator($playlist, $collaborator);
|
||||
return response()->noContent();
|
||||
} catch (KoelPlusRequiredException) {
|
||||
abort(Response::HTTP_FORBIDDEN, 'This feature is only available for Plus users.');
|
||||
} catch (CannotRemoveOwnerFromPlaylistException) {
|
||||
abort(Response::HTTP_FORBIDDEN, 'You cannot remove yourself from your own playlist.');
|
||||
} catch (NotAPlaylistCollaboratorException) {
|
||||
abort(Response::HTTP_NOT_FOUND, 'This user is not a collaborator of this playlist.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,9 @@ use App\Repositories\SongRepository;
|
|||
use App\Services\PlaylistService;
|
||||
use App\Services\SmartPlaylistService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class PlaylistSongController extends Controller
|
||||
{
|
||||
|
@ -36,11 +38,7 @@ class PlaylistSongController extends Controller
|
|||
|
||||
$this->authorize('collaborate', $playlist);
|
||||
|
||||
$songs = $this->songRepository->getByStandardPlaylist($playlist, $this->user);
|
||||
|
||||
return License::isPlus()
|
||||
? CollaborativeSongResource::collection($songs)
|
||||
: SongResource::collection($songs);
|
||||
return self::createResourceCollection($this->songRepository->getByStandardPlaylist($playlist, $this->user));
|
||||
}
|
||||
|
||||
public function store(Playlist $playlist, AddSongsToPlaylistRequest $request)
|
||||
|
@ -50,17 +48,21 @@ class PlaylistSongController extends Controller
|
|||
$this->authorize('collaborate', $playlist);
|
||||
|
||||
$songs = $this->songRepository->getMany(ids: $request->songs, scopedUser: $this->user);
|
||||
$songs->each(fn ($song) => $this->authorize('access', $song));
|
||||
|
||||
$this->playlistService->addSongsToPlaylist($playlist, $songs, $this->user);
|
||||
return self::createResourceCollection($this->playlistService->addSongsToPlaylist($playlist, $songs, $this->user));
|
||||
}
|
||||
|
||||
return response()->noContent();
|
||||
private static function createResourceCollection(Collection $songs): ResourceCollection
|
||||
{
|
||||
return License::isPlus()
|
||||
? CollaborativeSongResource::collection($songs)
|
||||
: SongResource::collection($songs);
|
||||
}
|
||||
|
||||
public function destroy(Playlist $playlist, RemoveSongsFromPlaylistRequest $request)
|
||||
{
|
||||
abort_if($playlist->is_smart, Response::HTTP_FORBIDDEN, 'Smart playlist content is automatically generated');
|
||||
|
||||
|
||||
$this->authorize('collaborate', $playlist);
|
||||
$this->playlistService->removeSongsFromPlaylist($playlist, $request->songs);
|
||||
|
||||
|
|
|
@ -17,8 +17,6 @@ class PrivatizeSongsController extends Controller
|
|||
$songs = Song::query()->findMany($request->songs);
|
||||
$songs->each(fn ($song) => $this->authorize('own', $song));
|
||||
|
||||
$songService->privatizeSongs($songs);
|
||||
|
||||
return response()->noContent();
|
||||
return response()->json($songService->markSongsAsPrivate($songs));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ class PublicizeSongsController extends Controller
|
|||
$songs = Song::query()->findMany($request->songs);
|
||||
$songs->each(fn ($song) => $this->authorize('own', $song));
|
||||
|
||||
$songService->publicizeSongs($songs);
|
||||
$songService->markSongsAsPublic($songs);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use App\Http\Requests\API\AcceptUserInvitationRequest;
|
|||
use App\Http\Requests\API\GetUserInvitationRequest;
|
||||
use App\Http\Requests\API\InviteUserRequest;
|
||||
use App\Http\Requests\API\RevokeUserInvitationRequest;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Http\Resources\UserProspectResource;
|
||||
use App\Models\User;
|
||||
use App\Services\AuthenticationService;
|
||||
use App\Services\UserInvitationService;
|
||||
|
@ -37,13 +37,13 @@ class UserInvitationController extends Controller
|
|||
$this->invitor
|
||||
);
|
||||
|
||||
return UserResource::collection($invitees);
|
||||
return UserProspectResource::collection($invitees);
|
||||
}
|
||||
|
||||
public function get(GetUserInvitationRequest $request)
|
||||
{
|
||||
try {
|
||||
return UserResource::make($this->invitationService->getUserProspectByToken($request->token));
|
||||
return UserProspectResource::make($this->invitationService->getUserProspectByToken($request->token));
|
||||
} catch (InvitationNotFoundException) {
|
||||
abort(Response::HTTP_NOT_FOUND, 'The invitation token is invalid.');
|
||||
}
|
||||
|
|
|
@ -7,6 +7,35 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||
|
||||
class AlbumResource extends JsonResource
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'id',
|
||||
'name',
|
||||
'artist_id',
|
||||
'artist_name',
|
||||
'cover',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
public const PAGINATION_JSON_STRUCTURE = [
|
||||
'data' => [
|
||||
'*' => self::JSON_STRUCTURE,
|
||||
],
|
||||
'links' => [
|
||||
'first',
|
||||
'last',
|
||||
'prev',
|
||||
'next',
|
||||
],
|
||||
'meta' => [
|
||||
'current_page',
|
||||
'from',
|
||||
'path',
|
||||
'per_page',
|
||||
'to',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(private Album $album)
|
||||
{
|
||||
parent::__construct($album);
|
||||
|
|
|
@ -7,6 +7,33 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||
|
||||
class ArtistResource extends JsonResource
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'id',
|
||||
'name',
|
||||
'image',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
public const PAGINATION_JSON_STRUCTURE = [
|
||||
'data' => [
|
||||
'*' => self::JSON_STRUCTURE,
|
||||
],
|
||||
'links' => [
|
||||
'first',
|
||||
'last',
|
||||
'prev',
|
||||
'next',
|
||||
],
|
||||
'meta' => [
|
||||
'current_page',
|
||||
'from',
|
||||
'path',
|
||||
'per_page',
|
||||
'to',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(private Artist $artist)
|
||||
{
|
||||
parent::__construct($artist);
|
||||
|
|
|
@ -7,6 +7,14 @@ use Carbon\Carbon;
|
|||
|
||||
class CollaborativeSongResource extends SongResource
|
||||
{
|
||||
public const JSON_STRUCTURE = SongResource::JSON_STRUCTURE + [
|
||||
'collaboration' => [
|
||||
'user' => PlaylistCollaboratorResource::JSON_STRUCTURE,
|
||||
'added_at',
|
||||
'fmt_added_at',
|
||||
],
|
||||
];
|
||||
|
||||
/** @return array<mixed> */
|
||||
public function toArray($request): array
|
||||
{
|
||||
|
|
|
@ -7,6 +7,18 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||
|
||||
class ExcerptSearchResource extends JsonResource
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'songs' => [
|
||||
SongResource::JSON_STRUCTURE,
|
||||
],
|
||||
'artists' => [
|
||||
ArtistResource::JSON_STRUCTURE,
|
||||
],
|
||||
'albums' => [
|
||||
AlbumResource::JSON_STRUCTURE,
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(private ExcerptSearchResult $result)
|
||||
{
|
||||
parent::__construct($result);
|
||||
|
|
|
@ -7,6 +7,13 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||
|
||||
class GenreResource extends JsonResource
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'name',
|
||||
'song_count',
|
||||
'length',
|
||||
];
|
||||
|
||||
public function __construct(private Genre $genre)
|
||||
{
|
||||
parent::__construct($genre);
|
||||
|
|
|
@ -7,6 +7,16 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||
|
||||
class InteractionResource extends JsonResource
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'id',
|
||||
'songId',
|
||||
'song_id',
|
||||
'liked',
|
||||
'playCount',
|
||||
'play_count',
|
||||
];
|
||||
|
||||
public function __construct(private Interaction $interaction)
|
||||
{
|
||||
parent::__construct($interaction);
|
||||
|
|
|
@ -7,6 +7,11 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||
|
||||
class PlaylistCollaborationTokenResource extends JsonResource
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'token',
|
||||
];
|
||||
|
||||
public function __construct(private PlaylistCollaborationToken $token)
|
||||
{
|
||||
parent::__construct($token);
|
||||
|
|
|
@ -7,6 +7,13 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||
|
||||
class PlaylistCollaboratorResource extends JsonResource
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'id',
|
||||
'name',
|
||||
'avatar',
|
||||
];
|
||||
|
||||
public function __construct(private PlaylistCollaborator $collaborator)
|
||||
{
|
||||
parent::__construct($collaborator);
|
||||
|
|
|
@ -7,6 +7,14 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||
|
||||
class PlaylistFolderResource extends JsonResource
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'id',
|
||||
'name',
|
||||
'user_id',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
public function __construct(private PlaylistFolder $folder)
|
||||
{
|
||||
parent::__construct($folder);
|
||||
|
|
|
@ -2,12 +2,23 @@
|
|||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Models\Playlist;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class PlaylistResource extends JsonResource
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'id',
|
||||
'name',
|
||||
'folder_id',
|
||||
'user_id',
|
||||
'is_smart',
|
||||
'rules',
|
||||
'own_songs_only',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
public function __construct(private Playlist $playlist)
|
||||
{
|
||||
parent::__construct($playlist);
|
||||
|
@ -23,8 +34,8 @@ class PlaylistResource extends JsonResource
|
|||
'folder_id' => $this->playlist->folder_id,
|
||||
'user_id' => $this->playlist->user_id,
|
||||
'is_smart' => $this->playlist->is_smart,
|
||||
'is_collaborative' => $this->playlist->is_collaborative,
|
||||
'rules' => $this->playlist->rules,
|
||||
'collaborators' => License::isPlus() ? UserResource::collection($this->playlist->collaborators) : [],
|
||||
'own_songs_only' => $this->playlist->own_songs_only,
|
||||
'created_at' => $this->playlist->created_at,
|
||||
];
|
||||
|
|
|
@ -7,6 +7,14 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||
|
||||
class QueueStateResource extends JsonResource
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'songs' => [
|
||||
SongResource::JSON_STRUCTURE,
|
||||
],
|
||||
'current_song',
|
||||
'playback_position',
|
||||
];
|
||||
|
||||
public function __construct(private QueueState $state)
|
||||
{
|
||||
parent::__construct($state);
|
||||
|
|
|
@ -7,6 +7,48 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||
|
||||
class SongResource extends JsonResource
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'id',
|
||||
'title',
|
||||
'lyrics',
|
||||
'album_id',
|
||||
'album_name',
|
||||
'artist_id',
|
||||
'artist_name',
|
||||
'album_artist_id',
|
||||
'album_artist_name',
|
||||
'album_cover',
|
||||
'length',
|
||||
'liked',
|
||||
'play_count',
|
||||
'track',
|
||||
'genre',
|
||||
'year',
|
||||
'disc',
|
||||
'is_public',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
public const PAGINATION_JSON_STRUCTURE = [
|
||||
'data' => [
|
||||
'*' => self::JSON_STRUCTURE,
|
||||
],
|
||||
'links' => [
|
||||
'first',
|
||||
'last',
|
||||
'prev',
|
||||
'next',
|
||||
],
|
||||
'meta' => [
|
||||
'current_page',
|
||||
'from',
|
||||
'path',
|
||||
'per_page',
|
||||
'to',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(protected Song $song)
|
||||
{
|
||||
parent::__construct($song);
|
||||
|
|
38
app/Http/Resources/UserProspectResource.php
Normal file
38
app/Http/Resources/UserProspectResource.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class UserProspectResource extends JsonResource
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'id',
|
||||
'name',
|
||||
'email',
|
||||
'avatar',
|
||||
'is_admin',
|
||||
'is_prospect',
|
||||
];
|
||||
|
||||
public function __construct(private User $user)
|
||||
{
|
||||
parent::__construct($user);
|
||||
}
|
||||
|
||||
/** @return array<mixed> */
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'type' => 'users',
|
||||
'id' => $this->user->id,
|
||||
'name' => $this->user->name,
|
||||
'email' => $this->user->email,
|
||||
'avatar' => $this->user->avatar,
|
||||
'is_admin' => $this->user->is_admin,
|
||||
'is_prospect' => $this->user->is_prospect,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -7,7 +7,18 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||
|
||||
class UserResource extends JsonResource
|
||||
{
|
||||
public function __construct(private User $user, private bool $includePreferences = false)
|
||||
public const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'id',
|
||||
'name',
|
||||
'email',
|
||||
'avatar',
|
||||
'is_admin',
|
||||
'preferences',
|
||||
'is_prospect',
|
||||
];
|
||||
|
||||
public function __construct(private User $user)
|
||||
{
|
||||
parent::__construct($user);
|
||||
}
|
||||
|
@ -22,7 +33,7 @@ class UserResource extends JsonResource
|
|||
'email' => $this->user->email,
|
||||
'avatar' => $this->user->avatar,
|
||||
'is_admin' => $this->user->is_admin,
|
||||
'preferences' => $this->when($this->includePreferences, $this->user->preferences),
|
||||
'preferences' => $this->user->preferences,
|
||||
'is_prospect' => $this->user->is_prospect,
|
||||
];
|
||||
}
|
||||
|
|
19
app/Listeners/MakePlaylistSongsPublic.php
Normal file
19
app/Listeners/MakePlaylistSongsPublic.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\NewPlaylistCollaboratorJoined;
|
||||
use App\Services\PlaylistService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class MakePlaylistSongsPublic implements ShouldQueue
|
||||
{
|
||||
public function __construct(private PlaylistService $service)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(NewPlaylistCollaboratorJoined $event): void
|
||||
{
|
||||
$this->service->makePlaylistSongsPublic($event->token->playlist);
|
||||
}
|
||||
}
|
|
@ -30,6 +30,7 @@ use Laravel\Scout\Searchable;
|
|||
* @property Carbon $created_at
|
||||
* @property bool $own_songs_only
|
||||
* @property Collection|array<array-key, User> $collaborators
|
||||
* @property-read bool $is_collaborative
|
||||
*/
|
||||
class Playlist extends Model
|
||||
{
|
||||
|
@ -144,7 +145,9 @@ class Playlist extends Model
|
|||
|
||||
protected function isCollaborative(): Attribute
|
||||
{
|
||||
return Attribute::get(fn (): bool => LicenseFacade::isPlus() && $this->collaborators->isNotEmpty());
|
||||
return Attribute::get(fn (): bool => !$this->is_smart &&
|
||||
LicenseFacade::isPlus()
|
||||
&& $this->collaborators->isNotEmpty());
|
||||
}
|
||||
|
||||
/** @return array<mixed> */
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Models;
|
|||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Str;
|
||||
|
@ -12,10 +13,13 @@ use Illuminate\Support\Str;
|
|||
* @property string $token
|
||||
* @property Carbon $created_at
|
||||
* @property-read bool $expired
|
||||
* @property string $playlist_id
|
||||
* @property Playlist $playlist
|
||||
*/
|
||||
class PlaylistCollaborationToken extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(static function (PlaylistCollaborationToken $token): void {
|
||||
|
|
|
@ -6,11 +6,13 @@ use App\Events\LibraryChanged;
|
|||
use App\Events\MediaScanCompleted;
|
||||
use App\Events\MultipleSongsLiked;
|
||||
use App\Events\MultipleSongsUnliked;
|
||||
use App\Events\NewPlaylistCollaboratorJoined;
|
||||
use App\Events\PlaybackStarted;
|
||||
use App\Events\SongLikeToggled;
|
||||
use App\Listeners\DeleteNonExistingRecordsPostSync;
|
||||
use App\Listeners\LoveMultipleTracksOnLastfm;
|
||||
use App\Listeners\LoveTrackOnLastfm;
|
||||
use App\Listeners\MakePlaylistSongsPublic;
|
||||
use App\Listeners\PruneLibrary;
|
||||
use App\Listeners\UnloveMultipleTracksOnLastfm;
|
||||
use App\Listeners\UpdateLastfmNowPlaying;
|
||||
|
@ -46,6 +48,10 @@ class EventServiceProvider extends BaseServiceProvider
|
|||
DeleteNonExistingRecordsPostSync::class,
|
||||
WriteSyncLog::class,
|
||||
],
|
||||
|
||||
NewPlaylistCollaboratorJoined::class => [
|
||||
MakePlaylistSongsPublic::class,
|
||||
],
|
||||
];
|
||||
|
||||
public function boot(): void
|
||||
|
|
|
@ -118,11 +118,11 @@ class SongRepository extends Repository
|
|||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->sort($sortColumn, $sortDirection)
|
||||
->limit($limit)
|
||||
->get();
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->sort($sortColumn, $sortDirection)
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
/** @return Collection|array<array-key, Song> */
|
||||
|
@ -186,16 +186,15 @@ class SongRepository extends Repository
|
|||
->when(License::isPlus(), static function (SongBuilder $query): SongBuilder {
|
||||
return
|
||||
$query->join('users as collaborators', 'playlist_song.user_id', '=', 'collaborators.id')
|
||||
->addSelect(
|
||||
'collaborators.id as collaborator_id',
|
||||
'collaborators.name as collaborator_name',
|
||||
'collaborators.email as collaborator_email',
|
||||
'playlist_song.created_at as added_at'
|
||||
);
|
||||
->addSelect(
|
||||
'collaborators.id as collaborator_id',
|
||||
'collaborators.name as collaborator_name',
|
||||
'collaborators.email as collaborator_email',
|
||||
'playlist_song.created_at as added_at'
|
||||
);
|
||||
})
|
||||
->where('playlists.id', $playlist->id)
|
||||
->orderBy('songs.title')
|
||||
->logSql()
|
||||
->get();
|
||||
}
|
||||
|
||||
|
@ -228,6 +227,35 @@ class SongRepository extends Repository
|
|||
return $inThatOrder ? $songs->orderByArray($ids) : $songs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets several songs, but also includes collaborative information.
|
||||
*
|
||||
* @return Collection|array<array-key, Song>
|
||||
*/
|
||||
public function getManyInCollaborativeContext(array $ids, ?User $scopedUser = null): Collection
|
||||
{
|
||||
/** @var ?User $scopedUser */
|
||||
$scopedUser ??= $this->auth->user();
|
||||
|
||||
return Song::query()
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->when(License::isPlus(), static function (SongBuilder $query): SongBuilder {
|
||||
return
|
||||
$query->leftJoin('playlist_song', 'songs.id', '=', 'playlist_song.song_id')
|
||||
->leftJoin('playlists', 'playlists.id', '=', 'playlist_song.playlist_id')
|
||||
->join('users as collaborators', 'playlist_song.user_id', '=', 'collaborators.id')
|
||||
->addSelect(
|
||||
'collaborators.id as collaborator_id',
|
||||
'collaborators.name as collaborator_name',
|
||||
'collaborators.email as collaborator_email',
|
||||
'playlist_song.created_at as added_at'
|
||||
);
|
||||
})
|
||||
->whereIn('songs.id', $ids)
|
||||
->get();
|
||||
}
|
||||
|
||||
/** @param string $id */
|
||||
public function getOne($id, ?User $scopedUser = null): Song
|
||||
{
|
||||
|
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Events\NewPlaylistCollaboratorJoined;
|
||||
use App\Exceptions\CannotRemoveOwnerFromPlaylistException;
|
||||
use App\Exceptions\KoelPlusRequiredException;
|
||||
use App\Exceptions\NotAPlaylistCollaboratorException;
|
||||
use App\Exceptions\PlaylistCollaborationTokenExpiredException;
|
||||
use App\Exceptions\SmartPlaylistsAreNotCollaborativeException;
|
||||
use App\Facades\License;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\PlaylistCollaborationToken;
|
||||
|
@ -9,14 +15,14 @@ use App\Models\User;
|
|||
use App\Values\PlaylistCollaborator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class PlaylistCollaborationService
|
||||
{
|
||||
public function createToken(Playlist $playlist): PlaylistCollaborationToken
|
||||
{
|
||||
self::assertKoelPlus();
|
||||
Assert::false($playlist->is_smart, 'Smart playlists are not collaborative.');
|
||||
|
||||
throw_if($playlist->is_smart, SmartPlaylistsAreNotCollaborativeException::class);
|
||||
|
||||
return $playlist->collaborationTokens()->create();
|
||||
}
|
||||
|
@ -28,7 +34,7 @@ class PlaylistCollaborationService
|
|||
/** @var PlaylistCollaborationToken $collaborationToken */
|
||||
$collaborationToken = PlaylistCollaborationToken::query()->where('token', $token)->firstOrFail();
|
||||
|
||||
Assert::false($collaborationToken->expired, 'The token has expired.');
|
||||
throw_if($collaborationToken->expired, PlaylistCollaborationTokenExpiredException::class);
|
||||
|
||||
if ($collaborationToken->playlist->ownedBy($user)) {
|
||||
return $collaborationToken->playlist;
|
||||
|
@ -36,6 +42,10 @@ class PlaylistCollaborationService
|
|||
|
||||
$collaborationToken->playlist->addCollaborator($user);
|
||||
|
||||
// Now that we have at least one external collaborator, the songs in the playlist should be made public.
|
||||
// Here we dispatch an event for that to happen.
|
||||
event(new NewPlaylistCollaboratorJoined($user, $collaborationToken));
|
||||
|
||||
return $collaborationToken->playlist;
|
||||
}
|
||||
|
||||
|
@ -55,6 +65,9 @@ class PlaylistCollaborationService
|
|||
{
|
||||
self::assertKoelPlus();
|
||||
|
||||
throw_if($user->is($playlist->user), CannotRemoveOwnerFromPlaylistException::class);
|
||||
throw_if(!$playlist->hasCollaborator($user), NotAPlaylistCollaboratorException::class);
|
||||
|
||||
DB::transaction(static function () use ($playlist, $user): void {
|
||||
$playlist->collaborators()->detach($user);
|
||||
$playlist->songs()->wherePivot('user_id', $user->id)->detach();
|
||||
|
@ -63,6 +76,6 @@ class PlaylistCollaborationService
|
|||
|
||||
private static function assertKoelPlus(): void
|
||||
{
|
||||
Assert::true(License::isPlus(), 'Playlist collaboration is only available with Koel Plus.');
|
||||
throw_unless(License::isPlus(), KoelPlusRequiredException::class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ use App\Models\Playlist;
|
|||
use App\Models\PlaylistFolder as Folder;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
use App\Repositories\SongRepository;
|
||||
use App\Values\SmartPlaylistRuleGroupCollection;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
@ -16,6 +17,10 @@ use Webmozart\Assert\Assert;
|
|||
|
||||
class PlaylistService
|
||||
{
|
||||
public function __construct(private SongRepository $songRepository)
|
||||
{
|
||||
}
|
||||
|
||||
public function createPlaylist(
|
||||
string $name,
|
||||
User $user,
|
||||
|
@ -78,16 +83,29 @@ class PlaylistService
|
|||
return $playlist;
|
||||
}
|
||||
|
||||
public function addSongsToPlaylist(Playlist $playlist, Collection|Song|array $songs, User $user): void
|
||||
public function addSongsToPlaylist(Playlist $playlist, Collection|Song|array $songs, User $user): Collection
|
||||
{
|
||||
$playlist->addSongs(
|
||||
Collection::wrap($songs)->filter(static fn ($song): bool => !$playlist->songs->contains($song)),
|
||||
$user
|
||||
);
|
||||
return DB::transaction(function () use ($playlist, $songs, $user) {
|
||||
$songs = Collection::wrap($songs);
|
||||
$playlist->addSongs($songs->filter(static fn ($song): bool => !$playlist->songs->contains($song)), $user);
|
||||
|
||||
// if the playlist is collaborative, make the songs public
|
||||
if ($playlist->is_collaborative) {
|
||||
$this->makePlaylistSongsPublic($playlist);
|
||||
}
|
||||
|
||||
// we want a fresh copy of the songs with the possibly updated visibility
|
||||
return $this->songRepository->getManyInCollaborativeContext(ids: $songs->pluck('id')->all(), scopedUser: $user);
|
||||
});
|
||||
}
|
||||
|
||||
public function removeSongsFromPlaylist(Playlist $playlist, Collection|Song|array $songs): void
|
||||
{
|
||||
$playlist->removeSongs($songs);
|
||||
}
|
||||
|
||||
public function makePlaylistSongsPublic(Playlist $playlist): void
|
||||
{
|
||||
$playlist->songs()->where('is_public', false)->update(['is_public' => true]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Services;
|
||||
|
||||
use App\Events\LibraryChanged;
|
||||
use App\Facades\License;
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Models\Song;
|
||||
|
@ -82,14 +83,36 @@ class SongService
|
|||
return $this->songRepository->getOne($song->id);
|
||||
}
|
||||
|
||||
public function publicizeSongs(Collection $songs): void
|
||||
public function markSongsAsPublic(Collection $songs): void
|
||||
{
|
||||
Song::query()->whereIn('id', $songs->pluck('id'))->update(['is_public' => true]);
|
||||
}
|
||||
|
||||
public function privatizeSongs(Collection $songs): void
|
||||
/** @return array<string> IDs of songs that are marked as private */
|
||||
public function markSongsAsPrivate(Collection $songs): array
|
||||
{
|
||||
Song::query()->whereIn('id', $songs->pluck('id'))->update(['is_public' => false]);
|
||||
if (License::isPlus()) {
|
||||
/**
|
||||
* @var Collection|array<array-key, Song> $collaborativeSongs
|
||||
* Songs that are in collaborative playlists and can't be marked as private as a result
|
||||
*/
|
||||
$collaborativeSongs = Song::query()
|
||||
->whereIn('songs.id', $songs->pluck('id'))
|
||||
->join('playlist_song', 'songs.id', '=', 'playlist_song.song_id')
|
||||
->join('playlist_collaborators', 'playlist_song.playlist_id', '=', 'playlist_collaborators.playlist_id')
|
||||
->select('songs.id')
|
||||
->distinct()
|
||||
->pluck('songs.id')
|
||||
->all();
|
||||
|
||||
$applicableSongIds = $songs->whereNotIn('id', $collaborativeSongs)->pluck('id')->all();
|
||||
} else {
|
||||
$applicableSongIds = $songs->pluck('id')->all();
|
||||
}
|
||||
|
||||
Song::query()->whereIn('id', $applicableSongIds)->update(['is_public' => false]);
|
||||
|
||||
return $applicableSongIds;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,6 +9,22 @@ final class AlbumInformation implements Arrayable
|
|||
{
|
||||
use FormatsLastFmText;
|
||||
|
||||
public const JSON_STRUCTURE = [
|
||||
'url',
|
||||
'cover',
|
||||
'wiki' => [
|
||||
'summary',
|
||||
'full',
|
||||
],
|
||||
'tracks' => [
|
||||
'*' => [
|
||||
'title',
|
||||
'length',
|
||||
'url',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
private function __construct(public ?string $url, public ?string $cover, public array $wiki, public array $tracks)
|
||||
{
|
||||
}
|
||||
|
|
|
@ -8,6 +8,15 @@ final class ArtistInformation implements Arrayable
|
|||
{
|
||||
use FormatsLastFmText;
|
||||
|
||||
public const JSON_STRUCTURE = [
|
||||
'url',
|
||||
'image',
|
||||
'bio' => [
|
||||
'summary',
|
||||
'full',
|
||||
],
|
||||
];
|
||||
|
||||
private function __construct(public ?string $url, public ?string $image, public array $bio)
|
||||
{
|
||||
}
|
||||
|
|
17
database/factories/PlaylistCollaborationTokenFactory.php
Normal file
17
database/factories/PlaylistCollaborationTokenFactory.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Playlist;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class PlaylistCollaborationTokenFactory extends Factory
|
||||
{
|
||||
/** @return array<mixed> */
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'playlist_id' => Playlist::factory(),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Playlist;
|
||||
use App\Models\User;
|
||||
use App\Values\SmartPlaylistRule;
|
||||
use App\Values\SmartPlaylistRuleGroup;
|
||||
|
@ -12,8 +11,6 @@ use Illuminate\Support\Str;
|
|||
|
||||
class PlaylistFactory extends Factory
|
||||
{
|
||||
protected $model = Playlist::class;
|
||||
|
||||
/** @return array<mixed> */
|
||||
public function definition(): array
|
||||
{
|
||||
|
|
|
@ -10,7 +10,7 @@ export default (faker: Faker): Playlist => ({
|
|||
is_smart: false,
|
||||
rules: [],
|
||||
own_songs_only: false,
|
||||
collaborators: []
|
||||
is_collaborative: false,
|
||||
})
|
||||
|
||||
export const states: Record<string, (faker: Faker) => Omit<Partial<Playlist>, 'type'>> = {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<Icon v-if="isRecentlyPlayedList(list)" :icon="faClockRotateLeft" class="text-green" fixed-width />
|
||||
<Icon v-else-if="isFavoriteList(list)" :icon="faHeart" class="text-maroon" fixed-width />
|
||||
<Icon v-else-if="list.is_smart" :icon="faWandMagicSparkles" fixed-width />
|
||||
<Icon v-else-if="list.collaborators.length" :icon="faUsers" fixed-width />
|
||||
<Icon v-else-if="list.is_collaborative" :icon="faUsers" fixed-width />
|
||||
<Icon v-else :icon="faFileLines" fixed-width />
|
||||
<span>{{ list.name }}</span>
|
||||
</a>
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
<main>
|
||||
<p class="intro text-secondary">
|
||||
Collaborative playlists allow multiple users to contribute. Please note that songs added to a collaborative
|
||||
playlist are made accessible to all users, and you cannot make a song private as long as it’s still a part of a
|
||||
collaborative playlist.
|
||||
Collaborative playlists allow multiple users to contribute. <br>
|
||||
Please note that songs added to a collaborative playlist are made accessible to all users,
|
||||
and you cannot mark a song as private if it’s still part of a collaborative playlist.
|
||||
</p>
|
||||
|
||||
<section class="collaborators">
|
||||
|
@ -38,8 +38,10 @@
|
|||
title="This is you!"
|
||||
/>
|
||||
</span>
|
||||
<span v-if="user.id === playlist.user_id" class="role text-secondary">Owner</span>
|
||||
<span v-else class="role text-secondary">Contributor</span>
|
||||
<span class="role text-secondary">
|
||||
<span v-if="user.id === playlist.user_id" class="owner">Owner</span>
|
||||
<span v-else class="contributor">Contributor</span>
|
||||
</span>
|
||||
<span v-if="canManageCollaborators" class="actions">
|
||||
<Btn v-if="user.id !== playlist.user_id" small red @click.prevent="removeCollaborator(user)">
|
||||
Remove
|
||||
|
@ -106,7 +108,7 @@ const inviteCollaborators = async () => {
|
|||
|
||||
const removeCollaborator = async (collaborator: PlaylistCollaborator) => {
|
||||
const deadSure = await showConfirmDialog(
|
||||
`Remove ${collaborator.name} as a collaborator? This will remove their contributed songs as well.`
|
||||
`Remove ${collaborator.name} as a collaborator? This will remove their contributions as well.`
|
||||
)
|
||||
|
||||
if (!deadSure) return
|
||||
|
@ -178,10 +180,7 @@ h2 {
|
|||
span {
|
||||
display: inline-block;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: flex;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
|
@ -192,6 +191,12 @@ h2 {
|
|||
text-align: right;
|
||||
flex: 0 0 96px;
|
||||
text-transform: uppercase;
|
||||
|
||||
span {
|
||||
padding: 3px 4px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, .2);
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
|
|
@ -13,13 +13,14 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { computed, toRefs } from 'vue'
|
||||
|
||||
import UserAvatar from '@/components/user/UserAvatar.vue'
|
||||
|
||||
const props = defineProps<{ playlist: Playlist }>()
|
||||
const { playlist } = toRefs(props)
|
||||
const props = defineProps<{ collaborators: PlaylistCollaborator[] }>()
|
||||
const { collaborators } = toRefs(props)
|
||||
|
||||
const displayedCollaborators = computed(() => playlist.value.collaborators.slice(0, 3))
|
||||
const remainderCount = computed(() => playlist.value.collaborators.length - displayedCollaborators.value.length)
|
||||
const displayedCollaborators = computed(() => collaborators.value.slice(0, 3))
|
||||
const remainderCount = computed(() => collaborators.value.length - displayedCollaborators.value.length)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
<ThumbnailStack :thumbnails="thumbnails" />
|
||||
</template>
|
||||
|
||||
<template v-if="songs.length || playlist.collaborators.length" #meta>
|
||||
<CollaboratorsBadge :playlist="playlist" v-if="playlist.collaborators.length" />
|
||||
<template v-if="songs.length || playlist.is_collaborative" #meta>
|
||||
<CollaboratorsBadge :collaborators="collaborators" v-if="collaborators.length" />
|
||||
<span>{{ pluralize(songs, 'song') }}</span>
|
||||
<span>{{ duration }}</span>
|
||||
<a
|
||||
|
@ -30,7 +30,7 @@
|
|||
@filter="applyFilter"
|
||||
@play-all="playAll"
|
||||
@play-selected="playSelected"
|
||||
@refresh="fetchSongs(true)"
|
||||
@refresh="fetchDetails(true)"
|
||||
/>
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
@ -68,9 +68,9 @@
|
|||
import { faFile } from '@fortawesome/free-regular-svg-icons'
|
||||
import { differenceBy } from 'lodash'
|
||||
import { ref, toRef, watch } from 'vue'
|
||||
import { eventBus, pluralize } from '@/utils'
|
||||
import { eventBus, logger, pluralize } from '@/utils'
|
||||
import { commonStore, playlistStore, songStore } from '@/stores'
|
||||
import { downloadService } from '@/services'
|
||||
import { downloadService, playlistCollaborationService } from '@/services'
|
||||
import { usePlaylistManagement, useRouter, useSongList, useAuthorization, useSongListControls } from '@/composables'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
|
@ -116,14 +116,23 @@ const download = () => downloadService.fromPlaylist(playlist.value!)
|
|||
const editPlaylist = () => eventBus.emit('MODAL_SHOW_EDIT_PLAYLIST_FORM', playlist.value!)
|
||||
|
||||
const removeSelected = async () => await removeSongsFromPlaylist(playlist.value!, selectedSongs.value)
|
||||
let collaborators = ref<PlaylistCollaborator[]>([])
|
||||
|
||||
const fetchSongs = async (refresh = false) => {
|
||||
const fetchDetails = async (refresh = false) => {
|
||||
if (loading.value) return
|
||||
|
||||
loading.value = true
|
||||
songs.value = await songStore.fetchForPlaylist(playlist.value!, refresh)
|
||||
loading.value = false
|
||||
sort()
|
||||
try {
|
||||
[songs.value, collaborators.value] = await Promise.all([
|
||||
songStore.fetchForPlaylist(playlist.value!, refresh),
|
||||
playlistCollaborationService.getCollaborators(playlist.value!),
|
||||
])
|
||||
|
||||
sort()
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(playlistId, async id => {
|
||||
|
@ -135,8 +144,8 @@ watch(playlistId, async id => {
|
|||
listConfig.collaborative = false
|
||||
|
||||
if (playlist.value) {
|
||||
await fetchSongs()
|
||||
listConfig.collaborative = playlist.value.collaborators.length > 0
|
||||
await fetchDetails()
|
||||
listConfig.collaborative = playlist.value.is_collaborative
|
||||
controlsConfig.deletePlaylist = playlist.value.user_id === currentUser.value?.id
|
||||
} else {
|
||||
await triggerNotFound()
|
||||
|
@ -146,8 +155,8 @@ watch(playlistId, async id => {
|
|||
onScreenActivated('Playlist', () => (playlistId.value = getRouteParam('id')!))
|
||||
|
||||
eventBus
|
||||
.on('PLAYLIST_UPDATED', async ({ id }) => id === playlistId.value && await fetchSongs())
|
||||
.on('PLAYLIST_COLLABORATOR_REMOVED', async ({ id }) => id === playlistId.value && await fetchSongs())
|
||||
.on('PLAYLIST_UPDATED', async ({ id }) => id === playlistId.value && await fetchDetails())
|
||||
.on('PLAYLIST_COLLABORATOR_REMOVED', async ({ id }) => id === playlistId.value && await fetchDetails())
|
||||
.on('PLAYLIST_SONGS_REMOVED', async ({ id }, removed) => {
|
||||
if (id !== playlistId.value) return
|
||||
songs.value = differenceBy(songs.value, removed, 'id')
|
||||
|
|
|
@ -327,7 +327,7 @@ new class extends UnitTestCase {
|
|||
await this.be(user).renderComponent(songs)
|
||||
const privatizeMock = this.mock(songStore, 'privatize')
|
||||
|
||||
await this.user.click(screen.getByText('Make Private'))
|
||||
await this.user.click(screen.getByText('Mark as Private'))
|
||||
|
||||
expect(privatizeMock).toHaveBeenCalledWith(songs)
|
||||
})
|
||||
|
@ -344,7 +344,7 @@ new class extends UnitTestCase {
|
|||
await this.be(user).renderComponent(songs)
|
||||
const publicizeMock = this.mock(songStore, 'publicize')
|
||||
|
||||
await this.user.click(screen.getByText('Make Public'))
|
||||
await this.user.click(screen.getByText('Unmark as Private'))
|
||||
|
||||
expect(publicizeMock).toHaveBeenCalledWith(songs)
|
||||
})
|
||||
|
@ -361,8 +361,8 @@ new class extends UnitTestCase {
|
|||
|
||||
await this.be(user).renderComponent(songs)
|
||||
|
||||
expect(screen.queryByText('Make Public')).toBeNull()
|
||||
expect(screen.queryByText('Make Private')).toBeNull()
|
||||
expect(screen.queryByText('Unmark as Private')).toBeNull()
|
||||
expect(screen.queryByText('Mark as Private')).toBeNull()
|
||||
})
|
||||
|
||||
it('has both options to make public and private if songs have mixed visibilities', async () => {
|
||||
|
@ -379,8 +379,8 @@ new class extends UnitTestCase {
|
|||
|
||||
await this.be(owner).renderComponent(songs)
|
||||
|
||||
screen.getByText('Make Public')
|
||||
screen.getByText('Make Private')
|
||||
screen.getByText('Unmark as Private')
|
||||
screen.getByText('Mark as Private')
|
||||
})
|
||||
|
||||
it('does not have an option to make songs public or private oin Community edition', async () => {
|
||||
|
@ -392,8 +392,8 @@ new class extends UnitTestCase {
|
|||
|
||||
await this.be(owner).renderComponent(songs)
|
||||
|
||||
expect(screen.queryByText('Make Public')).toBeNull()
|
||||
expect(screen.queryByText('Make Private')).toBeNull()
|
||||
expect(screen.queryByText('Unmark as Private')).toBeNull()
|
||||
expect(screen.queryByText('Mark as Private')).toBeNull()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ import {
|
|||
useSongMenuMethods
|
||||
} from '@/composables'
|
||||
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { toastSuccess, toastError, toastWarning } = useMessageToaster()
|
||||
const { showConfirmDialog } = useDialogBox()
|
||||
const { go, getRouteParam, isCurrentScreen } = useRouter()
|
||||
const { isAdmin, currentUser } = useAuthorization()
|
||||
|
@ -115,12 +115,23 @@ const normalPlaylists = computed(() => playlists.value.filter(({ is_smart }) =>
|
|||
|
||||
const makePublic = () => trigger(async () => {
|
||||
await songStore.publicize(songs.value)
|
||||
toastSuccess(`Made ${pluralize(songs.value, 'song')} public to everyone.`)
|
||||
toastSuccess(`Unmarked ${pluralize(songs.value, 'song')} as private.`)
|
||||
})
|
||||
|
||||
const makePrivate = () => trigger(async () => {
|
||||
await songStore.privatize(songs.value)
|
||||
toastSuccess(`Removed public access to ${pluralize(songs.value, 'song')}.`)
|
||||
const privatizedIds = await songStore.privatize(songs.value)
|
||||
|
||||
if (!privatizedIds.length) {
|
||||
toastError('Songs cannot be marked as private if they’part of a collaborative playlist.')
|
||||
return
|
||||
}
|
||||
|
||||
if (privatizedIds.length < songs.value.length) {
|
||||
toastWarning('Some songs cannot be marked as private as they’re part of a collaborative playlist.')
|
||||
return
|
||||
}
|
||||
|
||||
toastSuccess(`Marked ${pluralize(songs.value, 'song')} as private.`)
|
||||
})
|
||||
|
||||
const canBeShared = computed(() => !isPlus.value || songs.value[0].is_public)
|
||||
|
@ -136,19 +147,19 @@ const visibilityActions = computed(() => {
|
|||
if (visibilities.length === 2) {
|
||||
return [
|
||||
{
|
||||
label: 'Make Public',
|
||||
label: 'Unmark as Private',
|
||||
handler: makePublic
|
||||
},
|
||||
{
|
||||
label: 'Make Private',
|
||||
label: 'Mark as Private',
|
||||
handler: makePrivate
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return visibilities[0]
|
||||
? [{ label: 'Make Private', handler: makePrivate }]
|
||||
: [{ label: 'Make Public', handler: makePublic }]
|
||||
? [{ label: 'Mark as Private', handler: makePrivate }]
|
||||
: [{ label: 'Unmark as Private', handler: makePublic }]
|
||||
})
|
||||
|
||||
const canBeRemovedFromPlaylist = computed(() => {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { logger, uuid } from '@/utils'
|
|||
import { cache, http } from '@/services'
|
||||
import models from '@/config/smart-playlist/models'
|
||||
import operators from '@/config/smart-playlist/operators'
|
||||
import { songStore } from '@/stores/songStore'
|
||||
|
||||
type CreatePlaylistRequestData = {
|
||||
name: Playlist['name']
|
||||
|
@ -94,7 +95,11 @@ export const playlistStore = {
|
|||
return playlist
|
||||
}
|
||||
|
||||
await http.post(`playlists/${playlist.id}/songs`, { songs: songs.map(song => song.id) })
|
||||
const updatedSongs = await http.post<Song[]>(`playlists/${playlist.id}/songs`, {
|
||||
songs: songs.map(song => song.id)
|
||||
})
|
||||
|
||||
songStore.syncWithVault(updatedSongs)
|
||||
cache.remove(['playlist.songs', playlist.id])
|
||||
|
||||
return playlist
|
||||
|
|
|
@ -222,7 +222,7 @@ new class extends UnitTestCase {
|
|||
|
||||
it('fetches for playlist', async () => {
|
||||
const songs = factory<Song>('song', 3)
|
||||
const playlist = factory<Playlist>('playlist', { id: 42 })
|
||||
const playlist = factory<Playlist>('playlist', { id: '966268ea-935d-4f63-a84e-180385376a78' })
|
||||
const getMock = this.mock(http, 'get').mockResolvedValueOnce(songs)
|
||||
const syncMock = this.mock(songStore, 'syncWithVault', songs)
|
||||
|
||||
|
@ -235,7 +235,7 @@ new class extends UnitTestCase {
|
|||
|
||||
it('fetches for playlist with cache', async () => {
|
||||
const songs = factory<Song>('song', 3)
|
||||
const playlist = factory<Playlist>('playlist', { id: 42 })
|
||||
const playlist = factory<Playlist>('playlist', { id: '966268ea-935d-4f63-a84e-180385376a78' })
|
||||
cache.set(['playlist.songs', playlist.id], songs)
|
||||
|
||||
const getMock = this.mock(http, 'get')
|
||||
|
@ -248,7 +248,7 @@ new class extends UnitTestCase {
|
|||
|
||||
it('fetches for playlist discarding cache', async () => {
|
||||
const songs = factory<Song>('song', 3)
|
||||
const playlist = factory<Playlist>('playlist', { id: 42 })
|
||||
const playlist = factory<Playlist>('playlist', { id: '966268ea-935d-4f63-a84e-180385376a78' })
|
||||
cache.set(['playlist.songs', playlist.id], songs)
|
||||
|
||||
const getMock = this.mock(http, 'get').mockResolvedValueOnce([])
|
||||
|
|
|
@ -174,7 +174,7 @@ export const songStore = {
|
|||
))
|
||||
},
|
||||
|
||||
async fetchForPlaylist (playlist: Playlist | string, refresh = false) {
|
||||
async fetchForPlaylist (playlist: Playlist | Playlist['id'], refresh = false) {
|
||||
const id = typeof playlist === 'string' ? playlist : playlist.id
|
||||
|
||||
if (refresh) {
|
||||
|
@ -197,7 +197,7 @@ export const songStore = {
|
|||
return uniqBy(songs, 'id')
|
||||
},
|
||||
|
||||
async paginateForGenre (genre: Genre | string, params: GenreSongListPaginateParams) {
|
||||
async paginateForGenre (genre: Genre | Genre['name'], params: GenreSongListPaginateParams) {
|
||||
const name = typeof genre === 'string' ? genre : genre.name
|
||||
const resource = await http.get<PaginatorResource>(`genres/${name}/songs?${new URLSearchParams(params).toString()}`)
|
||||
const songs = this.syncWithVault(resource.data)
|
||||
|
@ -208,7 +208,7 @@ export const songStore = {
|
|||
}
|
||||
},
|
||||
|
||||
async fetchRandomForGenre (genre: Genre | string, limit = 500) {
|
||||
async fetchRandomForGenre (genre: Genre | Genre['name'], limit = 500) {
|
||||
const name = typeof genre === 'string' ? genre : genre.name
|
||||
return this.syncWithVault(await http.get<Song[]>(`genres/${name}/songs/random?limit=${limit}`))
|
||||
},
|
||||
|
@ -251,10 +251,15 @@ export const songStore = {
|
|||
},
|
||||
|
||||
async privatize (songs: Song[]) {
|
||||
await http.put('songs/privatize', {
|
||||
songs: songs.map(song => song.id)
|
||||
const privatizedIds = await http.put<Song['id'][]>('songs/privatize', {
|
||||
songs: songs.map(({ id }) => id)
|
||||
})
|
||||
|
||||
songs.forEach(song => song.is_public = false)
|
||||
privatizedIds.forEach(id => {
|
||||
const song = this.byId(id)
|
||||
song && (song.is_public = false)
|
||||
})
|
||||
|
||||
return privatizedIds
|
||||
}
|
||||
}
|
||||
|
|
2
resources/assets/js/types.d.ts
vendored
2
resources/assets/js/types.d.ts
vendored
|
@ -232,9 +232,9 @@ interface Playlist {
|
|||
name: string
|
||||
folder_id: PlaylistFolder['id'] | null
|
||||
is_smart: boolean
|
||||
is_collaborative: boolean
|
||||
rules: SmartPlaylistRuleGroup[]
|
||||
own_songs_only: boolean
|
||||
collaborators: PlaylistCollaborator[]
|
||||
}
|
||||
|
||||
type PlaylistLike = Playlist | FavoriteList | RecentlyPlayedList
|
||||
|
|
|
@ -10,22 +10,6 @@ use Tests\TestCase;
|
|||
|
||||
class AlbumInformationTest extends TestCase
|
||||
{
|
||||
private const JSON_STRUCTURE = [
|
||||
'url',
|
||||
'cover',
|
||||
'wiki' => [
|
||||
'summary',
|
||||
'full',
|
||||
],
|
||||
'tracks' => [
|
||||
'*' => [
|
||||
'title',
|
||||
'length',
|
||||
'url',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
public function testGet(): void
|
||||
{
|
||||
config(['koel.lastfm.key' => 'foo']);
|
||||
|
@ -59,7 +43,7 @@ class AlbumInformationTest extends TestCase
|
|||
));
|
||||
|
||||
$this->getAs('api/albums/' . $album->id . '/information')
|
||||
->assertJsonStructure(self::JSON_STRUCTURE);
|
||||
->assertJsonStructure(AlbumInformation::JSON_STRUCTURE);
|
||||
}
|
||||
|
||||
public function testGetWithoutLastfmStillReturnsValidStructure(): void
|
||||
|
@ -71,6 +55,6 @@ class AlbumInformationTest extends TestCase
|
|||
$album = Album::factory()->create();
|
||||
|
||||
$this->getAs('api/albums/' . $album->id . '/information')
|
||||
->assertJsonStructure(self::JSON_STRUCTURE);
|
||||
->assertJsonStructure(AlbumInformation::JSON_STRUCTURE);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\Album;
|
||||
use App\Models\Song;
|
||||
use Tests\TestCase;
|
||||
|
@ -16,6 +17,6 @@ class AlbumSongTest extends TestCase
|
|||
Song::factory(5)->for($album)->create();
|
||||
|
||||
$this->getAs('api/albums/' . $album->id . '/songs')
|
||||
->assertJsonStructure(['*' => SongTest::JSON_STRUCTURE]);
|
||||
->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,46 +2,18 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\AlbumResource;
|
||||
use App\Models\Album;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AlbumTest extends TestCase
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'id',
|
||||
'name',
|
||||
'artist_id',
|
||||
'artist_name',
|
||||
'cover',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
private const JSON_COLLECTION_STRUCTURE = [
|
||||
'data' => [
|
||||
'*' => self::JSON_STRUCTURE,
|
||||
],
|
||||
'links' => [
|
||||
'first',
|
||||
'last',
|
||||
'prev',
|
||||
'next',
|
||||
],
|
||||
'meta' => [
|
||||
'current_page',
|
||||
'from',
|
||||
'path',
|
||||
'per_page',
|
||||
'to',
|
||||
],
|
||||
];
|
||||
|
||||
public function testIndex(): void
|
||||
{
|
||||
Album::factory(10)->create();
|
||||
|
||||
$this->getAs('api/albums')
|
||||
->assertJsonStructure(self::JSON_COLLECTION_STRUCTURE);
|
||||
->assertJsonStructure(AlbumResource::PAGINATION_JSON_STRUCTURE);
|
||||
}
|
||||
|
||||
public function testShow(): void
|
||||
|
@ -50,6 +22,6 @@ class AlbumTest extends TestCase
|
|||
$album = Album::factory()->create();
|
||||
|
||||
$this->getAs('api/albums/' . $album->id)
|
||||
->assertJsonStructure(self::JSON_STRUCTURE);
|
||||
->assertJsonStructure(AlbumResource::JSON_STRUCTURE);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\AlbumResource;
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use Tests\TestCase;
|
||||
|
@ -16,6 +17,6 @@ class ArtistAlbumTest extends TestCase
|
|||
Album::factory(5)->for($artist)->create();
|
||||
|
||||
$this->getAs('api/artists/' . $artist->id . '/albums')
|
||||
->assertJsonStructure(['*' => AlbumTest::JSON_STRUCTURE]);
|
||||
->assertJsonStructure(['*' => AlbumResource::JSON_STRUCTURE]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,15 +10,6 @@ use Tests\TestCase;
|
|||
|
||||
class ArtistInformationTest extends TestCase
|
||||
{
|
||||
private const JSON_STRUCTURE = [
|
||||
'url',
|
||||
'image',
|
||||
'bio' => [
|
||||
'summary',
|
||||
'full',
|
||||
],
|
||||
];
|
||||
|
||||
public function testGet(): void
|
||||
{
|
||||
config(['koel.lastfm.key' => 'foo']);
|
||||
|
@ -40,7 +31,7 @@ class ArtistInformationTest extends TestCase
|
|||
));
|
||||
|
||||
$this->getAs('api/artists/' . $artist->id . '/information')
|
||||
->assertJsonStructure(self::JSON_STRUCTURE);
|
||||
->assertJsonStructure(ArtistInformation::JSON_STRUCTURE);
|
||||
}
|
||||
|
||||
public function testGetWithoutLastfmStillReturnsValidStructure(): void
|
||||
|
@ -52,6 +43,6 @@ class ArtistInformationTest extends TestCase
|
|||
$artist = Artist::factory()->create();
|
||||
|
||||
$this->getAs('api/artists/' . $artist->id . '/information')
|
||||
->assertJsonStructure(self::JSON_STRUCTURE);
|
||||
->assertJsonStructure(ArtistInformation::JSON_STRUCTURE);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\Artist;
|
||||
use App\Models\Song;
|
||||
use Tests\TestCase;
|
||||
|
@ -16,6 +17,6 @@ class ArtistSongTest extends TestCase
|
|||
Song::factory(5)->for($artist)->create();
|
||||
|
||||
$this->getAs('api/artists/' . $artist->id . '/songs')
|
||||
->assertJsonStructure(['*' => SongTest::JSON_STRUCTURE]);
|
||||
->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,44 +2,18 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\ArtistResource;
|
||||
use App\Models\Artist;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ArtistTest extends TestCase
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'id',
|
||||
'name',
|
||||
'image',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
private const JSON_COLLECTION_STRUCTURE = [
|
||||
'data' => [
|
||||
'*' => self::JSON_STRUCTURE,
|
||||
],
|
||||
'links' => [
|
||||
'first',
|
||||
'last',
|
||||
'prev',
|
||||
'next',
|
||||
],
|
||||
'meta' => [
|
||||
'current_page',
|
||||
'from',
|
||||
'path',
|
||||
'per_page',
|
||||
'to',
|
||||
],
|
||||
];
|
||||
|
||||
public function testIndex(): void
|
||||
{
|
||||
Artist::factory(10)->create();
|
||||
|
||||
$this->getAs('api/artists')
|
||||
->assertJsonStructure(self::JSON_COLLECTION_STRUCTURE);
|
||||
->assertJsonStructure(ArtistResource::PAGINATION_JSON_STRUCTURE);
|
||||
}
|
||||
|
||||
public function testShow(): void
|
||||
|
@ -48,6 +22,6 @@ class ArtistTest extends TestCase
|
|||
$artist = Artist::factory()->create();
|
||||
|
||||
$this->getAs('api/artists/' . $artist->id)
|
||||
->assertJsonStructure(self::JSON_STRUCTURE);
|
||||
->assertJsonStructure(ArtistResource::JSON_STRUCTURE);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\AlbumResource;
|
||||
use App\Http\Resources\ArtistResource;
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Models\Song;
|
||||
|
@ -22,9 +25,9 @@ class ExcerptSearchTest extends TestCase
|
|||
|
||||
$this->getAs('api/search?q=foo')
|
||||
->assertJsonStructure([
|
||||
'songs' => ['*' => SongTest::JSON_STRUCTURE],
|
||||
'artists' => ['*' => ArtistTest::JSON_STRUCTURE],
|
||||
'albums' => ['*' => AlbumTest::JSON_STRUCTURE],
|
||||
'songs' => ['*' => SongResource::JSON_STRUCTURE],
|
||||
'artists' => ['*' => ArtistResource::JSON_STRUCTURE],
|
||||
'albums' => ['*' => AlbumResource::JSON_STRUCTURE],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\Interaction;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
@ -15,6 +16,6 @@ class FavoriteSongTest extends TestCase
|
|||
Interaction::factory(5)->for($user)->create(['liked' => true]);
|
||||
|
||||
$this->getAs('api/songs/favorite', $user)
|
||||
->assertJsonStructure(['*' => SongTest::JSON_STRUCTURE]);
|
||||
->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,19 +2,14 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\GenreResource;
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\Song;
|
||||
use App\Values\Genre;
|
||||
use Tests\TestCase;
|
||||
|
||||
class GenreTest extends TestCase
|
||||
{
|
||||
private const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'name',
|
||||
'song_count',
|
||||
'length',
|
||||
];
|
||||
|
||||
public function testGetAllGenres(): void
|
||||
{
|
||||
Song::factory()->count(5)->create(['genre' => 'Rock']);
|
||||
|
@ -22,7 +17,7 @@ class GenreTest extends TestCase
|
|||
Song::factory()->count(10)->create(['genre' => '']);
|
||||
|
||||
$this->getAs('api/genres')
|
||||
->assertJsonStructure(['*' => self::JSON_STRUCTURE])
|
||||
->assertJsonStructure(['*' => GenreResource::JSON_STRUCTURE])
|
||||
->assertJsonFragment(['name' => 'Rock', 'song_count' => 5])
|
||||
->assertJsonFragment(['name' => 'Pop', 'song_count' => 2])
|
||||
->assertJsonFragment(['name' => Genre::NO_GENRE, 'song_count' => 10]);
|
||||
|
@ -33,7 +28,7 @@ class GenreTest extends TestCase
|
|||
Song::factory()->count(5)->create(['genre' => 'Rock']);
|
||||
|
||||
$this->getAs('api/genres/Rock')
|
||||
->assertJsonStructure(self::JSON_STRUCTURE)
|
||||
->assertJsonStructure(GenreResource::JSON_STRUCTURE)
|
||||
->assertJsonFragment(['name' => 'Rock', 'song_count' => 5]);
|
||||
}
|
||||
|
||||
|
@ -47,7 +42,7 @@ class GenreTest extends TestCase
|
|||
Song::factory()->count(5)->create(['genre' => 'Rock']);
|
||||
|
||||
$this->getAs('api/genres/Rock/songs')
|
||||
->assertJsonStructure(SongTest::JSON_COLLECTION_STRUCTURE);
|
||||
->assertJsonStructure(SongResource::PAGINATION_JSON_STRUCTURE);
|
||||
}
|
||||
|
||||
public function testGetRandomSongsInGenre(): void
|
||||
|
@ -55,6 +50,6 @@ class GenreTest extends TestCase
|
|||
Song::factory()->count(5)->create(['genre' => 'Rock']);
|
||||
|
||||
$this->getAs('api/genres/Rock/songs/random?limit=500')
|
||||
->assertJsonStructure(['*' => SongTest::JSON_STRUCTURE]);
|
||||
->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]);
|
||||
}
|
||||
}
|
||||
|
|
35
tests/Feature/KoelPlus/PlaylistCollaborationTest.php
Normal file
35
tests/Feature/KoelPlus/PlaylistCollaborationTest.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\KoelPlus;
|
||||
|
||||
use App\Http\Resources\PlaylistCollaborationTokenResource;
|
||||
use App\Http\Resources\PlaylistResource;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\PlaylistCollaborationToken;
|
||||
use Tests\PlusTestCase;
|
||||
|
||||
use function Tests\create_user;
|
||||
|
||||
class PlaylistCollaborationTest extends PlusTestCase
|
||||
{
|
||||
public function testCreatePlaylistCollaborationToken(): void
|
||||
{
|
||||
/** @var Playlist $playlist */
|
||||
$playlist = Playlist::factory()->create();
|
||||
|
||||
$this->postAs("api/playlists/$playlist->id/collaborators/invite", [], $playlist->user)
|
||||
->assertJsonStructure(PlaylistCollaborationTokenResource::JSON_STRUCTURE);
|
||||
}
|
||||
|
||||
public function testAcceptPlaylistCollaborationViaToken(): void
|
||||
{
|
||||
/** @var PlaylistCollaborationToken $token */
|
||||
$token = PlaylistCollaborationToken::factory()->create();
|
||||
$user = create_user();
|
||||
|
||||
$this->postAs('api/playlists/collaborators/accept', ['token' => $token->token], $user)
|
||||
->assertJsonStructure(PlaylistResource::JSON_STRUCTURE);
|
||||
|
||||
self::assertTrue($token->playlist->hasCollaborator($user));
|
||||
}
|
||||
}
|
|
@ -2,33 +2,15 @@
|
|||
|
||||
namespace Tests\Feature\KoelPlus;
|
||||
|
||||
use App\Http\Resources\CollaborativeSongResource;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\Song;
|
||||
use Tests\Feature\SongTest as CommunitySongTest;
|
||||
use Tests\PlusTestCase;
|
||||
|
||||
use function Tests\create_user;
|
||||
|
||||
class PlaylistSongTest extends PlusTestCase
|
||||
{
|
||||
private array $songJsonStructure;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->songJsonStructure = CommunitySongTest::JSON_STRUCTURE + [
|
||||
'collaboration' => [
|
||||
'user' => [
|
||||
'avatar',
|
||||
'name',
|
||||
],
|
||||
'added_at',
|
||||
'fmt_added_at',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testGetSongsInCollaborativePlaylist(): void
|
||||
{
|
||||
/** @var Playlist $playlist */
|
||||
|
@ -40,7 +22,7 @@ class PlaylistSongTest extends PlusTestCase
|
|||
|
||||
$this->getAs("api/playlists/$playlist->id/songs", $collaborator)
|
||||
->assertSuccessful()
|
||||
->assertJsonStructure(['*' => $this->songJsonStructure])
|
||||
->assertJsonStructure(['*' => CollaborativeSongResource::JSON_STRUCTURE])
|
||||
->assertJsonCount(3);
|
||||
}
|
||||
|
||||
|
@ -59,7 +41,7 @@ class PlaylistSongTest extends PlusTestCase
|
|||
|
||||
$this->getAs("api/playlists/$playlist->id/songs", $collaborator)
|
||||
->assertSuccessful()
|
||||
->assertJsonStructure(['*' => $this->songJsonStructure])
|
||||
->assertJsonStructure(['*' => CollaborativeSongResource::JSON_STRUCTURE])
|
||||
->assertJsonCount(3)
|
||||
->assertJsonMissing(['id' => $privateSong->id]);
|
||||
}
|
||||
|
@ -75,7 +57,8 @@ class PlaylistSongTest extends PlusTestCase
|
|||
$this->postAs("api/playlists/$playlist->id/songs", ['songs' => $songs->pluck('id')->all()], $collaborator)
|
||||
->assertSuccessful();
|
||||
|
||||
self::assertArraySubset($songs->pluck('id')->all(), $playlist->songs->pluck('id')->all());
|
||||
$playlist->refresh();
|
||||
$songs->each(static fn (Song $song) => self::assertTrue($playlist->songs->contains($song)));
|
||||
}
|
||||
|
||||
public function testCollaboratorCanRemoveSongs(): void
|
||||
|
@ -90,6 +73,7 @@ class PlaylistSongTest extends PlusTestCase
|
|||
$this->deleteAs("api/playlists/$playlist->id/songs", ['songs' => $songs->pluck('id')->all()], $collaborator)
|
||||
->assertSuccessful();
|
||||
|
||||
self::assertEmpty($playlist->refresh()->songs);
|
||||
$playlist->refresh();
|
||||
$songs->each(static fn (Song $song) => self::assertFalse($playlist->songs->contains($song)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
namespace Tests\Feature\KoelPlus;
|
||||
|
||||
use App\Http\Resources\PlaylistResource;
|
||||
use App\Models\Playlist;
|
||||
use App\Values\SmartPlaylistRule;
|
||||
use Tests\Feature\PlaylistTest as BasePlaylistTest;
|
||||
use Tests\PlusTestCase;
|
||||
|
||||
use function Tests\create_user;
|
||||
|
@ -30,7 +30,7 @@ class PlaylistTest extends PlusTestCase
|
|||
],
|
||||
],
|
||||
'own_songs_only' => true,
|
||||
], $user)->assertJsonStructure(BasePlaylistTest::JSON_STRUCTURE);
|
||||
], $user)->assertJsonStructure(PlaylistResource::JSON_STRUCTURE);
|
||||
|
||||
/** @var Playlist $playlist */
|
||||
$playlist = Playlist::query()->latest()->first();
|
||||
|
@ -54,7 +54,7 @@ class PlaylistTest extends PlusTestCase
|
|||
'own_songs_only' => true,
|
||||
'rules' => $playlist->rules->toArray(),
|
||||
], $playlist->user)
|
||||
->assertJsonStructure(BasePlaylistTest::JSON_STRUCTURE);
|
||||
->assertJsonStructure(PlaylistResource::JSON_STRUCTURE);
|
||||
|
||||
$playlist->refresh();
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ class SongVisibilityTest extends PlusTestCase
|
|||
/** @var Collection<Song> $externalSongs */
|
||||
$externalSongs = Song::factory(3)->for($anotherUser, 'owner')->public()->create();
|
||||
|
||||
// We can't make private songs that are not ours.
|
||||
// We can't Mark as Private songs that are not ours.
|
||||
$this->putAs('api/songs/privatize', ['songs' => $externalSongs->pluck('id')->toArray()], $currentUser)
|
||||
->assertForbidden();
|
||||
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\AlbumResource;
|
||||
use App\Http\Resources\ArtistResource;
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\Interaction;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
@ -17,12 +20,12 @@ class OverviewTest extends TestCase
|
|||
|
||||
$this->getAs('api/overview', $user)
|
||||
->assertJsonStructure([
|
||||
'most_played_songs' => ['*' => SongTest::JSON_STRUCTURE],
|
||||
'recently_played_songs' => ['*' => SongTest::JSON_STRUCTURE],
|
||||
'recently_added_albums' => ['*' => AlbumTest::JSON_STRUCTURE],
|
||||
'recently_added_songs' => ['*' => SongTest::JSON_STRUCTURE],
|
||||
'most_played_artists' => ['*' => ArtistTest::JSON_STRUCTURE],
|
||||
'most_played_albums' => ['*' => AlbumTest::JSON_STRUCTURE],
|
||||
'most_played_songs' => ['*' => SongResource::JSON_STRUCTURE],
|
||||
'recently_played_songs' => ['*' => SongResource::JSON_STRUCTURE],
|
||||
'recently_added_albums' => ['*' => AlbumResource::JSON_STRUCTURE],
|
||||
'recently_added_songs' => ['*' => SongResource::JSON_STRUCTURE],
|
||||
'most_played_artists' => ['*' => ArtistResource::JSON_STRUCTURE],
|
||||
'most_played_albums' => ['*' => AlbumResource::JSON_STRUCTURE],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\PlaylistFolderResource;
|
||||
use App\Models\PlaylistFolder;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
@ -9,21 +10,13 @@ use function Tests\create_user;
|
|||
|
||||
class PlaylistFolderTest extends TestCase
|
||||
{
|
||||
private const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'id',
|
||||
'name',
|
||||
'user_id',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
public function testListing(): void
|
||||
{
|
||||
$user = create_user();
|
||||
PlaylistFolder::factory()->for($user)->count(3)->create();
|
||||
|
||||
$this->getAs('api/playlist-folders', $user)
|
||||
->assertJsonStructure(['*' => self::JSON_STRUCTURE])
|
||||
->assertJsonStructure(['*' => PlaylistFolderResource::JSON_STRUCTURE])
|
||||
->assertJsonCount(3, '*');
|
||||
}
|
||||
|
||||
|
@ -32,7 +25,7 @@ class PlaylistFolderTest extends TestCase
|
|||
$user = create_user();
|
||||
|
||||
$this->postAs('api/playlist-folders', ['name' => 'Classical'], $user)
|
||||
->assertJsonStructure(self::JSON_STRUCTURE);
|
||||
->assertJsonStructure(PlaylistFolderResource::JSON_STRUCTURE);
|
||||
|
||||
$this->assertDatabaseHas(PlaylistFolder::class, ['name' => 'Classical', 'user_id' => $user->id]);
|
||||
}
|
||||
|
@ -43,7 +36,7 @@ class PlaylistFolderTest extends TestCase
|
|||
$folder = PlaylistFolder::factory()->create(['name' => 'Metal']);
|
||||
|
||||
$this->patchAs('api/playlist-folders/' . $folder->id, ['name' => 'Classical'], $folder->user)
|
||||
->assertJsonStructure(self::JSON_STRUCTURE);
|
||||
->assertJsonStructure(PlaylistFolderResource::JSON_STRUCTURE);
|
||||
|
||||
self::assertSame('Classical', $folder->fresh()->name);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\Song;
|
||||
use Illuminate\Support\Collection;
|
||||
|
@ -18,7 +19,7 @@ class PlaylistSongTest extends TestCase
|
|||
$playlist->addSongs(Song::factory(5)->create());
|
||||
|
||||
$this->getAs("api/playlists/$playlist->id/songs", $playlist->user)
|
||||
->assertJsonStructure(['*' => SongTest::JSON_STRUCTURE]);
|
||||
->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]);
|
||||
}
|
||||
|
||||
public function testGetSmartPlaylist(): void
|
||||
|
@ -43,7 +44,7 @@ class PlaylistSongTest extends TestCase
|
|||
]);
|
||||
|
||||
$this->getAs("api/playlists/$playlist->id/songs", $playlist->user)
|
||||
->assertJsonStructure(['*' => SongTest::JSON_STRUCTURE]);
|
||||
->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]);
|
||||
}
|
||||
|
||||
public function testNonOwnerCannotAccessPlaylist(): void
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\PlaylistResource;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\Song;
|
||||
use App\Values\SmartPlaylistRule;
|
||||
|
@ -12,25 +13,13 @@ use function Tests\create_user;
|
|||
|
||||
class PlaylistTest extends TestCase
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'id',
|
||||
'name',
|
||||
'folder_id',
|
||||
'user_id',
|
||||
'is_smart',
|
||||
'rules',
|
||||
'own_songs_only',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
public function testListing(): void
|
||||
{
|
||||
$user = create_user();
|
||||
Playlist::factory()->for($user)->count(3)->create();
|
||||
|
||||
$this->getAs('api/playlists', $user)
|
||||
->assertJsonStructure(['*' => self::JSON_STRUCTURE])
|
||||
->assertJsonStructure(['*' => PlaylistResource::JSON_STRUCTURE])
|
||||
->assertJsonCount(3, '*');
|
||||
}
|
||||
|
||||
|
@ -46,7 +35,7 @@ class PlaylistTest extends TestCase
|
|||
'songs' => $songs->pluck('id')->all(),
|
||||
'rules' => [],
|
||||
], $user)
|
||||
->assertJsonStructure(self::JSON_STRUCTURE);
|
||||
->assertJsonStructure(PlaylistResource::JSON_STRUCTURE);
|
||||
|
||||
/** @var Playlist $playlist */
|
||||
$playlist = Playlist::query()->latest()->first();
|
||||
|
@ -75,7 +64,7 @@ class PlaylistTest extends TestCase
|
|||
'rules' => [$rule->toArray()],
|
||||
],
|
||||
],
|
||||
], $user)->assertJsonStructure(self::JSON_STRUCTURE);
|
||||
], $user)->assertJsonStructure(PlaylistResource::JSON_STRUCTURE);
|
||||
|
||||
/** @var Playlist $playlist */
|
||||
$playlist = Playlist::query()->latest()->first();
|
||||
|
@ -124,7 +113,7 @@ class PlaylistTest extends TestCase
|
|||
$playlist = Playlist::factory()->create(['name' => 'Foo']);
|
||||
|
||||
$this->putAs("api/playlists/$playlist->id", ['name' => 'Bar'], $playlist->user)
|
||||
->assertJsonStructure(self::JSON_STRUCTURE);
|
||||
->assertJsonStructure(PlaylistResource::JSON_STRUCTURE);
|
||||
|
||||
self::assertSame('Bar', $playlist->refresh()->name);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\QueueState;
|
||||
use App\Models\Song;
|
||||
use Tests\TestCase;
|
||||
|
@ -12,7 +13,7 @@ class QueueTest extends TestCase
|
|||
{
|
||||
public const QUEUE_STATE_JSON_STRUCTURE = [
|
||||
'current_song',
|
||||
'songs' => ['*' => SongTest::JSON_STRUCTURE],
|
||||
'songs' => ['*' => SongResource::JSON_STRUCTURE],
|
||||
'playback_position',
|
||||
];
|
||||
|
||||
|
@ -81,11 +82,11 @@ class QueueTest extends TestCase
|
|||
Song::factory(10)->create();
|
||||
|
||||
$this->getAs('api/queue/fetch?order=rand&limit=5')
|
||||
->assertJsonStructure(['*' => SongTest::JSON_STRUCTURE])
|
||||
->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE])
|
||||
->assertJsonCount(5, '*');
|
||||
|
||||
$this->getAs('api/queue/fetch?order=asc&sort=title&limit=5')
|
||||
->assertJsonStructure(['*' => SongTest::JSON_STRUCTURE])
|
||||
->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE])
|
||||
->assertJsonCount(5, '*');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\Interaction;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
@ -16,6 +17,6 @@ class RecentlyPlayedSongTest extends TestCase
|
|||
Interaction::factory(5)->for($user)->create();
|
||||
|
||||
$this->getAs('api/songs/recently-played', $user)
|
||||
->assertJsonStructure(['*' => SongTest::JSON_STRUCTURE]);
|
||||
->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\Song;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
@ -12,6 +13,6 @@ class SongSearchTest extends TestCase
|
|||
Song::factory(10)->create(['title' => 'A Foo Song']);
|
||||
|
||||
$this->getAs('api/search/songs?q=foo')
|
||||
->assertJsonStructure(['*' => SongTest::JSON_STRUCTURE]);
|
||||
->assertJsonStructure(['*' => SongResource::JSON_STRUCTURE]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\SongResource;
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Models\Song;
|
||||
|
@ -12,54 +13,12 @@ use function Tests\create_admin;
|
|||
|
||||
class SongTest extends TestCase
|
||||
{
|
||||
public const JSON_STRUCTURE = [
|
||||
'type',
|
||||
'id',
|
||||
'title',
|
||||
'lyrics',
|
||||
'album_id',
|
||||
'album_name',
|
||||
'artist_id',
|
||||
'artist_name',
|
||||
'album_artist_id',
|
||||
'album_artist_name',
|
||||
'album_cover',
|
||||
'length',
|
||||
'liked',
|
||||
'play_count',
|
||||
'track',
|
||||
'genre',
|
||||
'year',
|
||||
'disc',
|
||||
'is_public',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
public const JSON_COLLECTION_STRUCTURE = [
|
||||
'data' => [
|
||||
'*' => 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);
|
||||
$this->getAs('api/songs')->assertJsonStructure(SongResource::PAGINATION_JSON_STRUCTURE);
|
||||
$this->getAs('api/songs?sort=title&order=desc')->assertJsonStructure(SongResource::PAGINATION_JSON_STRUCTURE);
|
||||
}
|
||||
|
||||
public function testShow(): void
|
||||
|
@ -67,7 +26,7 @@ class SongTest extends TestCase
|
|||
/** @var Song $song */
|
||||
$song = Song::factory()->create();
|
||||
|
||||
$this->getAs('api/songs/' . $song->id)->assertJsonStructure(self::JSON_STRUCTURE);
|
||||
$this->getAs('api/songs/' . $song->id)->assertJsonStructure(SongResource::JSON_STRUCTURE);
|
||||
}
|
||||
|
||||
public function testDelete(): void
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Resources\UserProspectResource;
|
||||
use App\Mail\UserInvite;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
@ -12,8 +13,6 @@ use function Tests\create_admin;
|
|||
|
||||
class UserInvitationTest extends TestCase
|
||||
{
|
||||
private const JSON_STRUCTURE = ['id', 'name', 'email', 'is_admin'];
|
||||
|
||||
public function testInvite(): void
|
||||
{
|
||||
Mail::fake();
|
||||
|
@ -23,7 +22,7 @@ class UserInvitationTest extends TestCase
|
|||
'is_admin' => true,
|
||||
], create_admin())
|
||||
->assertSuccessful()
|
||||
->assertJsonStructure(['*' => self::JSON_STRUCTURE]);
|
||||
->assertJsonStructure(['*' => UserProspectResource::JSON_STRUCTURE]);
|
||||
|
||||
Mail::assertQueued(UserInvite::class, 2);
|
||||
}
|
||||
|
@ -44,7 +43,7 @@ class UserInvitationTest extends TestCase
|
|||
|
||||
$this->get("api/invitations?token=$prospect->invitation_token")
|
||||
->assertSuccessful()
|
||||
->assertJsonStructure(self::JSON_STRUCTURE);
|
||||
->assertJsonStructure(UserProspectResource::JSON_STRUCTURE);
|
||||
}
|
||||
|
||||
public function testRevoke(): void
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Integration\KoelPlus\Services;
|
||||
|
||||
use App\Events\NewPlaylistCollaboratorJoined;
|
||||
use App\Exceptions\CannotRemoveOwnerFromPlaylistException;
|
||||
use App\Exceptions\NotAPlaylistCollaboratorException;
|
||||
use App\Exceptions\PlaylistCollaborationTokenExpiredException;
|
||||
use App\Exceptions\SmartPlaylistsAreNotCollaborativeException;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\PlaylistCollaborationToken;
|
||||
use App\Services\PlaylistCollaborationService;
|
||||
use Tests\PlusTestCase;
|
||||
|
||||
use function Tests\create_user;
|
||||
|
||||
class PlaylistCollaborationServiceTest extends PlusTestCase
|
||||
{
|
||||
private PlaylistCollaborationService $service;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->service = app(PlaylistCollaborationService::class);
|
||||
}
|
||||
|
||||
public function testCreateToken(): void
|
||||
{
|
||||
/** @var Playlist $playlist */
|
||||
$playlist = Playlist::factory()->create();
|
||||
|
||||
$token = $this->service->createToken($playlist);
|
||||
|
||||
self::assertNotNull($token->token);
|
||||
self::assertFalse($token->expired);
|
||||
self::assertSame($playlist->id, $token->playlist_id);
|
||||
}
|
||||
|
||||
public function testCreateTokenFailsIfPlaylistIsSmart(): void
|
||||
{
|
||||
$this->expectException(SmartPlaylistsAreNotCollaborativeException::class);
|
||||
|
||||
/** @var Playlist $playlist */
|
||||
$playlist = Playlist::factory()->smart()->create();
|
||||
|
||||
$this->service->createToken($playlist);
|
||||
}
|
||||
|
||||
public function testAcceptUsingToken(): void
|
||||
{
|
||||
$this->expectsEvents(NewPlaylistCollaboratorJoined::class);
|
||||
|
||||
/** @var PlaylistCollaborationToken $token */
|
||||
$token = PlaylistCollaborationToken::factory()->create();
|
||||
$user = create_user();
|
||||
self::assertFalse($token->playlist->collaborators->contains($user));
|
||||
|
||||
$this->service->acceptUsingToken($token->token, $user);
|
||||
|
||||
self::assertTrue($token->refresh()->playlist->collaborators->contains($user));
|
||||
}
|
||||
|
||||
public function testFailsToAcceptExpiredToken(): void
|
||||
{
|
||||
$this->expectException(PlaylistCollaborationTokenExpiredException::class);
|
||||
$this->doesntExpectEvents(NewPlaylistCollaboratorJoined::class);
|
||||
|
||||
/** @var PlaylistCollaborationToken $token */
|
||||
$token = PlaylistCollaborationToken::factory()->create();
|
||||
$user = create_user();
|
||||
|
||||
$this->travel(8)->days();
|
||||
|
||||
$this->service->acceptUsingToken($token->token, $user);
|
||||
|
||||
self::assertFalse($token->refresh()->playlist->collaborators->contains($user));
|
||||
}
|
||||
|
||||
public function testGetCollaborators(): void
|
||||
{
|
||||
/** @var Playlist $playlist */
|
||||
$playlist = Playlist::factory()->create();
|
||||
$user = create_user();
|
||||
$playlist->addCollaborator($user);
|
||||
|
||||
$collaborators = $this->service->getCollaborators($playlist->refresh());
|
||||
|
||||
self::assertEqualsCanonicalizing([$playlist->user_id, $user->id], $collaborators->pluck('id')->toArray());
|
||||
}
|
||||
|
||||
public function testRemoveCollaborator(): void
|
||||
{
|
||||
/** @var Playlist $playlist */
|
||||
$playlist = Playlist::factory()->create();
|
||||
$user = create_user();
|
||||
$playlist->addCollaborator($user);
|
||||
|
||||
self::assertTrue($playlist->refresh()->hasCollaborator($user));
|
||||
|
||||
$this->service->removeCollaborator($playlist, $user);
|
||||
|
||||
self::assertFalse($playlist->refresh()->hasCollaborator($user));
|
||||
}
|
||||
|
||||
public function testCannotRemoveNonExistingCollaborator(): void
|
||||
{
|
||||
$this->expectException(NotAPlaylistCollaboratorException::class);
|
||||
|
||||
/** @var Playlist $playlist */
|
||||
$playlist = Playlist::factory()->create();
|
||||
$user = create_user();
|
||||
|
||||
$this->service->removeCollaborator($playlist, $user);
|
||||
}
|
||||
|
||||
public function testCannotRemoveOwner(): void
|
||||
{
|
||||
$this->expectException(CannotRemoveOwnerFromPlaylistException::class);
|
||||
|
||||
/** @var Playlist $playlist */
|
||||
$playlist = Playlist::factory()->create();
|
||||
|
||||
$this->service->removeCollaborator($playlist, $playlist->user);
|
||||
}
|
||||
}
|
|
@ -2,46 +2,56 @@
|
|||
|
||||
namespace Tests\Integration\KoelPlus\Services;
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\Song;
|
||||
use App\Services\License\FakePlusLicenseService;
|
||||
use Tests\Integration\Services\SmartPlaylistServiceTest as BaseSmartPlaylistServiceTest;
|
||||
use App\Services\SmartPlaylistService;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\PlusTestCase;
|
||||
|
||||
use function Tests\create_user;
|
||||
|
||||
class SmartPlaylistServiceTest extends BaseSmartPlaylistServiceTest
|
||||
class SmartPlaylistServiceTest extends PlusTestCase
|
||||
{
|
||||
private SmartPlaylistService $service;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
License::swap(app()->make(FakePlusLicenseService::class));
|
||||
$this->service = app(SmartPlaylistService::class);
|
||||
}
|
||||
|
||||
public function testOwnSongsOnlyOption(): void
|
||||
{
|
||||
$owner = create_user();
|
||||
$matches = Song::factory()->count(3)->for($owner, 'owner')->create(['title' => 'Foo Something']);
|
||||
|
||||
Song::factory()->count(2)->create(['title' => 'Foo Something']);
|
||||
Song::factory()->count(3)->create(['title' => 'Bar Something']);
|
||||
|
||||
$this->assertMatchesAgainstRules(
|
||||
matches: $matches,
|
||||
rules: [
|
||||
[
|
||||
'id' => 'aaf61bc3-3bdf-4fa4-b9f3-f7f7838ed502',
|
||||
'rules' => [
|
||||
[
|
||||
'id' => '70b08372-b733-4fe2-aedb-639f77120d6d',
|
||||
'model' => 'title',
|
||||
'operator' => 'is',
|
||||
'value' => ['Foo Something'],
|
||||
/** @var Playlist $playlist */
|
||||
$playlist = Playlist::factory()
|
||||
->for($owner)
|
||||
->create([
|
||||
'rules' => [
|
||||
[
|
||||
'id' => Str::uuid()->toString(),
|
||||
'rules' => [
|
||||
[
|
||||
'id' => Str::uuid()->toString(),
|
||||
'model' => 'title',
|
||||
'operator' => 'is',
|
||||
'value' => ['Foo Something'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
owner: $owner,
|
||||
ownSongsOnly: true
|
||||
'own_songs_only' => true,
|
||||
]);
|
||||
|
||||
self::assertEqualsCanonicalizing(
|
||||
$matches->pluck('id')->all(),
|
||||
$this->service->getSongs($playlist, $owner)->pluck('id')->all()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -538,15 +538,9 @@ class SmartPlaylistServiceTest extends TestCase
|
|||
Collection $matches,
|
||||
array $rules,
|
||||
?User $owner = null,
|
||||
bool $ownSongsOnly = false
|
||||
): void {
|
||||
/** @var Playlist $playlist */
|
||||
$playlist = Playlist::factory()
|
||||
->for($owner ?? create_admin())
|
||||
->create([
|
||||
'rules' => $rules,
|
||||
'own_songs_only' => $ownSongsOnly,
|
||||
]);
|
||||
$playlist = Playlist::factory()->for($owner ?? create_admin())->create(['rules' => $rules]);
|
||||
|
||||
self::assertEqualsCanonicalizing(
|
||||
$matches->pluck('id')->all(),
|
||||
|
|
31
tests/Unit/Listeners/MakePlaylistSongsPublicTest.php
Normal file
31
tests/Unit/Listeners/MakePlaylistSongsPublicTest.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Listeners;
|
||||
|
||||
use App\Events\NewPlaylistCollaboratorJoined;
|
||||
use App\Listeners\MakePlaylistSongsPublic;
|
||||
use App\Models\PlaylistCollaborationToken;
|
||||
use App\Services\PlaylistService;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
use function Tests\create_user;
|
||||
|
||||
class MakePlaylistSongsPublicTest extends TestCase
|
||||
{
|
||||
public function testHandle(): void
|
||||
{
|
||||
$collaborator = create_user();
|
||||
|
||||
/** @var PlaylistCollaborationToken $token */
|
||||
$token = PlaylistCollaborationToken::factory()->create();
|
||||
|
||||
$service = Mockery::mock(PlaylistService::class);
|
||||
|
||||
$service->shouldReceive('makePlaylistSongsPublic')
|
||||
->with($token->playlist)
|
||||
->once();
|
||||
|
||||
(new MakePlaylistSongsPublic($service))->handle(new NewPlaylistCollaboratorJoined($collaborator, $token));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue