Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/api/mutations/playlist/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import LibraryStackParamList from '@/src/screens/Library/types'
import { LibraryParamList } from '@/src/screens/Library/types'
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { InfiniteData, useMutation } from '@tanstack/react-query'
Expand All @@ -14,7 +14,7 @@ import { applyHapticFeedback } from '../../../utils/haptics'
export const useAddPlaylist = () => {
const user = getUser()

const libraryStackNavigation = useNavigation<NativeStackNavigationProp<LibraryStackParamList>>()
const libraryStackNavigation = useNavigation<NativeStackNavigationProp<LibraryParamList>>()

return useMutation({
mutationFn: ({ name }: { name: string }) => createPlaylist(name),
Expand Down
129 changes: 92 additions & 37 deletions src/api/queries/album/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'
import { fetchAlbums } from './utils/album'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
import flattenInfiniteQueryPages from '../../../utils/query-selectors'
import { ApiLimits, MaxPages } from '../../../configs/querying/index.config'
import { MaxPages } from '../../../configs/querying/index.config'
import { queryClient } from '../../../constants/query-client'
import { getApi, getUser } from '../../../stores/auth/utils'
import { useJellifyLibrary } from '../../../stores/auth'
Expand All @@ -14,6 +14,18 @@ import { fetchAlbumDiscs } from '../item'
import { Api } from '@jellyfin/sdk/lib/api'
import { AlbumDiscsQueryKey } from './keys'
import { AlbumQuery, RecentlyAddedQuery } from './queries'
import { InfiniteSectionListPageParam } from '../../types/page-param'
import { createLetterPageParamFns } from '../../utils/page-params'

const albumSortByOptions = [
ItemSortBy.Name,
ItemSortBy.SortName,
ItemSortBy.Album,
ItemSortBy.Artist,
ItemSortBy.PlayCount,
ItemSortBy.DateCreated,
ItemSortBy.PremiereDate,
] as ItemSortBy[]

export const useAlbum = (album: BaseItemDto) => useQuery(AlbumQuery(album))

Expand All @@ -28,16 +40,8 @@ const useAlbums = () => {
sortDescending: librarySortDescendingState,
} = useLibraryStore()
const rawAlbumSortBy = librarySortByState.albums ?? ItemSortBy.SortName
const albumSortByOptions = [
ItemSortBy.Name,
ItemSortBy.SortName,
ItemSortBy.Album,
ItemSortBy.Artist,
ItemSortBy.PlayCount,
ItemSortBy.DateCreated,
ItemSortBy.PremiereDate,
] as ItemSortBy[]
const librarySortBy = albumSortByOptions.includes(rawAlbumSortBy as ItemSortBy)

const librarySortBy = albumSortByOptions.includes(rawAlbumSortBy)
? (rawAlbumSortBy as ItemSortBy)
: ItemSortBy.Album
const sortDescending = librarySortDescendingState.albums ?? false
Expand All @@ -56,39 +60,90 @@ const useAlbums = () => {
return flattenInfiniteQueryPages(data)
}

return useInfiniteQuery({
queryKey: [
QueryKeys.InfiniteAlbums,
isFavorites,
library?.musicLibraryId,
librarySortBy,
sortDescending,
yearMin,
yearMax,
],
queryFn: ({ pageParam, signal }) =>
fetchAlbums(
const queryKey = [
QueryKeys.InfiniteAlbums,
isFavorites,
library?.musicLibraryId,
librarySortBy,
sortDescending,
yearMin,
yearMax,
]

const sortBy = [librarySortBy ?? ItemSortBy.SortName]
const sortOrder = [sortDescending ? SortOrder.Descending : SortOrder.Ascending]

const { getNextPageParam, getPreviousPageParam } = createLetterPageParamFns(sortDescending)

/**
* Jumps the albums list directly to {@link letter} with a single `nameStartsWith`/
* `nameLessThan` + `limit`-bounded request scoped to that letter, then seeds the query cache
* with that page - no fetching (or paginating through) albums outside the target letter.
*/
const jumpToLetter = async (letter: string, letterReverseOrder: boolean): Promise<boolean> => {
if (!isSortByLetter || !api || !user || !library) return false

try {
const pageParam: InfiniteSectionListPageParam = {
letter: letter.toUpperCase(),
index: 0,
}

const items = await fetchAlbums(
api,
user,
library,
pageParam,
isFavorites,
[librarySortBy ?? ItemSortBy.SortName],
[sortDescending ? SortOrder.Descending : SortOrder.Ascending],
sortBy,
[letterReverseOrder ? SortOrder.Descending : SortOrder.Ascending],
yearMin,
yearMax,
signal,
),
initialPageParam: 0,
select: selectAlbums,
maxPages: MaxPages.Library,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
return lastPage.length === ApiLimits.Library ? lastPageParam + 1 : undefined
},
getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
return firstPageParam === 0 ? null : firstPageParam - 1
},
})
)

// A jump seeks to a new spot in the list, so it replaces the cache with a single page
// rather than merging - old pages aren't adjacent to it, so keeping them around would
// just leave gaps getNextPageParam/getPreviousPageParam can't page across
queryClient.setQueryData<InfiniteData<BaseItemDto[], InfiniteSectionListPageParam>>(
queryKey,
{
pages: [items],
pageParams: [pageParam],
},
)
return true
} catch {
return false
}
}

return {
...useInfiniteQuery({
queryKey,
queryFn: ({ pageParam, signal }) =>
fetchAlbums(
api,
user,
library,
pageParam,
isFavorites,
sortBy,
sortOrder,
yearMin,
yearMax,
signal,
),
initialPageParam: {
index: 0,
letter: '#',
} as InfiniteSectionListPageParam,
select: selectAlbums,
maxPages: MaxPages.Library,
getNextPageParam,
getPreviousPageParam,
}),
jumpToLetter,
}
}

export default useAlbums
Expand Down
94 changes: 90 additions & 4 deletions src/api/queries/album/utils/album.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,22 @@ import { ApiLimits } from '../../../../configs/querying/index.config'
import buildYearsParam from '../../../../utils/mapping/build-years-param'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'
import { setQueryUserDataForItems } from '../../user-data'
import { InfiniteSectionListPageParam } from '@/src/api/types/page-param'

/**
* Maps an AZScroller {@link letter} ('#' or 'A'-'Z') to the `nameStartsWith`/`nameLessThan`
* filters that select just that letter's albums, so a single bounded request can fetch a
* letter directly instead of paginating through the whole library to reach it.
*/
function letterNameParams(letter: string): { nameStartsWith?: string; nameLessThan?: string } {
return letter === '#' ? { nameLessThan: 'A' } : { nameStartsWith: letter }
}

export function fetchAlbums(
api: Api | undefined,
user: JellifyUser | undefined,
library: JellifyLibrary | undefined,
page: number,
pageParam: InfiniteSectionListPageParam,
isFavorite: boolean | undefined,
sortBy: ItemSortBy[] = [ItemSortBy.SortName],
sortOrder: SortOrder[] = [SortOrder.Ascending],
Expand All @@ -32,27 +42,54 @@ export function fetchAlbums(
if (!library) return reject('Library has not been set')

const yearsParam = buildYearsParam(yearMin, yearMax)
const { nameStartsWith, nameLessThan } = letterNameParams(pageParam.letter)

getItemsApi(api)
.getItems(
const fetchPage = (startIndex: number) =>
getItemsApi(api).getItems(
{
parentId: library.musicLibraryId,
includeItemTypes: [BaseItemKind.MusicAlbum],
userId: user.id,
sortBy: sortBy,
sortOrder: sortOrder,
startIndex: page * ApiLimits.Library,
startIndex,
limit: ApiLimits.Library,
isFavorite: isFavorite,
fields: [ItemFields.SortName],
recursive: true,
years: yearsParam,
nameStartsWith,
nameLessThan,
enableUserData: true,
},
{
signal,
},
)

// A negative index requests this letter's *last* page (paging backwards across a
// letter boundary) - resolve it with a count-only lookup rather than paginating
// forward through the whole letter just to find where it ends.
const startIndexPromise: Promise<number> =
pageParam.index >= 0
? Promise.resolve(pageParam.index)
: fetchAlbumsCount(
api,
user,
library,
isFavorite,
pageParam.letter,
yearMin,
yearMax,
signal,
).then(
(count) =>
Math.max(0, Math.ceil(count / ApiLimits.Library) - 1) *
ApiLimits.Library,
)

startIndexPromise
.then(fetchPage)
.then(({ data }) => {
const items = data.Items ?? []
setQueryUserDataForItems(items)
Expand All @@ -65,6 +102,55 @@ export function fetchAlbums(
})
}

/**
* Fetches the number of albums matching {@link letter} ('#' or 'A'-'Z'), or the total album
* count when omitted. A `limit: 0` count-only request, used to resolve absolute/last-page
* indexes without ever fetching the items themselves.
*/
export function fetchAlbumsCount(
api: Api | undefined,
user: JellifyUser | undefined,
library: JellifyLibrary | undefined,
isFavorite: boolean | undefined,
letter?: string,
yearMin?: number,
yearMax?: number,
signal?: AbortSignal,
): Promise<number> {
return new Promise((resolve, reject) => {
if (!api) return reject('No API instance provided')
if (!user) return reject('No user provided')
if (!library) return reject('Library has not been set')

const yearsParam = buildYearsParam(yearMin, yearMax)
const { nameStartsWith, nameLessThan } = letter ? letterNameParams(letter) : {}

getItemsApi(api)
.getItems(
{
parentId: library.musicLibraryId,
includeItemTypes: [BaseItemKind.MusicAlbum],
userId: user.id,
startIndex: 0,
limit: 0,
isFavorite: isFavorite,
recursive: true,
years: yearsParam,
nameStartsWith,
nameLessThan,
enableTotalRecordCount: true,
},
{
signal,
},
)
.then(({ data }) => resolve(data.TotalRecordCount ?? 0))
.catch((error) => {
reject(error)
})
})
}

export function fetchAlbumById(
api: Api | undefined,
albumId: string,
Expand Down
Loading
Loading