mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
fix: only consider episodes accessible if subscribed to podcasts
This commit is contained in:
parent
ee4c97168b
commit
0b622ba0d7
7 changed files with 58 additions and 14 deletions
|
@ -69,17 +69,28 @@ class SongBuilder extends Builder
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function accessibleBy(User $user, bool $withTableName = true): self
|
public function accessibleBy(User $user): self
|
||||||
{
|
{
|
||||||
if (License::isCommunity()) {
|
if (License::isCommunity()) {
|
||||||
// In the Community Edition, all songs are accessible by all users.
|
// In the Community Edition, all songs are accessible by all users.
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->where(static function (Builder $query) use ($user, $withTableName): void {
|
// We want to alias both podcasts and podcast_user tables to avoid possible conflicts with other joins.
|
||||||
$query->where(($withTableName ? 'songs.' : '') . 'is_public', true)
|
return $this->leftJoin('podcasts as podcasts_a11y', 'songs.podcast_id', 'podcasts_a11y.id')
|
||||||
->orWhere(($withTableName ? 'songs.' : '') . 'owner_id', $user->id);
|
->leftJoin('podcast_user as podcast_user_a11y', static function (JoinClause $join) use ($user): void {
|
||||||
});
|
$join->on('podcasts_a11y.id', 'podcast_user_a11y.podcast_id')
|
||||||
|
->where('podcast_user_a11y.user_id', $user->id);
|
||||||
|
})
|
||||||
|
->where(static function (Builder $query) use ($user): void {
|
||||||
|
// Songs must be public or owned by the user.
|
||||||
|
$query->where('songs.is_public', true)
|
||||||
|
->orWhere('songs.owner_id', $user->id);
|
||||||
|
})->whereNot(static function (Builder $query): void {
|
||||||
|
// Episodes must belong to a podcast that the user is not subscribed to.
|
||||||
|
$query->whereNotNull('songs.podcast_id')
|
||||||
|
->whereNull('podcast_user_a11y.podcast_id');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sortByOneColumn(string $column, string $direction): self
|
private function sortByOneColumn(string $column, string $direction): self
|
||||||
|
|
|
@ -19,6 +19,10 @@ enum SmartPlaylistModel: string
|
||||||
public function toColumnName(): string
|
public function toColumnName(): string
|
||||||
{
|
{
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
|
self::TITLE => 'songs.title',
|
||||||
|
self::LENGTH => 'songs.length',
|
||||||
|
self::GENRE => 'songs.genre',
|
||||||
|
self::YEAR => 'songs.year',
|
||||||
self::ALBUM_NAME => 'albums.name',
|
self::ALBUM_NAME => 'albums.name',
|
||||||
self::ARTIST_NAME => 'artists.name',
|
self::ARTIST_NAME => 'artists.name',
|
||||||
self::DATE_ADDED => 'songs.created_at',
|
self::DATE_ADDED => 'songs.created_at',
|
||||||
|
|
|
@ -47,10 +47,10 @@ class PlaylistSongController extends Controller
|
||||||
|
|
||||||
$this->authorize('collaborate', $playlist);
|
$this->authorize('collaborate', $playlist);
|
||||||
|
|
||||||
$songs = $this->songRepository->getMany(ids: $request->songs, scopedUser: $this->user);
|
$playables = $this->songRepository->getMany(ids: $request->songs, scopedUser: $this->user);
|
||||||
|
|
||||||
return self::createResourceCollection(
|
return self::createResourceCollection(
|
||||||
$this->playlistService->addPlayablesToPlaylist($playlist, $songs, $this->user)
|
$this->playlistService->addPlayablesToPlaylist($playlist, $playables, $this->user)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,19 +2,18 @@
|
||||||
|
|
||||||
namespace App\Http\Requests\API;
|
namespace App\Http\Requests\API;
|
||||||
|
|
||||||
use App\Models\Song;
|
use App\Rules\AllPlayablesAreAccessibleBy;
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property-read array<string> $songs
|
* @property-read array<string> $songs
|
||||||
*/
|
*/
|
||||||
class AddSongsToPlaylistRequest extends Request
|
class AddSongsToPlaylistRequest extends Request
|
||||||
{
|
{
|
||||||
/** @return array<mixed> */
|
/** @inheritdoc */
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'songs' => ['required', 'array', Rule::exists(Song::class, 'id')],
|
'songs' => ['required', 'array', new AllPlayablesAreAccessibleBy($this->user())],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
namespace App\Http\Requests\API;
|
namespace App\Http\Requests\API;
|
||||||
|
|
||||||
use App\Models\PlaylistFolder;
|
use App\Models\PlaylistFolder;
|
||||||
use App\Models\Song;
|
use App\Rules\AllPlayablesAreAccessibleBy;
|
||||||
use App\Rules\ValidSmartPlaylistRulePayload;
|
use App\Rules\ValidSmartPlaylistRulePayload;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
@ -21,8 +21,7 @@ class PlaylistStoreRequest extends Request
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => 'required',
|
'name' => 'required',
|
||||||
'songs' => 'array',
|
'songs' => ['array', new AllPlayablesAreAccessibleBy($this->user())],
|
||||||
'songs.*' => [Rule::exists(Song::class, 'id')],
|
|
||||||
'rules' => ['array', 'nullable', new ValidSmartPlaylistRulePayload()],
|
'rules' => ['array', 'nullable', new ValidSmartPlaylistRulePayload()],
|
||||||
'folder_id' => ['nullable', 'sometimes', Rule::exists(PlaylistFolder::class, 'id')],
|
'folder_id' => ['nullable', 'sometimes', Rule::exists(PlaylistFolder::class, 'id')],
|
||||||
'own_songs_only' => 'sometimes',
|
'own_songs_only' => 'sometimes',
|
||||||
|
|
|
@ -54,6 +54,7 @@ use Throwable;
|
||||||
* // The following are only available for collaborative playlists
|
* // The following are only available for collaborative playlists
|
||||||
* @property-read ?string $collaborator_email The email of the user who added the song to the playlist
|
* @property-read ?string $collaborator_email The email of the user who added the song to the playlist
|
||||||
* @property-read ?string $collaborator_name The name of the user who added the song to the playlist
|
* @property-read ?string $collaborator_name The name of the user who added the song to the playlist
|
||||||
|
* @property-read ?string $collaborator_avatar The avatar of the user who added the song to the playlist
|
||||||
* @property-read ?int $collaborator_id The ID of the user who added the song to the playlist
|
* @property-read ?int $collaborator_id The ID of the user who added the song to the playlist
|
||||||
* @property-read ?string $added_at The date the song was added to the playlist
|
* @property-read ?string $added_at The date the song was added to the playlist
|
||||||
* @property-read PlayableType $type
|
* @property-read PlayableType $type
|
||||||
|
|
30
app/Rules/AllPlayablesAreAccessibleBy.php
Normal file
30
app/Rules/AllPlayablesAreAccessibleBy.php
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Rules;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Repositories\SongRepository;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
class AllPlayablesAreAccessibleBy implements ValidationRule
|
||||||
|
{
|
||||||
|
public function __construct(private readonly User $user)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the validation rule.
|
||||||
|
*
|
||||||
|
* @param array<string> $value
|
||||||
|
*/
|
||||||
|
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
|
{
|
||||||
|
$ids = array_unique(Arr::wrap($value));
|
||||||
|
|
||||||
|
if ($ids && app(SongRepository::class)->getMany(ids: $ids, scopedUser: $this->user)->count() !== count($ids)) {
|
||||||
|
$fail('Not all songs or episodes exist in the database or are accessible by the user.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue