Compare commits
No commits in common. "3b7d669a5db34721dca9697ca2220b7a11eba04d" and "ff733b97745d2dda0ab8d36e1316bfff77179c2a" have entirely different histories.
3b7d669a5d
...
ff733b9774
9 changed files with 24 additions and 563 deletions
|
|
@ -64,61 +64,6 @@ function createFakeDb(trackReleaseDate: Date | null) {
|
|||
} as unknown as Db;
|
||||
}
|
||||
|
||||
function createDetailedTrackDb() {
|
||||
const tracks = [
|
||||
{
|
||||
id: "track-1",
|
||||
albumId: "album-1",
|
||||
platform: "spotify",
|
||||
platform_id: "spotify:track:one",
|
||||
name: "First Track",
|
||||
popularity: 1,
|
||||
duration: 180000,
|
||||
explicit: false,
|
||||
disc_number: 1,
|
||||
track_number: 1,
|
||||
album: { name: "First Album", release_date: new Date("2001-01-01") },
|
||||
artists: [{ id: "artist-1", name: "Shared Artist" }],
|
||||
},
|
||||
{
|
||||
id: "track-2",
|
||||
albumId: "album-2",
|
||||
platform: "spotify",
|
||||
platform_id: "spotify:track:two",
|
||||
name: "Second Track",
|
||||
popularity: 1,
|
||||
duration: 240000,
|
||||
explicit: false,
|
||||
disc_number: 1,
|
||||
track_number: 1,
|
||||
album: { name: "Second Album", release_date: new Date("2010-01-01") },
|
||||
artists: [{ id: "artist-1", name: "Shared Artist" }],
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
query: {
|
||||
track: {
|
||||
findFirst: vi.fn(async () => tracks[0]),
|
||||
findMany: vi.fn(async ({ where }: { where?: { name?: string } } = {}) =>
|
||||
tracks.filter((track) => !where?.name || track.name === where.name),
|
||||
),
|
||||
},
|
||||
artist: {
|
||||
findFirst: vi.fn(async () => ({
|
||||
id: "artist-1",
|
||||
name: "Shared Artist",
|
||||
})),
|
||||
},
|
||||
},
|
||||
select: vi.fn(() => ({
|
||||
from: () => ({
|
||||
where: async () => [],
|
||||
}),
|
||||
})),
|
||||
} as unknown as Db;
|
||||
}
|
||||
|
||||
function makeSong(id: string, platformId: string, name: string): Song {
|
||||
return {
|
||||
id,
|
||||
|
|
@ -289,66 +234,6 @@ describe("question generation", () => {
|
|||
expect(question).toBeNull();
|
||||
});
|
||||
|
||||
it("builds a numeric release-year question for a track", async () => {
|
||||
const randomSpy = vi.spyOn(Math, "random").mockReturnValue(0);
|
||||
const analytics = {
|
||||
storyClusters: [
|
||||
{
|
||||
tracks: [
|
||||
{
|
||||
name: "First Track",
|
||||
artists: [{ name: "Shared Artist" }],
|
||||
albumName: "First Album",
|
||||
},
|
||||
{
|
||||
name: "Second Track",
|
||||
artists: [{ name: "Shared Artist" }],
|
||||
albumName: "Second Album",
|
||||
},
|
||||
],
|
||||
artists: [{ name: "Shared Artist" }],
|
||||
genres: [],
|
||||
},
|
||||
],
|
||||
groupSummary: { mostSharedGenres: [] },
|
||||
} as PartyAnalytics;
|
||||
|
||||
try {
|
||||
const question = await buildNumericQuestion({
|
||||
db: createDetailedTrackDb(),
|
||||
analytics,
|
||||
index: 0,
|
||||
members: [
|
||||
{ userId: "a", name: "A" },
|
||||
{ userId: "b", name: "B" },
|
||||
],
|
||||
history: [
|
||||
{
|
||||
questionIndex: 0,
|
||||
question: {
|
||||
type: "numeric",
|
||||
text: "What's the release year of First Album?",
|
||||
correct: 2001,
|
||||
startTimestamp: 1,
|
||||
endTimestamp: 2,
|
||||
points: 10,
|
||||
range: { min: 1991, max: 2011 },
|
||||
questionKey: "numeric:album-year:First Album",
|
||||
subjectKey: "album:First Album",
|
||||
},
|
||||
responses: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(question?.text).toBe('What year did "First Track" come out?');
|
||||
expect(question?.correct).toBe(2001);
|
||||
expect(question?.questionKey).toBe("numeric:track-year:First Track");
|
||||
} finally {
|
||||
randomSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("skips social fallback names for duplicate members", async () => {
|
||||
const db = createFakeDb(null);
|
||||
const members: PartyQuestionMember[] = [
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { PartyAnalytics } from "../question-utils";
|
||||
import {
|
||||
getQuestionRange,
|
||||
getReleaseYearRange,
|
||||
getTopClusterArtists,
|
||||
getTopClusterTracks,
|
||||
} from "../question-utils";
|
||||
import { getQuestionRange, getReleaseYearRange } from "../question-utils";
|
||||
|
||||
describe("question range helpers", () => {
|
||||
it("normalizes inverted generic ranges", () => {
|
||||
|
|
@ -31,34 +25,3 @@ describe("question range helpers", () => {
|
|||
expect(range.max).toBeLessThanOrEqual(2026);
|
||||
});
|
||||
});
|
||||
|
||||
describe("analytics cluster helpers", () => {
|
||||
it("returns usable tracks and artists across all story clusters", () => {
|
||||
const analytics: PartyAnalytics = {
|
||||
storyClusters: [
|
||||
{
|
||||
tracks: [
|
||||
{ name: "Shared Track", artists: [{ name: "Shared Artist" }] },
|
||||
],
|
||||
artists: [{ name: "Shared Artist" }],
|
||||
},
|
||||
{
|
||||
tracks: [
|
||||
{ name: "Solo Track", artists: [{ name: "Solo Artist" }] },
|
||||
{ name: "" },
|
||||
],
|
||||
artists: [{ name: "Solo Artist" }, { name: "" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(getTopClusterTracks(analytics).map((track) => track.name)).toEqual([
|
||||
"Shared Track",
|
||||
"Solo Track",
|
||||
]);
|
||||
expect(getTopClusterArtists(analytics)).toEqual([
|
||||
"Shared Artist",
|
||||
"Solo Artist",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,47 +14,6 @@ import {
|
|||
resolveQuestionSong,
|
||||
} from "./question-utils";
|
||||
|
||||
type TrackDetails = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
platform_id: string | null;
|
||||
duration: number | null;
|
||||
album?: { name: string | null; release_date: Date | null } | null;
|
||||
artists?: { name: string }[] | null;
|
||||
};
|
||||
|
||||
async function getDetailedTopTracks(
|
||||
dbClient: typeof Db,
|
||||
analytics: PartyAnalytics,
|
||||
): Promise<TrackDetails[]> {
|
||||
const tracks: TrackDetails[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const topTrack of getTopClusterTracks(analytics)) {
|
||||
const dbTracks = (await dbClient.query.track.findMany({
|
||||
where: { name: topTrack.name },
|
||||
with: { album: true, artists: true },
|
||||
})) as TrackDetails[];
|
||||
const sortedTracks = dbTracks.slice().sort((a, b) => {
|
||||
const aArtists = a.artists?.map((artist) => artist.name) ?? [];
|
||||
const bArtists = b.artists?.map((artist) => artist.name) ?? [];
|
||||
const topArtists = topTrack.artists?.map((artist) => artist.name) ?? [];
|
||||
const score = (track: typeof a, artistNames: string[]) =>
|
||||
(track.album?.name === topTrack.albumName ? 2 : 0) +
|
||||
(topArtists.some((name) => artistNames.includes(name)) ? 2 : 0);
|
||||
return score(b, bArtists) - score(a, aArtists);
|
||||
});
|
||||
const track = sortedTracks[0];
|
||||
if (!track || !isUsableText(track.name)) continue;
|
||||
const key = track.platform_id ?? track.id;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
tracks.push(track);
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
export async function buildAudioMetadataQuestion(
|
||||
dbClient: typeof Db,
|
||||
analytics: PartyAnalytics,
|
||||
|
|
@ -155,120 +114,6 @@ export async function buildAudioMetadataQuestion(
|
|||
}
|
||||
}
|
||||
|
||||
for (const topTrack of topTracks) {
|
||||
const correctArtist = topTrack.artists?.find((artist) =>
|
||||
isUsableText(artist.name),
|
||||
)?.name;
|
||||
if (!topTrack.albumName || !correctArtist) continue;
|
||||
const artistOptions = buildOptionsWithCorrect(correctArtist, topArtists, 4);
|
||||
if (artistOptions) {
|
||||
questions.push({
|
||||
key: `audio:album-artist:${topTrack.albumName}:${correctArtist}`,
|
||||
subjectKey: `album:${topTrack.albumName}`,
|
||||
question: {
|
||||
type: "choice",
|
||||
text: `Which artist appears on "${topTrack.albumName}"?`,
|
||||
options: artistOptions,
|
||||
correct: 0,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const detailedTracks = await getDetailedTopTracks(dbClient, analytics);
|
||||
const datedTracks = detailedTracks.filter(
|
||||
(track) => track.album?.release_date,
|
||||
);
|
||||
if (datedTracks.length >= 2) {
|
||||
const sortedByRelease = datedTracks
|
||||
.slice()
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Number(a.album?.release_date ?? 0) -
|
||||
Number(b.album?.release_date ?? 0),
|
||||
);
|
||||
const earliest = sortedByRelease[0];
|
||||
const latest = sortedByRelease.at(-1);
|
||||
const trackNames = sortedByRelease
|
||||
.map((track) => track.name)
|
||||
.filter((name): name is string => isUsableText(name));
|
||||
|
||||
if (earliest?.name) {
|
||||
const options = buildOptionsWithCorrect(earliest.name, trackNames, 4);
|
||||
if (options) {
|
||||
questions.push({
|
||||
key: `audio:release-first:${earliest.name}`,
|
||||
subjectKey: `track:${earliest.name}`,
|
||||
question: {
|
||||
type: "choice",
|
||||
text: "Which of these tracks came out first?",
|
||||
options,
|
||||
correct: 0,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (latest?.name && latest.name !== earliest?.name) {
|
||||
const options = buildOptionsWithCorrect(latest.name, trackNames, 4);
|
||||
if (options) {
|
||||
questions.push({
|
||||
key: `audio:release-last:${latest.name}`,
|
||||
subjectKey: `track:${latest.name}`,
|
||||
question: {
|
||||
type: "choice",
|
||||
text: "Which of these tracks came out most recently?",
|
||||
options,
|
||||
correct: 0,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tracksByArtist = new Map<string, TrackDetails[]>();
|
||||
for (const track of detailedTracks) {
|
||||
if (!track.duration) continue;
|
||||
for (const artist of track.artists ?? []) {
|
||||
if (!isUsableText(artist.name)) continue;
|
||||
const artistTracks = tracksByArtist.get(artist.name) ?? [];
|
||||
artistTracks.push(track);
|
||||
tracksByArtist.set(artist.name, artistTracks);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [artistName, artistTracks] of tracksByArtist) {
|
||||
if (artistTracks.length < 2) continue;
|
||||
const longest = artistTracks
|
||||
.slice()
|
||||
.sort((a, b) => (b.duration ?? 0) - (a.duration ?? 0))[0];
|
||||
if (!longest?.name) continue;
|
||||
const trackNames = artistTracks
|
||||
.map((track) => track.name)
|
||||
.filter((name): name is string => isUsableText(name));
|
||||
const options = buildOptionsWithCorrect(longest.name, trackNames, 4);
|
||||
if (options) {
|
||||
questions.push({
|
||||
key: `audio:artist-longest-track:${artistName}:${longest.name}`,
|
||||
subjectKey: `artist:${artistName}`,
|
||||
question: {
|
||||
type: "choice",
|
||||
text: `What's the longest track by ${artistName}?`,
|
||||
options,
|
||||
correct: 0,
|
||||
points: 10,
|
||||
song: topSong ?? undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const topTrack of topTracks) {
|
||||
const trackSong = await resolveQuestionSong(dbClient, analytics, {
|
||||
trackName: topTrack.name,
|
||||
|
|
|
|||
|
|
@ -21,14 +21,6 @@ type NumericQuestion = Omit<
|
|||
"startTimestamp" | "endTimestamp"
|
||||
>;
|
||||
|
||||
type TrackDetails = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
platform_id: string | null;
|
||||
album?: { name: string | null; release_date: Date | null } | null;
|
||||
artists?: { name: string }[] | null;
|
||||
};
|
||||
|
||||
type BuildNumericQuestionInput = {
|
||||
db: typeof db;
|
||||
analytics: PartyAnalytics;
|
||||
|
|
@ -37,43 +29,6 @@ type BuildNumericQuestionInput = {
|
|||
history: QuizRound[];
|
||||
};
|
||||
|
||||
async function getDetailedTopTracks({
|
||||
db,
|
||||
analytics,
|
||||
}: BuildNumericQuestionInput): Promise<TrackDetails[]> {
|
||||
const tracks: TrackDetails[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const topTrack of analytics?.storyClusters?.flatMap(
|
||||
(cluster) => cluster.tracks ?? [],
|
||||
) ?? []) {
|
||||
if (!isUsableText(topTrack.name)) continue;
|
||||
const dbTracks = (await db.query.track.findMany({
|
||||
where: { name: topTrack.name },
|
||||
with: { album: true, artists: true },
|
||||
})) as TrackDetails[];
|
||||
const topArtists = topTrack.artists?.map((artist) => artist.name) ?? [];
|
||||
const track = dbTracks.slice().sort((a, b) => {
|
||||
const score = (candidate: typeof a) => {
|
||||
const artistNames =
|
||||
candidate.artists?.map((artist) => artist.name) ?? [];
|
||||
return (
|
||||
(candidate.album?.name === topTrack.albumName ? 2 : 0) +
|
||||
(topArtists.some((name) => artistNames.includes(name)) ? 2 : 0)
|
||||
);
|
||||
};
|
||||
return score(b) - score(a);
|
||||
})[0];
|
||||
if (!track || !isUsableText(track.name)) continue;
|
||||
const key = track.platform_id ?? track.id;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
tracks.push(track);
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
async function getAlbumReleaseYear({
|
||||
db,
|
||||
analytics,
|
||||
|
|
@ -106,76 +61,6 @@ async function getAlbumReleaseYear({
|
|||
};
|
||||
}
|
||||
|
||||
async function getTrackReleaseYear(
|
||||
input: BuildNumericQuestionInput,
|
||||
): Promise<NumericQuestion | null> {
|
||||
const tracks = await getDetailedTopTracks(input);
|
||||
const track = tracks.find((track) => track.album?.release_date && track.name);
|
||||
if (!track?.name || !track.album?.release_date) return null;
|
||||
const song = await resolveQuestionSong(input.db, input.analytics, {
|
||||
trackName: track.name,
|
||||
artistNames: track.artists?.map((artist) => artist.name),
|
||||
albumName: track.album?.name ?? undefined,
|
||||
});
|
||||
const correct = track.album.release_date.getFullYear();
|
||||
return {
|
||||
type: "numeric",
|
||||
text: `What year did "${track.name}" come out?`,
|
||||
correct,
|
||||
range: getReleaseYearRange(correct),
|
||||
points: 10,
|
||||
song: song ?? undefined,
|
||||
questionKey: `numeric:track-year:${track.name}`,
|
||||
subjectKey: `track:${track.name}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function getArtistFirstTrackReleaseYear(
|
||||
input: BuildNumericQuestionInput,
|
||||
): Promise<NumericQuestion | null> {
|
||||
const tracks = await getDetailedTopTracks(input);
|
||||
const tracksByArtist = new Map<string, TrackDetails[]>();
|
||||
|
||||
for (const track of tracks) {
|
||||
if (!track.album?.release_date) continue;
|
||||
for (const artist of track.artists ?? []) {
|
||||
if (!isUsableText(artist.name)) continue;
|
||||
const artistTracks = tracksByArtist.get(artist.name) ?? [];
|
||||
artistTracks.push(track);
|
||||
tracksByArtist.set(artist.name, artistTracks);
|
||||
}
|
||||
}
|
||||
|
||||
const artistEntry = Array.from(tracksByArtist.entries()).find(
|
||||
([, artistTracks]) => artistTracks.length >= 2,
|
||||
);
|
||||
if (!artistEntry) return null;
|
||||
const [artistName, artistTracks] = artistEntry;
|
||||
const firstTrack = artistTracks
|
||||
.slice()
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Number(a.album?.release_date ?? 0) - Number(b.album?.release_date ?? 0),
|
||||
)[0];
|
||||
if (!firstTrack?.album?.release_date) return null;
|
||||
const song = await resolveQuestionSong(input.db, input.analytics, {
|
||||
trackName: firstTrack.name ?? undefined,
|
||||
artistNames: [artistName],
|
||||
albumName: firstTrack.album?.name ?? undefined,
|
||||
});
|
||||
const correct = firstTrack.album.release_date.getFullYear();
|
||||
return {
|
||||
type: "numeric",
|
||||
text: `What year did ${artistName}'s first party track come out?`,
|
||||
correct,
|
||||
range: getReleaseYearRange(correct),
|
||||
points: 10,
|
||||
song: song ?? undefined,
|
||||
questionKey: `numeric:artist-first-track-year:${artistName}`,
|
||||
subjectKey: `artist:${artistName}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function countTopTrackListeners({
|
||||
db,
|
||||
analytics,
|
||||
|
|
@ -264,26 +149,6 @@ export async function buildNumericQuestion(
|
|||
});
|
||||
}
|
||||
|
||||
const trackYearQ = await getTrackReleaseYear(input);
|
||||
if (trackYearQ) {
|
||||
questions.push({
|
||||
key: trackYearQ.questionKey ?? `numeric:track-year:${trackYearQ.text}`,
|
||||
subjectKey: trackYearQ.subjectKey,
|
||||
question: trackYearQ,
|
||||
});
|
||||
}
|
||||
|
||||
const artistFirstTrackYearQ = await getArtistFirstTrackReleaseYear(input);
|
||||
if (artistFirstTrackYearQ) {
|
||||
questions.push({
|
||||
key:
|
||||
artistFirstTrackYearQ.questionKey ??
|
||||
`numeric:artist-first-track-year:${artistFirstTrackYearQ.text}`,
|
||||
subjectKey: artistFirstTrackYearQ.subjectKey,
|
||||
question: artistFirstTrackYearQ,
|
||||
});
|
||||
}
|
||||
|
||||
const topTrackQ = await countTopTrackListeners(input);
|
||||
if (topTrackQ) {
|
||||
questions.push({
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ export type AnalyticsTrack = {
|
|||
albumName?: string;
|
||||
memberScores?: { userId: string; score: number }[];
|
||||
};
|
||||
type AnalyticsArtist = { name: string };
|
||||
type QuestionLike = {
|
||||
text: string;
|
||||
questionKey?: string;
|
||||
|
|
@ -181,13 +180,17 @@ function normalizeQuestionKey(value: string): string {
|
|||
}
|
||||
|
||||
export function getTopClusterArtists(analytics: PartyAnalytics): string[] {
|
||||
return getAllClusterArtists(analytics).map((artist) => artist.name);
|
||||
return (analytics?.storyClusters?.[0]?.artists ?? []).map(
|
||||
(artist) => artist.name,
|
||||
);
|
||||
}
|
||||
|
||||
export function getTopClusterTracks(
|
||||
analytics: PartyAnalytics,
|
||||
): AnalyticsTrack[] {
|
||||
return getAllClusterTracks(analytics);
|
||||
return (analytics?.storyClusters?.[0]?.tracks ?? []).filter((track) =>
|
||||
isUsableText(track.name),
|
||||
);
|
||||
}
|
||||
|
||||
export function pickRelevantTrack(
|
||||
|
|
@ -335,9 +338,9 @@ async function collectSongCandidates({
|
|||
);
|
||||
push(peopleSong);
|
||||
|
||||
const topClusterTracks = getAllClusterTracks(analytics).sort(
|
||||
(a, b) => getTrackScore(b) - getTrackScore(a),
|
||||
);
|
||||
const topClusterTracks = [...(analytics?.storyClusters?.[0]?.tracks ?? [])]
|
||||
.filter((track) => isUsableText(track.name))
|
||||
.sort((a, b) => getTrackScore(b) - getTrackScore(a));
|
||||
|
||||
for (const track of topClusterTracks) {
|
||||
const song = await resolveQuestionSong(db, analytics, {
|
||||
|
|
@ -397,9 +400,12 @@ async function resolveSongFromMentionedPeople(
|
|||
|
||||
if (userIds.length === 0) return null;
|
||||
|
||||
const tracks = getAllClusterTracks(analytics).sort(
|
||||
(a, b) => getMemberTrackScore(b, userIds) - getMemberTrackScore(a, userIds),
|
||||
);
|
||||
const tracks = [...(analytics?.storyClusters?.[0]?.tracks ?? [])]
|
||||
.filter((track) => isUsableText(track.name))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
getMemberTrackScore(b, userIds) - getMemberTrackScore(a, userIds),
|
||||
);
|
||||
|
||||
for (const track of tracks) {
|
||||
const song = await resolveQuestionSong(db, analytics, {
|
||||
|
|
@ -478,43 +484,6 @@ function getMemberTrackScore(
|
|||
}, 0);
|
||||
}
|
||||
|
||||
function getAllClusterTracks(analytics: PartyAnalytics): AnalyticsTrack[] {
|
||||
const tracks: AnalyticsTrack[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const cluster of analytics?.storyClusters ?? []) {
|
||||
for (const track of cluster.tracks ?? []) {
|
||||
if (!isUsableText(track.name)) continue;
|
||||
const key = [
|
||||
track.name,
|
||||
track.albumName ?? "",
|
||||
track.artists?.map((artist) => artist.name).join("|") ?? "",
|
||||
].join("::");
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
tracks.push(track);
|
||||
}
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
function getAllClusterArtists(analytics: PartyAnalytics): AnalyticsArtist[] {
|
||||
const artists: AnalyticsArtist[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const cluster of analytics?.storyClusters ?? []) {
|
||||
for (const artist of cluster.artists ?? []) {
|
||||
if (!isUsableText(artist.name)) continue;
|
||||
if (seen.has(artist.name)) continue;
|
||||
seen.add(artist.name);
|
||||
artists.push(artist);
|
||||
}
|
||||
}
|
||||
|
||||
return artists;
|
||||
}
|
||||
|
||||
export function isUsableText(
|
||||
value: string | null | undefined,
|
||||
): value is string {
|
||||
|
|
|
|||
|
|
@ -239,40 +239,6 @@ describe("PartyAnalysisWorkflow", () => {
|
|||
});
|
||||
|
||||
describe("analyzeParty - story clusters", () => {
|
||||
it("keeps names for entities that only appear for later members", async () => {
|
||||
const { partyId, userIdB } = await seedPartyWithTwoSimilarUsers();
|
||||
const uniqueAlbum = await createAlbum("User B Unique Album");
|
||||
const uniqueArtist = await createArtist("User B Unique Artist");
|
||||
const uniqueTrack = await createTrack(
|
||||
"User B Unique Track",
|
||||
uniqueAlbum.id,
|
||||
[uniqueArtist.id],
|
||||
);
|
||||
await addTopTrack(userIdB, uniqueTrack.id, 2);
|
||||
await addTopArtist(userIdB, uniqueArtist.id, 2);
|
||||
|
||||
const result = await partyAnalysisWorkflow.analyzeParty(partyId);
|
||||
const allTracks = result.storyClusters.flatMap(
|
||||
(cluster) => cluster.tracks,
|
||||
);
|
||||
const allArtists = result.storyClusters.flatMap(
|
||||
(cluster) => cluster.artists,
|
||||
);
|
||||
|
||||
expect(allTracks).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: uniqueTrack.id,
|
||||
name: "User B Unique Track",
|
||||
}),
|
||||
);
|
||||
expect(allArtists).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: uniqueArtist.id,
|
||||
name: "User B Unique Artist",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("sorts clusters with all-member cluster first", async () => {
|
||||
const { partyId, sharedTrackId } = await seedPartyWithTwoSimilarUsers();
|
||||
|
||||
|
|
|
|||
|
|
@ -469,9 +469,8 @@ export class PartyAnalysisWorkflow extends ConfiguredInstance {
|
|||
const result = new Map<string, TrackEntityScore>();
|
||||
|
||||
for (const [trackId, memberScores] of entityMap) {
|
||||
const trackInfo = Array.from(memberData.values())
|
||||
.find((data) => data.tracks.has(trackId))
|
||||
?.tracks.get(trackId) ?? {
|
||||
const firstData = memberData.values().next().value;
|
||||
const trackInfo = firstData?.tracks.get(trackId) ?? {
|
||||
name: "",
|
||||
artists: [],
|
||||
albumName: undefined,
|
||||
|
|
@ -526,9 +525,8 @@ export class PartyAnalysisWorkflow extends ConfiguredInstance {
|
|||
}
|
||||
}
|
||||
|
||||
const artistInfo = Array.from(memberData.values())
|
||||
.find((data) => data.artists.has(artistId))
|
||||
?.artists.get(artistId) ?? { name: "" };
|
||||
const firstData = memberData.values().next().value;
|
||||
const artistInfo = firstData?.artists.get(artistId) ?? { name: "" };
|
||||
|
||||
result.set(artistId, {
|
||||
id: artistId,
|
||||
|
|
@ -871,8 +869,7 @@ export class PartyAnalysisWorkflow extends ConfiguredInstance {
|
|||
genres.sort(
|
||||
(a, b) =>
|
||||
b.memberCount - a.memberCount ||
|
||||
b.memberScores.reduce((total, member) => total + member.score, 0) -
|
||||
a.memberScores.reduce((total, member) => total + member.score, 0),
|
||||
b.memberScores.length - a.memberScores.length,
|
||||
);
|
||||
|
||||
// Return top genres that are shared by at least 2 members
|
||||
|
|
|
|||
|
|
@ -1,12 +1,4 @@
|
|||
import { useParty } from "#/hooks/use-party";
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
} from "../ui/empty";
|
||||
import { Spinner } from "../ui/spinner";
|
||||
import { Question } from "./question";
|
||||
import { QuestionReview } from "./question-review";
|
||||
import { Results } from "./results";
|
||||
|
|
@ -14,26 +6,7 @@ import { SpotifyPlayback } from "./spotify-playback";
|
|||
|
||||
export function PartyView() {
|
||||
const { party } = useParty();
|
||||
if (!party) return null;
|
||||
if (!party.data) {
|
||||
if (party.status !== "started") return null;
|
||||
return (
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Preparing party</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Analyzing everyone's music taste and building the first question.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Spinner />
|
||||
<span>This can take a moment.</span>
|
||||
</div>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
);
|
||||
}
|
||||
if (!party?.data) return null;
|
||||
|
||||
switch (party.data.status) {
|
||||
case "running":
|
||||
|
|
|
|||
|
|
@ -20,10 +20,8 @@ function App() {
|
|||
<DeviceChoice />
|
||||
{!user?.lastSyncAt && <SyncButton />}
|
||||
{user && party?.data?.status !== "running" && <PartyQr />}
|
||||
{party?.status === "created" && !party.data && members.length > 1 && (
|
||||
<StartParty />
|
||||
)}
|
||||
{party && (party.data || party.status === "started") && <PartyView />}
|
||||
{party && !party.data && members.length > 1 && <StartParty />}
|
||||
{party?.data && <PartyView />}
|
||||
</MainContent>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue