feat(plus): allows filtering All Songs by own songs only

This commit is contained in:
Phan An 2024-01-11 23:14:22 +01:00
parent ec0bbfc88d
commit 94fc39e532
9 changed files with 77 additions and 79 deletions

View file

@ -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

View file

@ -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
)
);
}

View file

@ -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
{

View file

@ -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')

View file

@ -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',

View file

@ -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');
}
}

View file

@ -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);
}

View file

@ -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>

View file

@ -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')