Skip to content

Commit 75778ff

Browse files
committed
Improve playlist explorer and add button/endpoint for updating playlist details in db
1 parent 30d9bbb commit 75778ff

File tree

8 files changed

+169
-91
lines changed

8 files changed

+169
-91
lines changed

backend/src/controllers/database.py

+26
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from src.database.crud.playlist import (
1313
create_playlist,
1414
delete_playlist,
15+
get_playlist_albums,
1516
get_playlist_by_id_or_none,
1617
)
1718
from src.database.crud.user import get_or_create_user
@@ -54,6 +55,31 @@ def populate_user():
5455

5556
return make_response("Playlist data populated", 201)
5657

58+
@database_controller.route("populate_playlist/<id>", methods=["GET"])
59+
def populate_playlist(id):
60+
access_token = request.cookies.get("spotify_access_token")
61+
user = spotify.get_current_user(access_token)
62+
(db_user, _) = get_or_create_user(user)
63+
db_playlist = get_playlist_by_id_or_none(id)
64+
if db_playlist is not None:
65+
delete_playlist(db_playlist.id)
66+
playlist = spotify.get_playlist(access_token=access_token, id=id)
67+
create_playlist(playlist, db_user)
68+
albums = get_playlist_albums(playlist.id)
69+
batch_albums = split_list(albums, 20)
70+
for album_chunk in batch_albums:
71+
albums = spotify.get_multiple_albums(
72+
access_token=access_token, ids=[album.id for album in album_chunk]
73+
)
74+
for db_album in albums:
75+
with database.database.atomic():
76+
db_album.genres = musicbrainz.get_album_genres(
77+
db_album.artists[0].name, db_album.name
78+
)
79+
update_album(db_album)
80+
81+
return make_response("Playlist details populated", 201)
82+
5783
@database_controller.route("populate_additional_album_details", methods=["GET"])
5884
def populate_additional_album_details():
5985
access_token = request.cookies.get("spotify_access_token")

backend/src/controllers/music_data.py

+16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from flask import Blueprint, jsonify, make_response, request
22
from src.database.crud.playlist import (
3+
create_playlist_album_relationship,
34
get_playlist_albums_with_genres,
45
get_playlist_by_id_or_none,
56
get_playlist_duration,
@@ -112,6 +113,21 @@ def find_associated_playlists():
112113
search = request.json
113114
return search_playlist_names(user_id, search)
114115

116+
@music_controller.route("add_album_to_playlist", methods=["POST"])
117+
def add_album_to_playlist():
118+
access_token = request.cookies.get("spotify_access_token")
119+
request_body = request.json
120+
playlist_id = request_body["playlistId"]
121+
album_id = request_body["albumId"]
122+
if not playlist_id or not album_id:
123+
return make_response(
124+
"Invalid request payload. Expected playlistId and albumId.", 400
125+
)
126+
create_playlist_album_relationship(playlist_id=playlist_id, album_id=album_id)
127+
return spotify.add_album_to_playlist(
128+
access_token=access_token, playlist_id=playlist_id, album_id=album_id
129+
)
130+
115131
@music_controller.route("playback", methods=["GET"])
116132
def get_playback_info():
117133
access_token = request.cookies.get("spotify_access_token")

backend/src/database/crud/playlist.py

+12
Original file line numberDiff line numberDiff line change
@@ -326,3 +326,15 @@ def search_playlist_names(user_id: str, search: str) -> List[dict]:
326326
]
327327

328328
return result
329+
330+
331+
def create_playlist_album_relationship(playlist_id: str, album_id: str):
332+
max_album_index = (
333+
PlaylistAlbumRelationship.select(fn.MAX(PlaylistAlbumRelationship.album_index))
334+
.where(PlaylistAlbumRelationship.playlist == playlist_id)
335+
.scalar()
336+
)
337+
result = PlaylistAlbumRelationship.create(
338+
playlist=playlist_id, album=album_id, album_index=max_album_index + 1
339+
)
340+
return

backend/src/spotify.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -466,8 +466,8 @@ def add_album_to_playlist(self, access_token, playlist_id, album_id) -> Response
466466
auth=BearerAuth(access_token),
467467
)
468468
self.response_handler(response, jsonify=False)
469-
if response.status_code == 200:
470-
return make_response("Album successfully added to playlist", 200)
469+
if response.status_code == 201:
470+
return make_response("Album successfully added to playlist", 201)
471471
else:
472472
return make_response("Failed to add album to playlist", 400)
473473

frontend/src/MainPage.tsx

-11
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,12 @@ export const Index: FC = () => {
3131
},
3232
});
3333

34-
const allQuery = useQuery<Playlist[]>({
35-
queryKey: ["playlists", pagination, search],
36-
queryFn: () => {
37-
return getPlaylists(search, pagination.pageIndex, pagination.pageSize);
38-
},
39-
});
40-
4134
return (
4235
<div className="py-4 px-2 space-y-2">
4336
<Box className="space-y-2">
4437
<SearchBar search={searchRecent} setSearch={setSearchRecent}/>
4538
<Carousel slides={(recentQuery.data ?? createUndefinedArray(pagination.pageSize)).map(PlaylistSlide)} />
4639
</Box>
47-
<Box className="space-y-2">
48-
<SearchBar search={search} setSearch={setSearch}/>
49-
<Carousel slides={(allQuery.data ?? createUndefinedArray(pagination.pageSize)).map(PlaylistSlide)} />
50-
</Box>
5140
<Box>
5241
<AddPlaylistForm />
5342
</Box>

frontend/src/api/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ export const populateAdditionalAlbumDetails = async (): Promise<Response> => {
162162
return request('database/populate_additional_album_details', RequestMethod.GET)
163163
}
164164

165+
export const populatePlaylist = async (id: string): Promise<Response> => {
166+
return request(`database/populate_playlist/${id}`, RequestMethod.GET)
167+
}
168+
165169
export const populateUniversalGenreList = async (): Promise<Response> => {
166170
return request('database/populate_universal_genre_list', RequestMethod.GET)
167171
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React, { FC, useState, MouseEvent } from "react";
2+
3+
const ButtonAsync: FC<
4+
React.DetailedHTMLProps<
5+
React.ButtonHTMLAttributes<HTMLButtonElement>,
6+
HTMLButtonElement
7+
>
8+
> = ({className, onClick, ...props}) => {
9+
const [isLoading, setIsLoading] = useState(false);
10+
11+
const handleClick = async (event: MouseEvent<HTMLButtonElement>): Promise<void> => {
12+
if (onClick) {
13+
setIsLoading(true)
14+
await onClick(event)
15+
setIsLoading(false);
16+
}
17+
}
18+
return (
19+
<button
20+
{...props}
21+
disabled={isLoading}
22+
onClick={handleClick}
23+
className={`bg-primary rounded p-2 cursor-pointer hover:bg-primary-lighter active:bg-primary-lighter disabled:bg-background-offset ${className}`}
24+
/>
25+
);
26+
};
27+
28+
export default ButtonAsync;

frontend/src/playlistExplorer/PlaylistExplorer.tsx

+81-78
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
getPlaylistAlbums,
88
getPlaylistTracks,
99
playlistSearch,
10+
populatePlaylist,
1011
updatePlaylist,
1112
} from "../api";
1213
import { useQuery } from "@tanstack/react-query";
@@ -16,6 +17,7 @@ import { TrackList } from "./TrackList/TrackList";
1617
import { usePlaybackContext } from "../hooks/usePlaybackContext";
1718
import { Track } from "../interfaces/Track";
1819
import InputWithLabelPlaceholder from "../components/Inputs/InputWithLabelPlaceholder";
20+
import ButtonAsync from "../components/ButtonAsync";
1921

2022
enum ViewMode {
2123
ALBUM = "album",
@@ -63,86 +65,87 @@ export const PlaylistExplorer: FC = () => {
6365
const { playbackInfo } = usePlaybackContext();
6466

6567
return (
66-
<div className="flex flex-col h-full space-y-1 ">
67-
<div className="m-2 text-sm sm:text-base">
68-
<Form
69-
onSubmit={() => {
70-
updatePlaylist(getValues());
71-
}}
72-
control={control}
73-
>
74-
<div className="flex flex-col my-4 space-y-2">
75-
<InputWithLabelPlaceholder
76-
register={register("name")}
77-
type="text"
78-
name="name"
79-
placeholder={"Title"}
80-
defaultValue={playlist.name}
81-
/>
82-
<InputWithLabelPlaceholder
83-
register={register("description")}
84-
type="text"
85-
name="description"
86-
placeholder={"Description"}
87-
defaultValue={playlist.description}
88-
/>
68+
<div className="m-2 text-sm sm:text-base space-y-4">
69+
<Form
70+
onSubmit={() => {
71+
updatePlaylist(getValues());
72+
}}
73+
control={control}
74+
>
75+
<div className="flex flex-col my-4 space-y-2">
76+
<InputWithLabelPlaceholder
77+
register={register("name")}
78+
type="text"
79+
name="name"
80+
placeholder={"Title"}
81+
defaultValue={playlist.name}
82+
/>
83+
<InputWithLabelPlaceholder
84+
register={register("description")}
85+
type="text"
86+
name="description"
87+
placeholder={"Description"}
88+
defaultValue={playlist.description}
89+
/>
90+
</div>
91+
<div className="flex flex-row space-x-4 justify-end sm:justify-start mx-2">
92+
<Button className="flex" type="submit">
93+
Update details
94+
</Button>
95+
<div className="flex my-auto">
96+
<Link to={`/`}>Back</Link>
8997
</div>
90-
<div className="flex flex-row space-x-4 justify-end sm:justify-start mx-2">
91-
<Button className="flex" type="submit">
92-
Submit
93-
</Button>
94-
<div className="flex my-auto">
95-
<Link to={`/`}>Back</Link>
96-
</div>
97-
</div>
98-
</Form>
99-
<>
100-
<div className=" mt-2">
101-
<button
102-
className="border-solid rounded-md border border-primary-500 w-full flex justify-between overflow-hidden"
103-
disabled={!playlistAlbums}
104-
onClick={() => {
105-
if (viewMode === ViewMode.ALBUM) {
106-
setViewMode(ViewMode.TRACK);
107-
} else {
108-
setViewMode(ViewMode.ALBUM);
109-
}
110-
}}
98+
</div>
99+
</Form>
100+
<ButtonAsync className="flex" onClick={() => populatePlaylist(playlist.id)}>
101+
Sync new playlist data
102+
</ButtonAsync>
103+
<>
104+
<div className=" mt-2">
105+
<button
106+
className="border-solid rounded-md border border-primary-500 w-full flex justify-between overflow-hidden"
107+
disabled={!playlistAlbums}
108+
onClick={() => {
109+
if (viewMode === ViewMode.ALBUM) {
110+
setViewMode(ViewMode.TRACK);
111+
} else {
112+
setViewMode(ViewMode.ALBUM);
113+
}
114+
}}
115+
>
116+
<h2
117+
className={`p-2 flex grow text-right ${
118+
viewMode === ViewMode.ALBUM ? "bg-primary-darker" : ""
119+
} ${!playlistAlbums ? "opacity-50 disabled" : ""}`}
111120
>
112-
<h2
113-
className={`p-2 flex grow text-right ${
114-
viewMode === ViewMode.ALBUM ? "bg-primary-darker" : ""
115-
} ${!playlistAlbums ? "opacity-50 disabled" : ""}`}
116-
>
117-
Album View
118-
</h2>
119-
<h2
120-
className={`p-2 flex grow ${
121-
viewMode === ViewMode.TRACK ? "bg-primary-darker" : ""
122-
}`}
123-
>
124-
Track View
125-
</h2>
126-
</button>
127-
</div>
128-
<div className="my-2">
129-
{viewMode == ViewMode.ALBUM && playlistAlbums && (
130-
<AlbumList
131-
albumList={playlistAlbums}
132-
activeAlbumId={playbackInfo?.album_id}
133-
contextPlaylist={playlist}
134-
associatedPlaylists={associatedPlaylists}
135-
/>
136-
)}
137-
{viewMode == ViewMode.TRACK && playlistTracks &&(
138-
<TrackList
139-
trackList={playlistTracks}
140-
activeTrackId={playbackInfo?.track_id}
141-
/>
142-
)}
143-
</div>
144-
</>
145-
</div>
121+
Album View
122+
</h2>
123+
<h2
124+
className={`p-2 flex grow ${
125+
viewMode === ViewMode.TRACK ? "bg-primary-darker" : ""
126+
}`}
127+
>
128+
Track View
129+
</h2>
130+
</button>
131+
</div>
132+
<div className="my-2">
133+
{viewMode == ViewMode.ALBUM && playlistAlbums && (
134+
<AlbumList
135+
albumList={playlistAlbums}
136+
activeAlbumId={playbackInfo?.album_id}
137+
contextPlaylist={playlist}
138+
associatedPlaylists={associatedPlaylists}
139+
/>
140+
)}
141+
{viewMode == ViewMode.TRACK && playlistTracks &&(
142+
<TrackList
143+
trackList={playlistTracks}
144+
activeTrackId={playbackInfo?.track_id}
145+
/>
146+
)}
147+
</div>
148+
</>
146149
</div>
147150
);
148151
};

0 commit comments

Comments
 (0)