mirror of
https://github.com/koel/koel
synced 2024-11-10 14:44:13 +00:00
feat: allow making songs public/private
This commit is contained in:
parent
72ba1ea5fb
commit
abb66f2da8
12 changed files with 169 additions and 4 deletions
28
app/Http/Controllers/API/MakeSongsPrivateController.php
Normal file
28
app/Http/Controllers/API/MakeSongsPrivateController.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\ChangeSongsVisibilityRequest;
|
||||
use App\Models\User;
|
||||
use App\Repositories\SongRepository;
|
||||
use App\Services\SongService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class MakeSongsPrivateController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __invoke(
|
||||
ChangeSongsVisibilityRequest $request,
|
||||
SongRepository $repository,
|
||||
SongService $songService,
|
||||
Authenticatable $user
|
||||
) {
|
||||
$songs = $repository->getMany(ids: $request->songs, scopedUser: $user);
|
||||
$songs->each(fn ($song) => $this->authorize('own', $song));
|
||||
|
||||
$songService->makeSongsPrivate($songs);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
28
app/Http/Controllers/API/MakeSongsPublicController.php
Normal file
28
app/Http/Controllers/API/MakeSongsPublicController.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\ChangeSongsVisibilityRequest;
|
||||
use App\Models\User;
|
||||
use App\Repositories\SongRepository;
|
||||
use App\Services\SongService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
class MakeSongsPublicController extends Controller
|
||||
{
|
||||
/** @param User $user */
|
||||
public function __invoke(
|
||||
ChangeSongsVisibilityRequest $request,
|
||||
SongRepository $repository,
|
||||
SongService $songService,
|
||||
Authenticatable $user
|
||||
) {
|
||||
$songs = $repository->getMany(ids: $request->songs, scopedUser: $user);
|
||||
$songs->each(fn ($song) => $this->authorize('own', $song));
|
||||
|
||||
$songService->makeSongsPublic($songs);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
17
app/Http/Requests/API/ChangeSongsVisibilityRequest.php
Normal file
17
app/Http/Requests/API/ChangeSongsVisibilityRequest.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\API;
|
||||
|
||||
/**
|
||||
* @property-read array<string> $songs
|
||||
*/
|
||||
class ChangeSongsVisibilityRequest extends Request
|
||||
{
|
||||
/** @return array<mixed> */
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'songs' => 'required|exists:songs,id',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -35,6 +35,7 @@ class SongResource extends JsonResource
|
|||
'disc' => $this->song->disc,
|
||||
'genre' => $this->song->genre,
|
||||
'year' => $this->song->year,
|
||||
'is_public' => $this->song->is_public,
|
||||
'created_at' => $this->song->created_at,
|
||||
];
|
||||
}
|
||||
|
|
|
@ -8,6 +8,11 @@ use App\Models\User;
|
|||
|
||||
class SongPolicy
|
||||
{
|
||||
public function own(User $user, Song $song): bool
|
||||
{
|
||||
return $song->owner_id === $user->id;
|
||||
}
|
||||
|
||||
public function play(User $user, Song $song): bool
|
||||
{
|
||||
return License::isCommunity() || $song->is_public || $song->owner_id === $user->id;
|
||||
|
|
|
@ -82,6 +82,16 @@ class SongService
|
|||
return $this->songRepository->getOne($song->id);
|
||||
}
|
||||
|
||||
public function makeSongsPublic(Collection $songs): void
|
||||
{
|
||||
Song::query()->whereIn('id', $songs->pluck('id'))->update(['is_public' => true]);
|
||||
}
|
||||
|
||||
public function makeSongsPrivate(Collection $songs): void
|
||||
{
|
||||
Song::query()->whereIn('id', $songs->pluck('id'))->update(['is_public' => false]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string>|string $ids
|
||||
*/
|
||||
|
|
|
@ -25,6 +25,7 @@ const generate = (partOfCompilation = false): Song => {
|
|||
lyrics: faker.lorem.paragraph(),
|
||||
play_count: faker.datatype.number(),
|
||||
liked: faker.datatype.boolean(),
|
||||
is_public: faker.datatype.boolean(),
|
||||
created_at: faker.date.past().toISOString(),
|
||||
playback_state: 'Stopped'
|
||||
}
|
||||
|
|
|
@ -22,9 +22,11 @@
|
|||
<li @click="addSongsToFavorite">Favorites</li>
|
||||
</template>
|
||||
<li v-if="normalPlaylists.length" class="separator" />
|
||||
<ul v-if="normalPlaylists.length" v-koel-overflow-fade class="playlists">
|
||||
<li v-for="p in normalPlaylists" :key="p.id" @click="addSongsToExistingPlaylist(p)">{{ p.name }}</li>
|
||||
</ul>
|
||||
<li v-if="normalPlaylists.length">
|
||||
<ul class="playlists" v-koel-overflow-fade>
|
||||
<li v-for="p in normalPlaylists" :key="p.id" @click="addSongsToExistingPlaylist(p)">{{ p.name }}</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="separator" />
|
||||
<li @click="addSongsToNewPlaylist">New Playlist…</li>
|
||||
</ul>
|
||||
|
@ -42,6 +44,12 @@
|
|||
<li class="separator" />
|
||||
</template>
|
||||
|
||||
<template v-if="visibilityActions.length">
|
||||
<li class="separator" />
|
||||
<li v-for="action in visibilityActions" :key="action.label" @click="action.handler">{{ action.label }}</li>
|
||||
<li class="separator" />
|
||||
</template>
|
||||
|
||||
<li v-if="canModify" @click="openEditForm">Edit…</li>
|
||||
<li v-if="allowsDownload" @click="download">Download</li>
|
||||
<li v-if="onlyOneSongSelected" @click="copyUrl">Copy Shareable URL</li>
|
||||
|
@ -107,6 +115,42 @@ const onlyOneSongSelected = computed(() => songs.value.length === 1)
|
|||
const firstSongPlaying = computed(() => songs.value.length ? songs.value[0].playback_state === 'Playing' : false)
|
||||
const normalPlaylists = computed(() => playlists.value.filter(playlist => !playlist.is_smart))
|
||||
|
||||
const makePublic = () => trigger(async () => {
|
||||
await songStore.makePublic(songs.value)
|
||||
toastSuccess(`Made ${pluralize(songs.value, 'song')} public to everyone.`)
|
||||
})
|
||||
|
||||
const makePrivate = () => trigger(async () => {
|
||||
await songStore.makePrivate(songs.value)
|
||||
toastSuccess(`Removed public access to ${pluralize(songs.value, 'song')}.`)
|
||||
})
|
||||
|
||||
const visibilityActions = computed(() => {
|
||||
if (!isPlus) return []
|
||||
|
||||
// If some songs don't belong to the current user, no actions are available.
|
||||
if (songs.value.some(song => song.owner_id !== currentUser.value?.id)) return []
|
||||
|
||||
const visibilities = Array.from(new Set(songs.value.map(song => song.is_public)))
|
||||
|
||||
if (visibilities.length === 2) {
|
||||
return [
|
||||
{
|
||||
label: 'Make Public',
|
||||
handler: makePublic
|
||||
},
|
||||
{
|
||||
label: 'Make Private',
|
||||
handler: makePrivate
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return visibilities[0]
|
||||
? [{ label: 'Make Private', handler: makePrivate }]
|
||||
: [{ label: 'Make Public', handler: makePublic }]
|
||||
})
|
||||
|
||||
const canBeRemovedFromPlaylist = computed(() => {
|
||||
if (!isCurrentScreen('Playlist')) return false
|
||||
const playlist = playlistStore.byId(parseInt(getRouteParam('id')!))
|
||||
|
@ -174,5 +218,9 @@ ul.playlists {
|
|||
position: relative;
|
||||
max-height: 192px;
|
||||
overflow-y: auto;
|
||||
|
||||
li {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -16,6 +16,7 @@ export type SongUpdateData = {
|
|||
lyrics?: string
|
||||
year?: number | null
|
||||
genre?: string
|
||||
visibility?: 'public' | 'private' | 'unchanged'
|
||||
}
|
||||
|
||||
export interface SongUpdateResult {
|
||||
|
@ -233,5 +234,21 @@ export const songStore = {
|
|||
})
|
||||
|
||||
await http.delete('songs', { songs: ids })
|
||||
},
|
||||
|
||||
async makePublic (songs: Song[]) {
|
||||
await http.put('songs/make-public', {
|
||||
songs: songs.map(song => song.id)
|
||||
})
|
||||
|
||||
songs.forEach(song => song.is_public = true)
|
||||
},
|
||||
|
||||
async makePrivate (songs: Song[]) {
|
||||
await http.put('songs/make-private', {
|
||||
songs: songs.map(song => song.id)
|
||||
})
|
||||
|
||||
songs.forEach(song => song.is_public = false)
|
||||
}
|
||||
}
|
||||
|
|
3
resources/assets/js/types.d.ts
vendored
3
resources/assets/js/types.d.ts
vendored
|
@ -150,6 +150,7 @@ interface Song {
|
|||
play_start_time?: number
|
||||
fmt_length?: string
|
||||
created_at: string
|
||||
is_public: boolean
|
||||
deleted?: boolean
|
||||
}
|
||||
|
||||
|
@ -384,7 +385,7 @@ interface PaginatorResource {
|
|||
}
|
||||
}
|
||||
|
||||
type EditSongFormTabName = 'details' | 'lyrics'
|
||||
type EditSongFormTabName = 'details' | 'lyrics' | 'visibility'
|
||||
|
||||
type ToastMessage = {
|
||||
id: string
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Facades\YouTube;
|
||||
use App\Http\Controllers\API\AlbumController;
|
||||
use App\Http\Controllers\API\AlbumSongController;
|
||||
|
@ -24,6 +25,8 @@ use App\Http\Controllers\API\GenreSongController;
|
|||
use App\Http\Controllers\API\Interaction\BatchLikeController;
|
||||
use App\Http\Controllers\API\Interaction\HandlePlaybackStartedController;
|
||||
use App\Http\Controllers\API\Interaction\ToggleLikeSongController;
|
||||
use App\Http\Controllers\API\MakeSongsPrivateController;
|
||||
use App\Http\Controllers\API\MakeSongsPublicController;
|
||||
use App\Http\Controllers\API\ObjectStorage\S3\SongController as S3SongController;
|
||||
use App\Http\Controllers\API\PlaylistController;
|
||||
use App\Http\Controllers\API\PlaylistFolderController;
|
||||
|
@ -155,6 +158,11 @@ Route::prefix('api')->middleware('api')->group(static function (): void {
|
|||
|
||||
Route::post('invitations', [UserInvitationController::class, 'invite']);
|
||||
Route::delete('invitations', [UserInvitationController::class, 'revoke']);
|
||||
|
||||
if (License::isPlus()) {
|
||||
Route::put('songs/make-public', MakeSongsPublicController::class);
|
||||
Route::put('songs/make-private', MakeSongsPrivateController::class);
|
||||
}
|
||||
});
|
||||
|
||||
// Object-storage (S3) routes
|
||||
|
|
|
@ -29,6 +29,7 @@ class SongTest extends TestCase
|
|||
'genre',
|
||||
'year',
|
||||
'disc',
|
||||
'is_public',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
|
|
Loading…
Reference in a new issue