diff --git a/packages/common/src/api/tan-query/lineups/useForYouFeed.ts b/packages/common/src/api/tan-query/lineups/useForYouFeed.ts index 9a0a16b864d..408aa421710 100644 --- a/packages/common/src/api/tan-query/lineups/useForYouFeed.ts +++ b/packages/common/src/api/tan-query/lineups/useForYouFeed.ts @@ -1,15 +1,16 @@ -import { Id } from '@audius/sdk' +import { EntityType, Id } from '@audius/sdk' import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query' -import { userTrackMetadataFromSDK } from '~/adapters/track' -import { transformAndCleanList } from '~/adapters/utils' -import { primeTrackData, useQueryContext } from '~/api/tan-query/utils' -import { ID } from '~/models' +import { transformAndCleanList, userFeedItemFromSDK } from '~/adapters' +import { useQueryContext } from '~/api/tan-query/utils' +import { ID, UserCollectionMetadata, UserTrackMetadata } from '~/models' import { QUERY_KEYS } from '../queryKeys' -import { QueryKey, QueryOptions } from '../types' +import { LineupData, QueryKey, QueryOptions } from '../types' import { useCurrentUserId } from '../users/account/useCurrentUserId' import { makeLoadNextPage } from '../utils/infiniteQueryLoadNextPage' +import { primeCollectionData } from '../utils/primeCollectionData' +import { primeTrackData } from '../utils/primeTrackData' export const FOR_YOU_INITIAL_PAGE_SIZE = 10 export const FOR_YOU_LOAD_MORE_PAGE_SIZE = 10 @@ -20,13 +21,16 @@ type ForYouFeedArgs = { } export const getForYouFeedQueryKey = (userId: ID | null | undefined) => { - return [QUERY_KEYS.forYouFeed, userId] as unknown as QueryKey + return [QUERY_KEYS.forYouFeed, userId] as unknown as QueryKey } /** * "For You" feed for the Feed page. Backed by the dedicated - * `GET /v1/users/{id}/feed/for-you` endpoint — a lean 3-source pipeline - * (in-network, trending, underground) with linear ranking and diversity pass. + * `GET /v1/users/{id}/feed/for-you` endpoint — a lean 6-source pipeline + * (in-network, trending, underground × tracks + playlists) with linear + * ranking and a shared per-owner diversity pass. Returns a heterogenous + * feed of tracks and playlists/albums, mirroring the chronological feed + * shape; consumers that only render tracks can use `trackIds`. */ export const useForYouFeed = ( { @@ -43,15 +47,15 @@ export const useForYouFeed = ( const query = useInfiniteQuery({ initialPageParam: 0, - getNextPageParam: (lastPage: ID[], allPages) => { + getNextPageParam: (lastPage: LineupData[], allPages) => { const isFirstPage = allPages.length === 1 const currentPageSize = isFirstPage ? initialPageSize : loadMorePageSize if (lastPage.length < currentPageSize) return undefined return allPages.reduce((total, page) => total + page.length, 0) }, queryKey, - queryFn: async ({ pageParam }) => { - if (!currentUserId) return [] as ID[] + queryFn: async ({ pageParam }): Promise => { + if (!currentUserId) return [] const isFirstPage = pageParam === 0 const currentPageSize = isFirstPage ? initialPageSize : loadMorePageSize const sdk = await audiusSdk() @@ -62,19 +66,45 @@ export const useForYouFeed = ( offset: pageParam }) - const tracks = primeTrackData({ - tracks: transformAndCleanList(data, userTrackMetadataFromSDK), - queryClient - }) + const feed = transformAndCleanList(data, userFeedItemFromSDK).map( + ({ item }) => item + ) + if (feed === null) return [] + + const { tracks, collections } = feed.reduce( + (acc, item) => { + if ('track_id' in item) { + acc.tracks.push(item) + } else { + acc.collections.push(item) + } + return acc + }, + { + tracks: [] as UserTrackMetadata[], + collections: [] as UserCollectionMetadata[] + } + ) + + // Prime caches so tile renders don't have to re-fetch + primeTrackData({ tracks, queryClient }) + primeCollectionData({ collections, queryClient }) - return tracks.map(({ track_id }) => track_id) + return feed.map((item) => + 'track_id' in item + ? { id: item.track_id, type: EntityType.TRACK } + : { id: item.playlist_id, type: EntityType.PLAYLIST } + ) }, select: (data) => data?.pages.flat(), ...options, enabled: options?.enabled !== false && currentUserId !== null }) - const trackIds = query.data ?? [] + const data = query.data ?? [] + const trackIds = data + .filter((d) => d.type === EntityType.TRACK) + .map((d) => d.id as ID) // When the query is disabled, react-query keeps isPending/isLoading true // (data is undefined). Surface them as false so consumers can render an @@ -82,6 +112,7 @@ export const useForYouFeed = ( const isDisabled = currentUserId === null || options?.enabled === false return { + data, trackIds, isPending: isDisabled ? false : query.isPending, isLoading: isDisabled ? false : query.isLoading, diff --git a/packages/sdk/src/sdk/api/generated/default/apis/UsersApi.ts b/packages/sdk/src/sdk/api/generated/default/apis/UsersApi.ts index 729b339d9c8..931c3652767 100644 --- a/packages/sdk/src/sdk/api/generated/default/apis/UsersApi.ts +++ b/packages/sdk/src/sdk/api/generated/default/apis/UsersApi.ts @@ -4182,10 +4182,10 @@ export class UsersApi extends runtime.BaseAPI { /** * @hidden - * Returns a personalized For You feed for the user identified in the path. Twitter-style multi-source pipeline — candidate retrieval (in-network, trending, underground) → linear ranking (recency decay × engagement × social affinity, weighted by source) → diversity (per-artist cap + consecutive-same-artist lookahead). + * Returns a personalized For You feed for the user identified in the path. The response is a heterogenous list of tracks and playlists/albums (`{type, timestamp, item}` envelope), mirroring the chronological /v1/users/{id}/feed shape so clients can render both in a single ranked list. Twitter-style multi-source pipeline — candidate retrieval (in-network, trending, underground) → linear ranking (recency decay × engagement × social affinity, weighted by source) → diversity (shared per-artist cap across tracks + collections + consecutive-same-artist lookahead). * Get For You feed for user */ - async getUserForYouFeedRaw(params: GetUserForYouFeedRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + async getUserForYouFeedRaw(params: GetUserForYouFeedRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { if (params.id === null || params.id === undefined) { throw new runtime.RequiredError('id','Required parameter params.id was null or undefined when calling getUserForYouFeed.'); } @@ -4224,14 +4224,14 @@ export class UsersApi extends runtime.BaseAPI { query: queryParameters, }, initOverrides); - return new runtime.JSONApiResponse(response, (jsonValue) => TracksFromJSON(jsonValue)); + return new runtime.JSONApiResponse(response, (jsonValue) => UserFeedResponseFromJSON(jsonValue)); } /** - * Returns a personalized For You feed for the user identified in the path. Twitter-style multi-source pipeline — candidate retrieval (in-network, trending, underground) → linear ranking (recency decay × engagement × social affinity, weighted by source) → diversity (per-artist cap + consecutive-same-artist lookahead). + * Returns a personalized For You feed for the user identified in the path. The response is a heterogenous list of tracks and playlists/albums (`{type, timestamp, item}` envelope), mirroring the chronological /v1/users/{id}/feed shape so clients can render both in a single ranked list. Twitter-style multi-source pipeline — candidate retrieval (in-network, trending, underground) → linear ranking (recency decay × engagement × social affinity, weighted by source) → diversity (shared per-artist cap across tracks + collections + consecutive-same-artist lookahead). * Get For You feed for user */ - async getUserForYouFeed(params: GetUserForYouFeedRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + async getUserForYouFeed(params: GetUserForYouFeedRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { const response = await this.getUserForYouFeedRaw(params, initOverrides); return await response.value(); }