Skip to content

Commit d6c0b8b

Browse files
fix: polish Spotify Track Selection UI optimization PR (#9547)
- Keep `duration` field alongside `duration_ms` for backward compatibility in API responses and types. - Standardize `SpotifyPlaylistItem` type by simplifying `artists` to a `string` and removing redundant `imageUrl`. - Remove redundant Play/Pause `IconButton` in `PlaylistTracksDisplay` and handle clicks on the row directly. - Migrate index-based playback to URI-based playback in `PlaylistTracksDisplay`. - Fix TypeScript error in `socketManager.ts` by replacing `as any` cast. - Resolve unused imports linting issues. - Update related unit tests to match architecture changes. Co-authored-by: arii <342438+arii@users.noreply.github.com>
1 parent 61ab14c commit d6c0b8b

8 files changed

Lines changed: 32 additions & 84 deletions

File tree

app/api/spotify/playlists/[playlistId]/tracks/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ async function getPlaylistTracks(
104104
images: track.album.images,
105105
},
106106
duration_ms: track.duration_ms,
107+
duration: track.duration_ms, // Alias for backward compatibility
107108
uri: track.uri,
108109
}
109110
})

components/Playlist/PlaylistTracksDisplay.tsx

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ import Box from '@mui/material/Box'
77
import Typography from '@mui/material/Typography'
88
import CircularProgress from '@mui/material/CircularProgress'
99
import Alert from '@mui/material/Alert'
10-
import IconButton from '@mui/material/IconButton'
11-
import PlayArrowIcon from '@mui/icons-material/PlayArrow'
12-
import PauseIcon from '@mui/icons-material/Pause'
1310
import List from '@mui/material/List'
1411
import Paper from '@mui/material/Paper'
1512
import Button from '@mui/material/Button'
@@ -60,10 +57,10 @@ const PlaylistTracksDisplay = ({ playlistId }: PlaylistTracksDisplayProps) => {
6057
fetchTracks(offset)
6158
}, [fetchTracks, offset])
6259

63-
const handlePlayTrack = (playlistUri: string, position: number) => {
60+
const handlePlayTrack = (playlistUri: string, uri: string) => {
6461
executeSpotify('PLAY', {
6562
contextUri: playlistUri,
66-
offset: { position },
63+
offset: { uri },
6764
})
6865
}
6966

@@ -111,7 +108,7 @@ const PlaylistTracksDisplay = ({ playlistId }: PlaylistTracksDisplayProps) => {
111108
<Box>
112109
<Paper>
113110
<List dense sx={{ width: '100%', bgcolor: 'background.paper', p: 0 }}>
114-
{tracks.map((track, index) => {
111+
{tracks.map((track) => {
115112
const isPlaying =
116113
spotifyData.playback.is_playing &&
117114
spotifyData.playback.track.id === track.id
@@ -125,32 +122,19 @@ const PlaylistTracksDisplay = ({ playlistId }: PlaylistTracksDisplayProps) => {
125122
onClick={() =>
126123
isPlaying
127124
? handlePause()
128-
: handlePlayTrack(playlistUri, index)
125+
: handlePlayTrack(playlistUri, track.uri)
129126
}
130127
secondaryAction={
131-
<Box sx={{ display: 'flex', alignItems: 'center' }}>
132-
<Typography
133-
variant="caption"
134-
sx={{ color: 'text.secondary', mr: 1 }}
135-
>
136-
{!!track.duration_ms &&
137-
formatDuration(track.duration_ms, {
138-
unit: 'milliseconds',
139-
format: 'MM:SS',
140-
})}
141-
</Typography>
142-
<IconButton
143-
onClick={() =>
144-
isPlaying
145-
? handlePause()
146-
: handlePlayTrack(playlistUri, offset + index)
147-
}
148-
aria-label={isPlaying ? 'Pause' : 'Play'}
149-
size="small"
150-
>
151-
{isPlaying ? <PauseIcon /> : <PlayArrowIcon />}
152-
</IconButton>
153-
</Box>
128+
<Typography
129+
variant="caption"
130+
sx={{ color: 'text.secondary', mr: 1 }}
131+
>
132+
{!!track.duration_ms &&
133+
formatDuration(track.duration_ms, {
134+
unit: 'milliseconds',
135+
format: 'MM:SS',
136+
})}
137+
</Typography>
154138
}
155139
/>
156140
)

lib/spotify.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,5 @@ export async function refreshSpotifyToken(refreshToken: string) {
6666
}
6767

6868
export const getArtistNames = (artists: SpotifyPlaylistItem['artists']) => {
69-
if (!Array.isArray(artists)) return artists ?? 'Unknown'
70-
return artists
71-
.map((a) => (typeof a === 'string' ? a : a?.name))
72-
.filter(Boolean)
73-
.join(', ')
69+
return artists ?? 'Unknown'
7470
}

tests/unit/app/api/spotify/playlists/[playlistId]/tracks/route.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ describe('GET /api/spotify/playlists/[playlistId]/tracks', () => {
5959
name: 'Album 1',
6060
},
6161
duration_ms: 180000,
62+
duration: 180000,
6263
uri: 'spotify:track:t1',
6364
})
6465
})

tests/unit/components/Playlist/PlaylistTracksDisplay.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,14 @@ describe('PlaylistTracksDisplay', () => {
122122
</WebSocketContext.Provider>
123123
)
124124

125-
const playButton = await screen.findByRole('button', { name: /play/i })
126-
fireEvent.click(playButton)
125+
const trackButton = await screen.findByText('Track 1')
126+
fireEvent.click(trackButton)
127127

128128
expect(executeMock).toHaveBeenCalledWith(
129129
'PLAY',
130130
expect.objectContaining({
131131
contextUri: 'spotify:playlist:123',
132-
offset: { position: 0 },
132+
offset: { uri: 'spotify:track:t1' },
133133
})
134134
)
135135
})
@@ -173,8 +173,8 @@ describe('PlaylistTracksDisplay', () => {
173173
</WebSocketContext.Provider>
174174
)
175175

176-
const pauseButton = await screen.findByRole('button', { name: /pause/i })
177-
fireEvent.click(pauseButton)
176+
const trackButton = await screen.findByText('Track 1')
177+
fireEvent.click(trackButton)
178178

179179
expect(executeMock).toHaveBeenCalledWith('PAUSE')
180180
})

tests/unit/components/Spotify/PlaylistDetails.test.tsx

Lines changed: 6 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,23 @@ const mockFetch = jest.fn()
99
global.fetch = mockFetch
1010

1111
describe('PlaylistDetails', () => {
12-
const mockTracksAsArray: Track[] = [
12+
const mockTracksAsString: Track[] = [
1313
{
1414
id: '1',
1515
name: 'Track 1',
1616
uri: 'spotify:track:1',
17-
artists: [{ name: 'Artist 1' }],
18-
album: { name: 'Album 1' },
19-
imageUrl: '',
17+
artists: 'Artist 1',
18+
album: { name: 'Album 1', images: [] },
2019
},
2120
{
2221
id: '2',
2322
name: 'Track 2',
2423
uri: 'spotify:track:2',
25-
artists: [{ name: 'Artist 2' }],
26-
album: { name: 'Album 2' },
27-
imageUrl: '',
24+
artists: 'Artist 2',
25+
album: { name: 'Album 2', images: [] },
2826
},
2927
]
3028

31-
const mockTracksAsString = mockTracksAsArray.map((track) => ({
32-
...track,
33-
artists: track.artists.map((a) => a.name).join(', '),
34-
})) as unknown as Track[]
35-
3629
beforeEach(() => {
3730
jest.clearAllMocks()
3831
})
@@ -45,7 +38,7 @@ describe('PlaylistDetails', () => {
4538
() =>
4639
resolve({
4740
ok: true,
48-
json: () => Promise.resolve({ tracks: mockTracksAsArray }),
41+
json: () => Promise.resolve({ tracks: mockTracksAsString }),
4942
}),
5043
100
5144
)
@@ -59,34 +52,6 @@ describe('PlaylistDetails', () => {
5952
await waitFor(() => expect(screen.queryByRole('progressbar')).toBeNull())
6053
})
6154

62-
it('displays the track list and handles play clicks when artists is an array', async () => {
63-
mockFetch.mockResolvedValue({
64-
ok: true,
65-
json: () => Promise.resolve({ tracks: mockTracksAsArray }),
66-
})
67-
const onTrackPlay = jest.fn()
68-
69-
render(
70-
<PlaylistDetails
71-
playlistId="test-playlist-id"
72-
onTrackPlay={onTrackPlay}
73-
/>
74-
)
75-
76-
await waitFor(() => {
77-
expect(screen.getByText('Track 1')).toBeInTheDocument()
78-
})
79-
expect(
80-
screen.getByText('Artist 1 • Album 1', { exact: false })
81-
).toBeInTheDocument()
82-
expect(screen.getByText('Track 2')).toBeInTheDocument()
83-
expect(
84-
screen.getByText('Artist 2 • Album 2', { exact: false })
85-
).toBeInTheDocument()
86-
87-
fireEvent.click(screen.getByText('Track 1'))
88-
expect(onTrackPlay).toHaveBeenCalledWith('spotify:track:1')
89-
})
9055

9156
it('displays the track list and handles play clicks when artists is a string', async () => {
9257
mockFetch.mockResolvedValue({

types/core.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,13 @@ export interface SpotifyPlaylistItem {
218218
id: string
219219
name: string
220220
uri: string
221-
artists?: { name: string }[] | string
221+
artists?: string
222222
album?: {
223223
name: string
224224
images: { url: string; height: number; width: number }[]
225225
}
226-
imageUrl?: string | null
227226
duration_ms?: number
227+
duration?: number
228228
}
229229

230230
/**

utils/socketManager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,8 @@ const handleIncomingMessage = (
447447
spotifyCommandParams.contextUri = commandMsg.contextUri
448448
if (commandMsg.uri) spotifyCommandParams.uri = commandMsg.uri
449449
if (commandMsg.offset) {
450-
spotifyCommandParams.offset = commandMsg.offset as any
450+
spotifyCommandParams.offset =
451+
commandMsg.offset as SpotifyCommandParameters['offset']
451452
}
452453

453454
spotifyService.handleCommand(commandMsg.command, spotifyCommandParams)

0 commit comments

Comments
 (0)