ccolas commited on
Commit
80d5795
1 Parent(s): c2d257f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +252 -143
app.py CHANGED
@@ -1,6 +1,8 @@
1
  from PIL import Image
2
  from utils import *
3
  from app_utils import *
 
 
4
 
5
  debug = False
6
  dir_path = os.path.dirname(os.path.realpath(__file__))
@@ -20,171 +22,278 @@ st.set_page_config(
20
  st.title('Customize Emotional Playlists')
21
 
22
 
23
-
24
- def setup_streamlite():
25
- setup_credentials()
26
-
27
- print('Here1')
28
- image = Image.open(dir_path + '/image.png')
29
- st.image(image)
30
- st.markdown("This app let's you quickly build playlists in a customized way: ")
31
- st.markdown("* **It's easy**: you won't have to add songs one by one,\n"
32
- "* **You're in control**: you provide a source of candidate songs, select a list of genres and choose the mood for the playlist.")
33
-
34
  st.subheader("Step 1: Connect to your Spotify app")
35
  st.markdown("Log into your Spotify account to let the app create the custom playlist.")
36
- print('Here2')
37
  if 'login' not in st.session_state:
38
  sp, user_id = new_get_client(session=st.session_state)
39
  if sp != None:
40
- print("USER", user_id)
41
  legit_genres = sp.recommendation_genre_seeds()['genres']
42
  st.session_state['login'] = (sp, user_id, legit_genres)
43
 
44
- # if 'login' not in st.session_state:
45
- # login = centered_button(st.button, 'Log in', n_columns=7)
46
- # if login or debug:
47
- # sp, user_id = get_client(session=st.session_state)
48
- # user_id = sp.me()['id']
49
- # legit_genres = sp.recommendation_genre_seeds()['genres']
50
- # st.session_state['login'] = (sp, user_id, legit_genres)
51
 
52
- if 'login' in st.session_state or debug:
53
- print('Here8')
54
- if not debug: sp, user_id, legit_genres = st.session_state['login']
55
- st.success('You are logged in.')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
- st.subheader("Step 2: Select candidate songs")
58
- st.markdown("This can be done in three ways: \n"
59
- "1. Get songs from a list of artists\n"
60
- "2. Get songs from a list of users (and their playlists)\n"
61
- "3. Get songs from a list of playlists.\n"
62
- "For this you'll need to collect the urls of artists, users and/or playlists by clicking on 'Share' and copying the urls."
63
- "You need to provide at least one source of music.")
64
-
65
- label_artist = "Add a list of artist urls, one per line (optional)"
66
- artists_links = st.text_area(label_artist, value="")
67
- users_playlists = "Add a list of users urls, one per line (optional)"
68
- users_links = st.text_area(users_playlists, value="")
69
- label_playlists = "Add a list of playlists urls, one per line (optional)"
70
- playlist_links = st.text_area(label_playlists, value="https://open.spotify.com/playlist/1H7a4q8JZArMQiidRy6qon?si=529184bbe93c4f73")
71
-
72
- button = centered_button(st.button, 'Extract music', n_columns=5)
73
- if button or debug:
74
- if playlist_links != "":
75
- playlist_uris = extract_uris_from_links(playlist_links, url_type='playlist')
 
76
  else:
77
- raise ValueError('Please enter a list of playlists')
78
- # Scanning playlists
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  st.spinner(text="Scanning music sources..")
80
- data_tracks = get_all_tracks_from_playlists(sp, playlist_uris, verbose=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  st.success(f'{len(data_tracks.keys())} tracks found!')
 
82
 
83
- # Extract audio features
84
- st.spinner(text="Extracting audio features..")
85
- all_tracks_uris = np.array(list(data_tracks.keys()))
86
- all_audio_features = [data_tracks[uri]['track']['audio_features'] for uri in all_tracks_uris]
87
- all_tracks_audio_features = dict(zip(relevant_audio_features, [[audio_f[k] for audio_f in all_audio_features] for k in relevant_audio_features]))
88
- genres = dict()
89
- for index, uri in enumerate(all_tracks_uris):
90
- track = data_tracks[uri]
91
- track_genres = track['track']['genres']
92
- for g in track_genres:
93
- if g not in genres.keys():
94
- genres[g] = [index]
95
- else:
96
- genres[g].append(index)
97
- genres = aggregate_genres(genres, legit_genres)
98
- genres_labels = sorted(genres.keys())
99
- st.success(f'Audio features extracted!')
100
- st.session_state['music_extracted'] = dict(all_tracks_uris=all_tracks_uris,
101
- all_tracks_audio_features=all_tracks_audio_features,
102
- genres=genres,
103
- genres_labels=genres_labels)
104
-
105
- if 'music_extracted' in st.session_state.keys():
106
- all_tracks_uris = st.session_state['music_extracted']['all_tracks_uris']
107
- all_tracks_audio_features = st.session_state['music_extracted']['all_tracks_audio_features']
108
- genres = st.session_state['music_extracted']['genres']
109
- genres_labels = st.session_state['music_extracted']['genres_labels']
110
-
111
- st.subheader("Step 3: Customize it!")
112
- st.markdown("##### Which genres?")
113
- st.markdown("Check boxes to select genres, see how many tracks were selected below. Note: to check all, first uncheck all (bug).")
114
- columns = st.columns(np.ones(5))
115
- with columns[1]:
116
- check_all = st.button('Check all')
117
- with columns[3]:
118
- uncheck_all = st.button('Uncheck all')
119
-
120
- if 'checkboxes' not in st.session_state.keys():
121
- st.session_state['checkboxes'] = [True] * len(genres_labels)
122
-
123
- empty_checkboxes = wall_of_checkboxes(genres_labels, max_width=5)
124
- if check_all:
125
- st.session_state['checkboxes'] = [True] * len(genres_labels)
126
- if uncheck_all:
127
- st.session_state['checkboxes'] = [False] * len(genres_labels)
128
- for i_emc, emc in enumerate(empty_checkboxes):
129
- st.session_state['checkboxes'][i_emc] = emc.checkbox(genres_labels[i_emc], value=st.session_state['checkboxes'][i_emc])
130
-
131
-
132
- # filter songs by genres
133
- selected_labels = [genres_labels[i] for i in range(len(genres_labels)) if st.session_state['checkboxes'][i]]
134
- genre_selected_indexes = []
135
- for label in selected_labels:
136
- genre_selected_indexes += genres[label]
137
- genre_selected_indexes = np.array(sorted(set(genre_selected_indexes)))
138
- if len(genre_selected_indexes) < 10:
139
- st.warning('Please select more genres or add more music sources.')
140
- else:
141
- st.success(f'{len(genre_selected_indexes)} candidate tracks selected.')
142
 
143
- st.markdown("##### What's the mood?")
144
- valence = st.slider('Valence (0 negative, 100 positive)', min_value=0, max_value=100, value=100, step=1) / 100
145
- energy = st.slider('Energy (0 low, 100 high)', min_value=0, max_value=100, value=100, step=1) / 100
146
- danceability = st.slider('Danceability (0 low, 100 high)', min_value=0, max_value=100, value=100, step=1) / 100
 
 
147
 
148
- target_mood = np.array([valence, energy, danceability]).reshape(1, 3)
149
- candidate_moods = np.array([np.array(all_tracks_audio_features[feature])[genre_selected_indexes] for feature in ['valence', 'energy', 'danceability']]).T
150
 
151
- distances = np.sqrt(((candidate_moods - target_mood) ** 2).sum(axis=1))
152
- min_dist_indexes = np.argsort(distances)
153
 
154
- n_candidates = distances.shape[0]
155
- if n_candidates < 25:
156
- st.warning('Please add more music sources or select more genres.')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  else:
158
- playlist_length = st.number_input(f'Pick a playlist length, given {n_candidates} candidates.', min_value=5,
159
- value=min(10, n_candidates//5), max_value=n_candidates//5)
160
-
161
- selected_tracks_indexes = genre_selected_indexes[min_dist_indexes[:playlist_length]]
162
- selected_tracks_uris = all_tracks_uris[selected_tracks_indexes]
163
- np.random.shuffle(selected_tracks_uris)
164
- playlist_name = st.text_input('Playlist name', value='Mood Playlist')
165
- if playlist_name == '':
166
- st.warning('Please enter a playlist name.')
 
167
  else:
168
- generation_button = centered_button(st.button, 'Generate playlist', n_columns=5)
169
- if generation_button:
170
- description = f'Emotion Playlist for Valence: {valence}, Energy: {energy}, Danceability: {danceability}). ' \
171
- f'Playlist generated by the EmotionPlaylist app: https://huggingface.co/spaces/ccolas/EmotionPlaylist.'
172
- playlist_info = sp.user_playlist_create(user_id, playlist_name, public=True, collaborative=False, description=description)
173
- playlist_uri = playlist_info['uri'].split(':')[-1]
174
- sp.playlist_add_items(playlist_uri, selected_tracks_uris)
175
- st.write(
176
- f"""
177
- <html>
178
- <body>
179
- <center>
180
- <iframe style = "border-radius:12px" src="https://open.spotify.com/embed/playlist/{playlist_uri}" allowtransparency="true"
181
- allow="encrypted-media" width="80%" height="580" frameborder="0"></iframe></center></body></html>
182
- """, unsafe_allow_html=True)
183
-
184
- st.success(f'The playlist has been generated, find it [here](https://open.spotify.com/playlist/{playlist_uri}).')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
 
187
  stop = 1
188
 
189
  if __name__ == '__main__':
190
- setup_streamlite()
 
1
  from PIL import Image
2
  from utils import *
3
  from app_utils import *
4
+ import time
5
+
6
 
7
  debug = False
8
  dir_path = os.path.dirname(os.path.realpath(__file__))
 
22
  st.title('Customize Emotional Playlists')
23
 
24
 
25
+ def log_to_spotify():
 
 
 
 
 
 
 
 
 
 
26
  st.subheader("Step 1: Connect to your Spotify app")
27
  st.markdown("Log into your Spotify account to let the app create the custom playlist.")
 
28
  if 'login' not in st.session_state:
29
  sp, user_id = new_get_client(session=st.session_state)
30
  if sp != None:
 
31
  legit_genres = sp.recommendation_genre_seeds()['genres']
32
  st.session_state['login'] = (sp, user_id, legit_genres)
33
 
 
 
 
 
 
 
 
34
 
35
+ @st.cache
36
+ def get_user_playlists(users_links):
37
+ global sp
38
+ # Scanning users
39
+ n_playlists = 0
40
+ all_uris, all_names = [], []
41
+ if users_links != "":
42
+ try:
43
+ all_uris, all_names = get_all_playlists_uris_from_users(users_links.split('\n'))
44
+ n_playlists = len(all_uris)
45
+ except:
46
+ st.warning('Please enter a valid list of user names (one url per line)')
47
+ return all_uris, all_names, n_playlists
48
+
49
+ def get_filtered_user_playlists(user_links):
50
+ global sp
51
+ st.spinner(text="Scanning users..")
52
+ all_uris, all_names, n_playlists = get_user_playlists(user_links)
53
+ if n_playlists <= 1:
54
+ st.success(f'Currently {len(all_uris)} user playlists taken into account.')
55
+ return all_uris
56
+ else:
57
+ # let the user uncheck playlists
58
+ st.markdown("Check boxes to select playlists from the selected users."
59
+ "Note: to check all, first uncheck all (bug).")
60
+ columns = st.columns(np.ones(5))
61
+ with columns[1]:
62
+ check_all_playlists = st.button('Check all')
63
+ with columns[3]:
64
+ uncheck_all_playlists = st.button('Uncheck all')
65
+
66
+ if 'checkboxes' not in st.session_state.keys():
67
+ st.session_state['checkboxes_playlists'] = [True] * n_playlists
68
+
69
+ empty_checkboxes = wall_of_checkboxes(all_names, max_width=5)
70
+ if check_all_playlists:
71
+ st.session_state['checkboxes_playlists'] = [True] * n_playlists
72
+ if uncheck_all_playlists:
73
+ st.session_state['checkboxes_playlists'] = [False] * n_playlists
74
+ for i_emc, emc in enumerate(empty_checkboxes):
75
+ st.session_state['checkboxes_playlists'][i_emc] = emc.checkbox(all_names[i_emc], value=st.session_state['checkboxes_playlists'][i_emc])
76
+
77
+ filter_playlist = st.button('Update user playlists')
78
+ if filter_playlist:
79
+ return list(np.array(all_uris)[np.where(st.session_state['checkboxes_playlists'])])
80
+ else:
81
+ return []
82
+
83
+ @st.cache
84
+ def get_non_user_playlists(playlist_links):
85
+ # Scanning playlists
86
+ new_playlist_uris = []
87
+ if playlist_links != "":
88
+ st.spinner(text="Scanning playlists..")
89
+ try:
90
+ new_playlist_uris = extract_uris_from_links(playlist_links, url_type='playlist')
91
+ except:
92
+ st.warning('Please enter a valid list of playlists (one url per line)')
93
+ return new_playlist_uris
94
 
95
+ @st.cache
96
+ def extract_tracks(playlist_uris):
97
+ global sp
98
+ # extracting tracks
99
+ data_tracks = get_all_tracks_from_playlists(sp, playlist_uris, verbose=True)
100
+ return data_tracks
101
+
102
+ @st.cache
103
+ def extract_audio_features(data_tracks, legit_genres):
104
+ # Extract audio features
105
+ all_tracks_uris = np.array(list(data_tracks.keys()))
106
+ all_audio_features = [data_tracks[uri]['track']['audio_features'] for uri in all_tracks_uris]
107
+ all_tracks_audio_features = dict(zip(relevant_audio_features, [[audio_f[k] for audio_f in all_audio_features] for k in relevant_audio_features]))
108
+ genres = dict()
109
+ for index, uri in enumerate(all_tracks_uris):
110
+ track = data_tracks[uri]
111
+ track_genres = track['track']['genres']
112
+ for g in track_genres:
113
+ if g not in genres.keys():
114
+ genres[g] = [index]
115
  else:
116
+ genres[g].append(index)
117
+ genres = aggregate_genres(genres, legit_genres)
118
+ genres_labels = sorted(genres.keys())
119
+ return all_tracks_uris, all_tracks_audio_features, genres, genres_labels
120
+ # st.session_state['music_extracted'] = dict(all_tracks_uris=all_tracks_uris,
121
+ # all_tracks_audio_features=all_tracks_audio_features,
122
+ # genres=genres,
123
+ # genres_labels=genres_labels)
124
+
125
+ def select_songs(legit_genres):
126
+ global sp
127
+ st.subheader("Step 2: Select candidate songs")
128
+ st.markdown("This can be done in two ways: \n"
129
+ "1. Get songs from a list of users (and their playlists)\n"
130
+ "2. Get songs from a list of playlists.\n"
131
+ "For this you'll need to collect user names (e.g. 'bkayf') and/or playlist urls (e.g. "
132
+ "https://open.spotify.com/playlist/1H7a4q8JZArMQiidRy6qon) by clicking on 'Share' and copying the url. "
133
+ "You need to provide at least one source of music.")
134
+
135
+ users_playlists = "Add a list of user names, one per line (optional)"
136
+ users_links = st.text_area(users_playlists, value="")
137
+ label_playlists = "Add a list of playlists urls, one per line (optional)"
138
+ playlist_links = st.text_area(label_playlists, value="https://open.spotify.com/playlist/1H7a4q8JZArMQiidRy6qon")
139
+ extract_button = centered_button(st.button, 'Extract music', n_columns=5)
140
+
141
+ all_tracks_uris, all_tracks_audio_features, genres, genres_labels = [None] * 4
142
+ if extract_button or debug or 'extract_button' in st.session_state.keys():
143
+ st.session_state['extract_button'] = True
144
+ # check the user input music sources
145
+ if playlist_links == "" and users_links == "":
146
+ st.warning('Please enter at least one source of music.')
147
+ else:
148
  st.spinner(text="Scanning music sources..")
149
+ playlist_uris = []
150
+ init_time = time.time()
151
+ init_time_tot = init_time
152
+ user_playlists = get_filtered_user_playlists(users_links)
153
+ playlist_uris += user_playlists
154
+ print(f'1. user playlist: {time.time() - init_time:.2f}')
155
+ init_time = time.time()
156
+ new_playlist_uris = get_non_user_playlists(playlist_links)
157
+ playlist_uris += new_playlist_uris
158
+ st.success(f'{len(new_playlist_uris)} new playlists added from {len(user_playlists)} users.')
159
+ print(f'2. non user playlist: {time.time() - init_time:.2f}')
160
+ init_time = time.time()
161
+ if str(playlist_uris) in st.session_state.keys():
162
+ data_tracks = st.session_state[str(playlist_uris)]
163
+ else:
164
+ data_tracks = extract_tracks(playlist_uris)
165
+ st.session_state[str(playlist_uris)] = data_tracks
166
+ print(f'3. track extraction: {time.time() - init_time:.2f}')
167
+ init_time = time.time()
168
+ all_tracks_uris, all_tracks_audio_features, genres, genres_labels = extract_audio_features(data_tracks, legit_genres)
169
+ print(f'4. audio feature extraction: {time.time() - init_time:.2f}')
170
+ print(f'\t total extraction: {time.time() - init_time_tot:.2f}')
171
  st.success(f'{len(data_tracks.keys())} tracks found!')
172
+ return all_tracks_uris, all_tracks_audio_features, genres, genres_labels
173
 
174
+ def customize_widgets(genres_labels):
175
+ st.subheader("Step 3: Customize it!")
176
+ st.markdown("##### Which genres?")
177
+ st.markdown("Check boxes to select genres, see how many tracks were selected below. Note: to check all, first uncheck all (bug).")
178
+ columns = st.columns(np.ones(5))
179
+ with columns[1]:
180
+ check_all = st.button('Check all')
181
+ with columns[3]:
182
+ uncheck_all = st.button('Uncheck all')
183
+
184
+ if 'checkboxes' not in st.session_state.keys():
185
+ st.session_state['checkboxes'] = [True] * len(genres_labels)
186
+
187
+ empty_checkboxes = wall_of_checkboxes(genres_labels, max_width=5)
188
+ if check_all:
189
+ st.session_state['checkboxes'] = [True] * len(genres_labels)
190
+ if uncheck_all:
191
+ st.session_state['checkboxes'] = [False] * len(genres_labels)
192
+ for i_emc, emc in enumerate(empty_checkboxes):
193
+ st.session_state['checkboxes'][i_emc] = emc.checkbox(genres_labels[i_emc], value=st.session_state['checkboxes'][i_emc])
194
+
195
+ st.markdown("##### What's the mood?")
196
+ valence = st.slider('Valence (0 negative, 100 positive)', min_value=0, max_value=100, value=100, step=1) / 100
197
+ energy = st.slider('Energy (0 low, 100 high)', min_value=0, max_value=100, value=100, step=1) / 100
198
+ danceability = st.slider('Danceability (0 low, 100 high)', min_value=0, max_value=100, value=100, step=1) / 100
199
+ target_mood = np.array([valence, energy, danceability]).reshape(1, 3)
200
+ return target_mood
201
+
202
+ @st.cache
203
+ def filter_songs_by_genre(checkboxes, genres_labels, genres):
204
+ # filter songs by genres
205
+ selected_labels = [genres_labels[i] for i in range(len(genres_labels)) if checkboxes[i]]
206
+ genre_selected_indexes = []
207
+ for label in selected_labels:
208
+ genre_selected_indexes += genres[label]
209
+ genre_selected_indexes = np.array(sorted(set(genre_selected_indexes)))
210
+ return genre_selected_indexes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
+ def find_best_songs_for_mood(all_tracks_audio_features, genre_selected_indexes, target_mood):
213
+ candidate_moods = np.array([np.array(all_tracks_audio_features[feature])[genre_selected_indexes] for feature in ['valence', 'energy', 'danceability']]).T
214
+ distances = np.sqrt(((candidate_moods - target_mood) ** 2).sum(axis=1))
215
+ min_dist_indexes = np.argsort(distances)
216
+ n_candidates = distances.shape[0]
217
+ return min_dist_indexes, n_candidates
218
 
 
 
219
 
220
+ def run_app():
221
+ setup_credentials()
222
 
223
+ image = Image.open(dir_path + '/image.png')
224
+ st.image(image)
225
+ st.markdown("This app let's you quickly build playlists in a customized way: ")
226
+ st.markdown("* **It's easy**: you won't have to add songs one by one,\n"
227
+ "* **You're in control**: you provide a source of candidate songs, select a list of genres and choose the mood for the playlist.")
228
+ fake = centered_button(st.button, "Let's go", n_columns=7, disabled=True)
229
+
230
+ log_to_spotify()
231
+
232
+ if 'login' in st.session_state or debug:
233
+ global sp
234
+ if not debug: sp, user_id, legit_genres = st.session_state['login']
235
+ st.success('You are logged in.')
236
+
237
+ all_tracks_uris, all_tracks_audio_features, genres, genres_labels = select_songs(legit_genres)
238
+ if all_tracks_uris is not None:
239
+ target_mood = customize_widgets(genres_labels)
240
+
241
+ custom_button = centered_button(st.button, 'Run customization', n_columns=5)
242
+ if custom_button or 'run_custom' in st.session_state.keys():
243
+ st.session_state['run_custom'] = True
244
+ checkboxes = st.session_state['checkboxes'].copy()
245
+ init_time = time.time()
246
+ genre_selected_indexes = filter_songs_by_genre(checkboxes, genres_labels, genres)
247
+ if len(genre_selected_indexes) < 10:
248
+ genre_selected_indexes = None
249
+ st.warning('Please select more genres or add more music sources.')
250
  else:
251
+ st.success(f'{len(genre_selected_indexes)} candidate tracks selected.')
252
+ print(f'6. filter by genre: {time.time() - init_time:.2f}')
253
+ init_time = time.time()
254
+ if genre_selected_indexes is not None:
255
+ min_dist_indexes, n_candidates = find_best_songs_for_mood(all_tracks_audio_features, genre_selected_indexes, target_mood)
256
+
257
+ print(f'7. filter by mood: {time.time() - init_time:.2f}')
258
+ init_time = time.time()
259
+ if n_candidates < 25:
260
+ st.warning('Please add more music sources or select more genres.')
261
  else:
262
+ playlist_length = st.number_input(f'Pick a playlist length, given {n_candidates} candidates.', min_value=5,
263
+ value=min(10, n_candidates//5), max_value=n_candidates//5)
264
+
265
+ selected_tracks_indexes = genre_selected_indexes[min_dist_indexes[:playlist_length]]
266
+ selected_tracks_uris = all_tracks_uris[selected_tracks_indexes]
267
+ np.random.shuffle(selected_tracks_uris)
268
+
269
+ playlist_name = st.text_input('Playlist name', value='Mood Playlist')
270
+ if playlist_name == '':
271
+ st.warning('Please enter a playlist name.')
272
+ else:
273
+ print(f'8. build playlist: {time.time() - init_time:.2f}')
274
+ init_time = time.time()
275
+ generation_button = centered_button(st.button, 'Generate playlist', n_columns=5)
276
+ if generation_button:
277
+ description = f'Emotion Playlist for Valence: {target_mood[0]}, ' \
278
+ f'Energy: {target_mood[1]}, ' \
279
+ f'Danceability: {target_mood[2]}). ' \
280
+ f'Playlist generated by the EmotionPlaylist app: https://huggingface.co/spaces/ccolas/EmotionPlaylist.'
281
+ playlist_info = sp.user_playlist_create(user_id, playlist_name, public=True, collaborative=False, description=description)
282
+ playlist_uri = playlist_info['uri'].split(':')[-1]
283
+ sp.playlist_add_items(playlist_uri, selected_tracks_uris)
284
+ st.write(
285
+ f"""
286
+ <html>
287
+ <body>
288
+ <center>
289
+ <iframe style = "border-radius:12px" src="https://open.spotify.com/embed/playlist/{playlist_uri}" allowtransparency="true"
290
+ allow="encrypted-media" width="80%" height="580" frameborder="0"></iframe></center></body></html>
291
+ """, unsafe_allow_html=True)
292
+
293
+ st.success(f'The playlist has been generated, find it [here](https://open.spotify.com/playlist/{playlist_uri}).')
294
 
295
 
296
  stop = 1
297
 
298
  if __name__ == '__main__':
299
+ run_app()