228 lines
5.8 KiB
TypeScript
228 lines
5.8 KiB
TypeScript
import type { db as Db } from "../db";
|
|
import type { Question, QuizRound } from "../party-types";
|
|
import {
|
|
buildOptionsWithCorrect,
|
|
buildOrderedOptions,
|
|
buildQuestionWindow,
|
|
getMostSharedGenreNames,
|
|
getTopClusterArtists,
|
|
getTopClusterTracks,
|
|
isUsableText,
|
|
type PartyAnalytics,
|
|
pickQuestionCandidate,
|
|
pickRandom,
|
|
type QuestionCandidate,
|
|
resolveQuestionSong,
|
|
} from "./question-utils";
|
|
|
|
export async function buildAudioMetadataQuestion(
|
|
dbClient: typeof Db,
|
|
analytics: PartyAnalytics,
|
|
index: number,
|
|
history: QuizRound[],
|
|
): Promise<Question | null> {
|
|
type ChoiceQuestion = Extract<Question, { type: "choice" }>;
|
|
const questions: Array<
|
|
QuestionCandidate<Omit<ChoiceQuestion, "startTimestamp" | "endTimestamp">>
|
|
> = [];
|
|
const topSong = await resolveQuestionSong(dbClient, analytics);
|
|
const topSongName = topSong?.name;
|
|
const topTracks = getTopClusterTracks(analytics);
|
|
const topTrackNames = topTracks.map((track) => track.name);
|
|
if (isUsableText(topSongName)) {
|
|
const currentSongOptions = buildOptionsWithCorrect(
|
|
topSongName,
|
|
topTrackNames,
|
|
4,
|
|
);
|
|
if (currentSongOptions) {
|
|
questions.push({
|
|
key: `audio:current-song:${topSongName}`,
|
|
subjectKey: `track:${topSongName}`,
|
|
question: {
|
|
type: "choice",
|
|
text: "What song is currently playing?",
|
|
options: currentSongOptions,
|
|
correct: 0,
|
|
points: 10,
|
|
song: topSong ?? undefined,
|
|
hideSongTitle: true,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
const genreOptions = buildOrderedOptions(
|
|
getMostSharedGenreNames(analytics),
|
|
4,
|
|
);
|
|
if (genreOptions) {
|
|
const topGenre = genreOptions[0];
|
|
if (topGenre) {
|
|
questions.push({
|
|
key: `audio:genre:${topGenre}`,
|
|
subjectKey: `genre:${topGenre}`,
|
|
question: {
|
|
type: "choice",
|
|
text: "Which genre appears most in the party analytics?",
|
|
options: genreOptions,
|
|
correct: 0,
|
|
points: 10,
|
|
song: topSong ?? undefined,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
const topArtists = getTopClusterArtists(analytics);
|
|
const topArtist = topArtists[0];
|
|
if (topArtist) {
|
|
const artistOptions = buildOptionsWithCorrect(topArtist, topArtists, 4);
|
|
if (artistOptions) {
|
|
questions.push({
|
|
key: `audio:artist:${topArtist}`,
|
|
subjectKey: `artist:${topArtist}`,
|
|
question: {
|
|
type: "choice",
|
|
text: "Which artist shows up most often in the shared audio data?",
|
|
options: artistOptions,
|
|
correct: 0,
|
|
points: 10,
|
|
song: topSong ?? undefined,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
if (topTracks.length > 0) {
|
|
const topTrackName = topTrackNames[0];
|
|
const trackOptions = topTrackName
|
|
? buildOptionsWithCorrect(topTrackName, topTrackNames, 4)
|
|
: null;
|
|
if (trackOptions) {
|
|
questions.push({
|
|
key: `audio:track:${topTrackName}`,
|
|
subjectKey: `track:${topTrackName}`,
|
|
question: {
|
|
type: "choice",
|
|
text: "Which track looks most shared across the party?",
|
|
options: trackOptions,
|
|
correct: 0,
|
|
points: 10,
|
|
song: topSong ?? undefined,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
const randomTopTrack = pickRandom(topTracks);
|
|
if (randomTopTrack) {
|
|
const randomTrackSong = await resolveQuestionSong(dbClient, analytics, {
|
|
trackName: randomTopTrack.name,
|
|
artistNames: randomTopTrack.artists?.map((artist) => artist.name),
|
|
albumName: randomTopTrack.albumName,
|
|
});
|
|
const trackArtists =
|
|
randomTopTrack.artists?.map((artist) => artist.name) ?? [];
|
|
const allArtists = topArtists.length > 0 ? topArtists : trackArtists;
|
|
const correctArtist = trackArtists[0] ?? allArtists[0];
|
|
if (correctArtist) {
|
|
const artistOptions = buildOptionsWithCorrect(
|
|
correctArtist,
|
|
allArtists,
|
|
4,
|
|
);
|
|
if (artistOptions) {
|
|
questions.push({
|
|
key: `audio:performer:${randomTopTrack.name}`,
|
|
subjectKey: `track:${randomTopTrack.name}`,
|
|
question: {
|
|
type: "choice",
|
|
text: `Who performs "${randomTopTrack.name}"?`,
|
|
options: artistOptions,
|
|
correct: 0,
|
|
points: 10,
|
|
song: randomTrackSong ?? topSong ?? undefined,
|
|
},
|
|
});
|
|
}
|
|
|
|
const trackNames = topTracks.map((t) => t.name);
|
|
const trackNameOptions = buildOptionsWithCorrect(
|
|
randomTopTrack.name,
|
|
trackNames,
|
|
4,
|
|
);
|
|
if (trackNameOptions) {
|
|
questions.push({
|
|
key: `audio:title:${randomTopTrack.name}`,
|
|
subjectKey: `track:${randomTopTrack.name}`,
|
|
question: {
|
|
type: "choice",
|
|
text: `What is the name of this track by ${correctArtist}?`,
|
|
options: trackNameOptions,
|
|
correct: 0,
|
|
points: 10,
|
|
song: randomTrackSong ?? topSong ?? undefined,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (isUsableText(topSongName) && topSongName !== randomTopTrack.name) {
|
|
const alternateSongOptions = buildOptionsWithCorrect(
|
|
topSongName,
|
|
trackNames,
|
|
4,
|
|
);
|
|
if (alternateSongOptions) {
|
|
questions.push({
|
|
key: `audio:current-song:${topSongName}`,
|
|
subjectKey: `track:${topSongName}`,
|
|
question: {
|
|
type: "choice",
|
|
text: "Which song is this audio clip from?",
|
|
options: alternateSongOptions,
|
|
correct: 0,
|
|
points: 10,
|
|
song: topSong ?? undefined,
|
|
hideSongTitle: true,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (randomTopTrack.albumName) {
|
|
const albumNames = topTracks
|
|
.map((track) => track.albumName)
|
|
.filter((name): name is string => Boolean(name));
|
|
const albumOptions = buildOptionsWithCorrect(
|
|
randomTopTrack.albumName,
|
|
albumNames,
|
|
4,
|
|
);
|
|
if (albumOptions) {
|
|
questions.push({
|
|
key: `audio:album:${randomTopTrack.albumName}`,
|
|
subjectKey: `track:${randomTopTrack.name}`,
|
|
question: {
|
|
type: "choice",
|
|
text: `"${randomTopTrack.name}" appears on which album?`,
|
|
options: albumOptions,
|
|
correct: 0,
|
|
points: 10,
|
|
song: randomTrackSong ?? topSong ?? undefined,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (questions.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const question = pickQuestionCandidate(questions, history, index);
|
|
if (!question) return null;
|
|
return buildQuestionWindow(question);
|
|
}
|