mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat(plus): allows filtering All Songs by own songs only
This commit is contained in:
parent
ec529266df
commit
3326bc5081
9 changed files with 77 additions and 79 deletions
|
@ -42,14 +42,14 @@ class SongBuilder extends Builder
|
|||
|
||||
public function withMetaFor(User $user, bool $requiresInteractions = false): static
|
||||
{
|
||||
$joinType = $requiresInteractions ? 'join' : 'leftJoin';
|
||||
$joinClosure = static function (JoinClause $join) use ($user): void {
|
||||
$join->on('interactions.song_id', '=', 'songs.id')->where('interactions.user_id', $user->id);
|
||||
};
|
||||
|
||||
return $this
|
||||
->with('artist', 'album', 'album.artist')
|
||||
->$joinType('interactions', static function (JoinClause $join) use ($user): void {
|
||||
$join->on('interactions.song_id', '=', 'songs.id')
|
||||
->where('interactions.user_id', $user->id);
|
||||
})
|
||||
->when($requiresInteractions, static fn (self $query) => $query->join('interactions', $joinClosure))
|
||||
->unless($requiresInteractions, static fn (self $query) => $query->leftJoin('interactions', $joinClosure))
|
||||
->join('albums', 'songs.album_id', '=', 'albums.id')
|
||||
->join('artists', 'songs.artist_id', '=', 'artists.id')
|
||||
->select(
|
||||
|
@ -81,24 +81,17 @@ class SongBuilder extends Builder
|
|||
Assert::oneOf($column, self::VALID_SORT_COLUMNS);
|
||||
Assert::oneOf(strtolower($direction), ['asc', 'desc']);
|
||||
|
||||
$this->orderBy($column, $direction);
|
||||
|
||||
if ($column === 'artists.name') {
|
||||
$this->orderBy('albums.name')
|
||||
return $this->orderBy($column, $direction)
|
||||
->when($column === 'artists.name', static fn (self $query) => $query->orderBy('albums.name')
|
||||
->orderBy('songs.disc')
|
||||
->orderBy('songs.track')
|
||||
->orderBy('songs.title');
|
||||
} elseif ($column === 'albums.name') {
|
||||
$this->orderBy('artists.name')
|
||||
->orderBy('songs.title'))
|
||||
->when($column === 'albums.name', static fn (self $query) => $query->orderBy('artists.name')
|
||||
->orderBy('songs.disc')
|
||||
->orderBy('songs.track')
|
||||
->orderBy('songs.title');
|
||||
} elseif ($column === 'track') {
|
||||
$this->orderBy('songs.disc')
|
||||
->orderBy('songs.track');
|
||||
}
|
||||
|
||||
return $this;
|
||||
->orderBy('songs.title'))
|
||||
->when($column === 'track', static fn (self $query) => $query->orderBy('songs.disc')
|
||||
->orderBy('songs.track'));
|
||||
}
|
||||
|
||||
private static function normalizeSortColumn(string $column): string
|
||||
|
|
|
@ -36,9 +36,10 @@ class SongController extends Controller
|
|||
{
|
||||
return SongResource::collection(
|
||||
$this->songRepository->getForListing(
|
||||
$request->sort ?: 'songs.title',
|
||||
$request->order ?: 'asc',
|
||||
$this->user
|
||||
sortColumn: $request->sort ?: 'songs.title',
|
||||
sortDirection: $request->order ?: 'asc',
|
||||
ownSongOnly: (bool) $request->ownSongsOnly,
|
||||
scopedUser: $this->user
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace App\Http\Requests\API;
|
|||
/**
|
||||
* @property-read string $order
|
||||
* @property-read string $sort
|
||||
* @property-read boolean|string|integer $ownSongsOnly
|
||||
*/
|
||||
class SongListRequest extends Request
|
||||
{
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Builders\AlbumBuilder;
|
||||
use App\Facades\License;
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
|
@ -31,19 +32,16 @@ class AlbumRepository extends Repository
|
|||
/** @var ?User $user */
|
||||
$user ??= $this->auth->user();
|
||||
|
||||
$query = Album::query()
|
||||
return Album::query()
|
||||
->isStandard()
|
||||
->accessibleBy($user);
|
||||
|
||||
if (License::isCommunity()) {
|
||||
// if the license is Plus, accessibleBy() would have already joined the songs table
|
||||
// and we don't want to join it twice
|
||||
$query->leftJoin('songs', 'albums.id', 'songs.album_id');
|
||||
}
|
||||
|
||||
return $query->join('interactions', static function (JoinClause $join) use ($user): void {
|
||||
$join->on('songs.id', 'interactions.song_id')->where('interactions.user_id', $user->id);
|
||||
})
|
||||
->accessibleBy($user)
|
||||
->unless(
|
||||
License::isPlus(), // if the license is Plus, accessibleBy() would have already joined with `songs`
|
||||
static fn (AlbumBuilder $query) => $query->leftJoin('songs', 'albums.id', 'songs.album_id')
|
||||
)
|
||||
->join('interactions', static function (JoinClause $join) use ($user): void {
|
||||
$join->on('songs.id', 'interactions.song_id')->where('interactions.user_id', $user->id);
|
||||
})
|
||||
->groupBy('albums.id')
|
||||
->distinct()
|
||||
->orderByDesc('play_count')
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Builders\ArtistBuilder;
|
||||
use App\Facades\License;
|
||||
use App\Models\Artist;
|
||||
use App\Models\User;
|
||||
|
@ -17,19 +18,16 @@ class ArtistRepository extends Repository
|
|||
/** @var ?User $user */
|
||||
$user ??= auth()->user();
|
||||
|
||||
$query = Artist::query()
|
||||
return Artist::query()
|
||||
->isStandard()
|
||||
->accessibleBy($user);
|
||||
|
||||
if (License::isCommunity()) {
|
||||
// if the license is Plus, accessibleBy() would have already joined the songs table
|
||||
// and we don't want to join it twice
|
||||
$query->leftJoin('songs', 'artists.id', 'songs.artist_id');
|
||||
}
|
||||
|
||||
return $query->join('interactions', static function (JoinClause $join) use ($user): void {
|
||||
$join->on('interactions.song_id', '=', 'songs.id')->where('interactions.user_id', $user->id);
|
||||
})
|
||||
->accessibleBy($user)
|
||||
->unless(
|
||||
License::isPlus(), // if the license is Plus, accessibleBy() would have already joined with `songs`
|
||||
static fn (ArtistBuilder $query) => $query->leftJoin('songs', 'artists.id', 'songs.artist_id')
|
||||
)
|
||||
->join('interactions', static function (JoinClause $join) use ($user): void {
|
||||
$join->on('interactions.song_id', '=', 'songs.id')->where('interactions.user_id', $user->id);
|
||||
})
|
||||
->groupBy([
|
||||
'artists.id',
|
||||
'play_count',
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Interaction;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class InteractionRepository extends Repository
|
||||
{
|
||||
/** @return Collection|array<Interaction> */
|
||||
public function getUserFavorites(User $user): Collection
|
||||
{
|
||||
return Interaction::query()
|
||||
->where([
|
||||
'user_id' => $user->id,
|
||||
'liked' => true,
|
||||
])
|
||||
->with('song')
|
||||
->pluck('song');
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Builders\SongBuilder;
|
||||
use App\Models\Album;
|
||||
use App\Models\Artist;
|
||||
use App\Models\Playlist;
|
||||
|
@ -72,6 +73,7 @@ class SongRepository extends Repository
|
|||
public function getForListing(
|
||||
string $sortColumn,
|
||||
string $sortDirection,
|
||||
bool $ownSongOnly = false,
|
||||
?User $scopedUser = null,
|
||||
int $perPage = 50
|
||||
): Paginator {
|
||||
|
@ -81,6 +83,7 @@ class SongRepository extends Repository
|
|||
return Song::query()
|
||||
->accessibleBy($scopedUser)
|
||||
->withMetaFor($scopedUser)
|
||||
->when($ownSongOnly, static fn (SongBuilder $query) => $query->where('songs.owner_id', $scopedUser->id))
|
||||
->sort($sortColumn, $sortDirection)
|
||||
->simplePaginate($perPage);
|
||||
}
|
||||
|
|
|
@ -14,11 +14,16 @@
|
|||
</template>
|
||||
|
||||
<template #controls>
|
||||
<SongListControls
|
||||
v-if="totalSongCount && (!isPhone || showingControls)"
|
||||
@play-all="playAll"
|
||||
@play-selected="playSelected"
|
||||
/>
|
||||
<div class="controls">
|
||||
<SongListControls
|
||||
v-if="totalSongCount && (!isPhone || showingControls)"
|
||||
@play-all="playAll"
|
||||
@play-selected="playSelected"
|
||||
/>
|
||||
<label class="text-secondary" v-if="isPlus">
|
||||
<CheckBox v-model="ownSongsOnly" /> Own songs only
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
||||
|
@ -35,14 +40,15 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, toRef } from 'vue'
|
||||
import { computed, ref, toRef, watch } from 'vue'
|
||||
import { logger, pluralize, secondsToHumanReadable } from '@/utils'
|
||||
import { commonStore, queueStore, songStore } from '@/stores'
|
||||
import { playbackService } from '@/services'
|
||||
import { useMessageToaster, useRouter, useSongList } from '@/composables'
|
||||
import { localStorageService, playbackService } from '@/services'
|
||||
import { useMessageToaster, useKoelPlus, useRouter, useSongList } from '@/composables'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import SongListSkeleton from '@/components/ui/skeletons/SongListSkeleton.vue'
|
||||
import CheckBox from '@/components/ui/CheckBox.vue'
|
||||
|
||||
const totalSongCount = toRef(commonStore.state, 'song_count')
|
||||
const totalDuration = computed(() => secondsToHumanReadable(commonStore.state.song_length))
|
||||
|
@ -66,6 +72,7 @@ const {
|
|||
|
||||
const { toastError } = useMessageToaster()
|
||||
const { go, onScreenActivated } = useRouter()
|
||||
const { isPlus } = useKoelPlus()
|
||||
|
||||
let initialized = false
|
||||
const loading = ref(false)
|
||||
|
@ -76,6 +83,16 @@ const page = ref<number | null>(1)
|
|||
const moreSongsAvailable = computed(() => page.value !== null)
|
||||
const showSkeletons = computed(() => loading.value && songs.value.length === 0)
|
||||
|
||||
const ownSongsOnly = ref(isPlus.value ? Boolean(localStorageService.get('own-songs-only')) : false)
|
||||
|
||||
watch(ownSongsOnly, async value => {
|
||||
localStorageService.set('own-songs-only', value)
|
||||
page.value = 1
|
||||
songStore.state.songs = []
|
||||
|
||||
await fetchSongs()
|
||||
})
|
||||
|
||||
const sort = async (field: SongListSortField, order: SortOrder) => {
|
||||
page.value = 1
|
||||
songStore.state.songs = []
|
||||
|
@ -91,7 +108,7 @@ const fetchSongs = async () => {
|
|||
loading.value = true
|
||||
|
||||
try {
|
||||
page.value = await songStore.paginate(sortField, sortOrder, page.value!)
|
||||
page.value = await songStore.paginate(sortField, sortOrder, page.value!, ownSongsOnly.value)
|
||||
} catch (error) {
|
||||
toastError('Failed to load songs.')
|
||||
logger.error(error)
|
||||
|
@ -118,3 +135,12 @@ onScreenActivated('Songs', async () => {
|
|||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.controls {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -204,9 +204,9 @@ export const songStore = {
|
|||
return this.syncWithVault(await http.get<Song[]>(`genres/${name}/songs/random?limit=${limit}`))
|
||||
},
|
||||
|
||||
async paginate (sortField: SongListSortField, sortOrder: SortOrder, page: number) {
|
||||
async paginate (sortField: SongListSortField, sortOrder: SortOrder, page: number, ownSongOnly: boolean) {
|
||||
const resource = await http.get<PaginatorResource>(
|
||||
`songs?page=${page}&sort=${sortField}&order=${sortOrder}`
|
||||
`songs?page=${page}&sort=${sortField}&order=${sortOrder}&ownSongsOnly=${ownSongOnly ? 1 : 0}`
|
||||
)
|
||||
|
||||
this.state.songs = unionBy(this.state.songs, this.syncWithVault(resource.data), 'id')
|
||||
|
|
Loading…
Reference in a new issue