Skip to content
Open
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
17 changes: 10 additions & 7 deletions src/consts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from "node:path";
import { AnalysisData } from "./types";
import { AnalysisData, TrackAnalysis } from "./types";

export const PORT = process.env.PORT ?? 8080;
export const SPOTIFY_CLIENT_ID = process.env.SPOTIFY_CLIENT_ID as string;
Expand All @@ -12,11 +12,14 @@ export const UI_PATH = path.join(process.cwd(), 'dist', 'ui');
export const cookieName = 'auth_token'

export const defaultAnalysis: AnalysisData = {
trackAnalysis: undefined,
albumAnalysis: undefined,
personnelAndPlaces: undefined,
artistBiography: undefined,
culturalContext: undefined,
recommendations: undefined,
track: undefined,
album: undefined,
artist: undefined,
}

export const defaultTrackAnalysis: TrackAnalysis = {
tags: [],
}
Comment on lines +20 to 22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaultTrackAnalysis is unused in the codebase.




2 changes: 2 additions & 0 deletions src/spotify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export const getRecentlyPlayed = async (session: Omit<SessionData, 'spotifyId'>)
export const convertSpotifyTrackToTrackDetails = (track: SpotifyTrack): TrackDetails => {
return {
trackId: track.id,
albumId: track.album.id,
artistId: track.artists[0].id,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assumes track.artists[0] exists. Consider adding a fallback or validation if the artists array could be empty.

name: track.name,
artist: track.artists.map((artist) => artist.name).join(', '),
album: track.album.name,
Expand Down
54 changes: 43 additions & 11 deletions src/store.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,52 @@
import redisClient from "./redis";
import { TrackDetails } from "./types";
import { TrackDetails, AlbumAnalysis, TrackAnalysis, ArtistAnalysis } from "./types";

const TRACK_KEY_PREFIX = "track:";
const HISTORY_KEY_PREFIX = "history:";
const ANALYSIS_KEY_PREFIX = "analysis:";

const getUserHistoryKey = (userId: string) => `${HISTORY_KEY_PREFIX}${userId}`;
const getTrackKey = (trackId: string) => `${TRACK_KEY_PREFIX}${trackId}`;
//
const getArtistAnalysisKey = (artistId: string) => `${ANALYSIS_KEY_PREFIX}${artistId}`;
const getAlbumAnalysisKey = (albumId: string) => `${ANALYSIS_KEY_PREFIX}${albumId}`;
const getTrackAnalysisKey = (trackId: string) => `${ANALYSIS_KEY_PREFIX}${trackId}`;
Comment on lines +10 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three analysis types share the same "analysis:" prefix, causing Redis key collisions if an artistId, albumId, or trackId match. Use distinct prefixes like "artist:analysis:", "album:analysis:", and "track:analysis:" instead.


export const updateJSON = async <T>(key: string, data: T) => {
await redisClient.set(key, JSON.stringify(data));
};

export const getJSON = async <T>(key: string): Promise<T | undefined> => {
const data = await redisClient.get(key);
return data ? JSON.parse(data) : undefined;
};

export const updateTrack = async (trackId: string, trackDetails: TrackDetails) => {
// handle the different fields
await redisClient.set(getTrackKey(trackId), JSON.stringify(trackDetails));
await updateJSON(getTrackKey(trackId), trackDetails);

const analysis = trackDetails.analysis;
if (analysis?.track) {
await updateJSON(getTrackAnalysisKey(trackId), analysis.track);
}
if (analysis?.album) {
await updateJSON(getAlbumAnalysisKey(trackDetails.albumId), analysis.album);
}
if (analysis?.artist) {
await updateJSON(getArtistAnalysisKey(trackDetails.artistId), analysis.artist);
}
};
Comment on lines 23 to 36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateTrack stores the full track including analysis at line 24, then stores analyses separately at lines 27-35. When reading with getTrack, the track's stored analysis is discarded and replaced with separately-fetched analyses. Consider excluding analysis from the track storage to avoid data duplication and reduce Redis storage usage.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


export const getTrack = async (trackId: string): Promise<TrackDetails | null> => {
const data = await redisClient.get(getTrackKey(trackId));
return data ? JSON.parse(data) : null;
export const getTrack = async (trackId: string): Promise<TrackDetails | undefined> => {
const track = await getJSON<TrackDetails>(getTrackKey(trackId));
if(!track) return undefined;
const analysis = {
album: await getJSON<AlbumAnalysis>(getAlbumAnalysisKey(track.albumId)),
track: await getJSON<TrackAnalysis>(getTrackAnalysisKey(track.trackId)),
artist: await getJSON<ArtistAnalysis>(getArtistAnalysisKey(track.artistId)),
}
return {
...track,
analysis
};
};

export const pushHistory = async (userId: string, trackId: string) => {
Expand Down Expand Up @@ -57,19 +88,20 @@ export const getRelatedTracks = async (trackId: string, userId: string, count =
};

const findTracksWithCommonTags = (track: TrackDetails, tracks: TrackDetails[]) => {
const targetTags = track?.analysis?.tags ?? [];
const targetTags = track?.analysis?.track?.tags ?? [];
const commonTracks = tracks.reduce<TrackDetails[]>((prev, curr) => {
const currTags = curr?.analysis?.track?.tags ?? [];
if (curr.trackId === track.trackId) return prev;
if (prev.some(t => t.trackId === curr.trackId)) return prev;
if (curr.analysis?.tags?.length === 0) return prev;
if (curr.analysis?.tags?.some(tag => targetTags.includes(tag))) {
if (currTags.length === 0) return prev;
if (currTags.some(tag => targetTags.includes(tag))) {
return [...prev, curr];
}
return prev;
}, []);

const countTagsInCommon = (track: TrackDetails) => {
const tags = track.analysis?.tags ?? [];
const tags = track.analysis?.track?.tags ?? [];
return targetTags.filter(tag => tags.includes(tag)).length;
};
return commonTracks.sort((a, b) => {
Expand Down
27 changes: 22 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface SpotifyImage {
}

export interface SpotifyArtist {
id: string;
name: string;
}

Expand All @@ -13,6 +14,7 @@ export interface SpotifyTrack {
name: string;
artists: SpotifyArtist[];
album: {
id: string;
name: string;
images: SpotifyImage[];
release_date: string;
Expand All @@ -35,6 +37,8 @@ export interface SpotifyTokenResponse {

export interface TrackDetails {
trackId: string;
albumId: string;
artistId: string;
name: string;
artist: string;
album: string;
Expand All @@ -52,11 +56,15 @@ export interface Recommendation {
}

export interface AnalysisData {
albumAnalysis?: string;
trackAnalysis?: string;
track?: TrackAnalysis,
album?: AlbumAnalysis,
artist?: ArtistAnalysis,
// video / images / audio
}

export interface TrackAnalysis {
personnelAndPlaces?: string;
artistBiography?: string;
culturalContext?: string;
analysis?: string;
recommendations?: {
albums: Recommendation[];
tracks: Recommendation[];
Expand All @@ -66,12 +74,21 @@ export interface AnalysisData {
tags?: string[];
}

export interface AlbumAnalysis {
analysis?: string;
culturalContext?: string;
}

export interface ArtistAnalysis {
biography?: string;
}
Comment on lines 58 to +84
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These type changes break the existing analysis generation code in src/analysis.ts. The ANALYSIS_SHAPE schema still expects flat fields like trackAnalysis, albumAnalysis, artistBiography, but the new types use nested structures (track.analysis, album.analysis, artist.biography). The analysis system needs updating to match this new structure.


export interface SessionData {
accessToken: string;
refreshToken: string;
expiresAt: number;
spotifyId: string;
}
}

export interface SpotifyUser {
id: string;
Expand Down
Loading