File size: 12,645 Bytes
1767f84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
import streamlit as st
from PIL import Image
import spotipy.util as util
import pickle
import spotipy
from utils import *

st.set_page_config(
    page_title="EmotionPlaylist",
    page_icon="🎧",
)
debug = False
dir_path = os.path.dirname(os.path.realpath(__file__))

st.title('Customize Emotional Playlists')

def centered_button(func, text, n_columns=7, args=None):
    columns = st.columns(np.ones(n_columns))
    with columns[n_columns//2]:
        return func(text)

# get credentials
def setup_credentials():
    if 'client_id' in os.environ.keys() and 'client_secret' in os.environ.keys():
        client_info = dict(client_id=os.environ['client_id'],
                           client_secret=os.environ['client_secret'])
    else:
        with open(dir_path + "/ids.pk", 'rb') as f:
            client_info = pickle.load(f)

    os.environ['SPOTIPY_CLIENT_ID'] = client_info['client_id']
    os.environ['SPOTIPY_CLIENT_SECRET'] = client_info['client_secret']
    os.environ['SPOTIPY_REDIRECT_URI'] = 'http://localhost:8080/'

relevant_audio_features = ["danceability", "energy", "loudness", "mode", "valence", "tempo"]


def get_client():
    scope = "playlist-modify-public"
    token = util.prompt_for_user_token(scope=scope)
    sp = spotipy.Spotify(auth=token)
    user_id = sp.me()['id']
    return sp, user_id

def extract_uris_from_links(links, url_type):
    assert url_type in ['playlist', 'artist', 'user']
    urls = links.split('\n')
    uris = []
    for url in urls:
        if 'playlist' in url:
            uri = url.split(f'{url_type}/')[-1].split('?')[0]
        else:
            uri = url.split('?')[0]
        uris.append(uri)
    return uris

def wall_of_checkboxes(labels, max_width=10):
    n_labels = len(labels)
    n_rows = int(np.ceil(n_labels/max_width))
    checkboxes = []
    for i in range(n_rows):
        columns = st.columns(np.ones(max_width))
        row_length = n_labels % max_width if i == n_rows - 1 else max_width
        for j in range(row_length):
            with columns[j]:
                checkboxes.append(st.empty())
    return checkboxes

def aggregate_genres(genres, legit_genres, verbose=False):
    genres_output = dict()
    legit_genres_formatted = [lg.replace('-', '').replace(' ', '') for lg in legit_genres]
    for glabel in genres.keys():
        if verbose: print('\n', glabel)
        glabel_formatted = glabel.replace(' ', '').replace('-', '')
        best_match = None
        best_match_score = 0
        for legit_glabel, legit_glabel_formatted in zip(legit_genres, legit_genres_formatted):
            if 'jazz' in glabel_formatted:
                best_match = 'jazz'
                if verbose: print('\t', 'pop')
                break
            if 'ukpop' in glabel_formatted:
                best_match = 'pop'
                if verbose: print('\t', 'pop')
                break
            if legit_glabel_formatted == glabel_formatted:
                if verbose: print('\t', legit_glabel_formatted)
                best_match = legit_glabel
                break
            elif glabel_formatted in legit_glabel_formatted:
                if verbose: print('\t', legit_glabel_formatted)
                if len(glabel_formatted) > best_match_score:
                    best_match = legit_glabel
                    best_match_score = len(glabel_formatted)
            elif legit_glabel_formatted in glabel_formatted:
                if verbose: print('\t', legit_glabel_formatted)
                if len(legit_glabel_formatted) > best_match_score:
                    best_match = legit_glabel
                    best_match_score = len(legit_glabel_formatted)

        if best_match is not None:
            if verbose: print('\t', '-->', best_match)
            if best_match in genres_output.keys():
                genres_output[best_match] += genres[glabel]
            else:
                genres_output[best_match] = genres[glabel]
    return genres_output

def setup_streamlite():
    setup_credentials()
    image = Image.open(dir_path + '/image.png')
    st.image(image)

    st.markdown("This app let's you quickly build playlists in a customized way: ")
    st.markdown("* **It's easy**: you won't have to add songs one by one,\n"
                "* **You're in control**: you provide a source of candidate songs, select a list of genres and choose the mood for the playlist.")

    st.subheader("Step 1: Connect to your Spotify app")
    st.markdown("Log into your Spotify account to let the app create the custom playlist.")
    if 'login' not in st.session_state:
        login = centered_button(st.button, 'Log in', n_columns=7)
        if login or debug:
            sp, user_id = get_client()
            legit_genres = sp.recommendation_genre_seeds()['genres']
            st.session_state['login'] = (sp, user_id, legit_genres)

    if 'login' in st.session_state or debug:
        if not debug: sp, user_id, legit_genres = st.session_state['login']
        st.success('You are logged in.')

        st.subheader("Step 2: Select candidate songs")
        st.markdown("This can be done in three ways: \n"
                    "1. Get songs from a list of artists\n"
                    "2. Get songs from a list of users (and their playlists)\n"
                    "3. Get songs from a list of playlists.\n"
                    "For this you'll need to collect the urls of artists, users and/or playlists by clicking on 'Share' and copying the urls."
                    "You need to provide at least one source of music.")

        label_artist = "Add a list of artist urls, one per line (optional)"
        artists_links = st.text_area(label_artist, value="")
        users_playlists = "Add a list of users urls, one per line (optional)"
        users_links = st.text_area(users_playlists, value="")
        label_playlists = "Add a list of playlists urls, one per line (optional)"
        playlist_links = st.text_area(label_playlists, value="https://open.spotify.com/playlist/1H7a4q8JZArMQiidRy6qon?si=529184bbe93c4f73")

        button = centered_button(st.button, 'Extract music', n_columns=5)
        if button or debug:
            if playlist_links != "":
                playlist_uris = extract_uris_from_links(playlist_links, url_type='playlist')
            else:
                raise ValueError('Please enter a list of playlists')
            # Scanning playlists
            st.spinner(text="Scanning music sources..")
            data_tracks = get_all_tracks_from_playlists(sp, playlist_uris, verbose=True)
            st.success(f'{len(data_tracks.keys())} tracks found!')

            # Extract audio features
            st.spinner(text="Extracting audio features..")
            all_tracks_uris = np.array(list(data_tracks.keys()))
            all_audio_features = [data_tracks[uri]['track']['audio_features'] for uri in all_tracks_uris]
            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]))
            genres = dict()
            for index, uri in enumerate(all_tracks_uris):
                track = data_tracks[uri]
                track_genres = track['track']['genres']
                for g in track_genres:
                    if g not in genres.keys():
                        genres[g] = [index]
                    else:
                        genres[g].append(index)
            genres = aggregate_genres(genres, legit_genres)
            genres_labels = sorted(genres.keys())
            st.success(f'Audio features extracted!')
            st.session_state['music_extracted'] = dict(all_tracks_uris=all_tracks_uris,
                                                       all_tracks_audio_features=all_tracks_audio_features,
                                                       genres=genres,
                                                       genres_labels=genres_labels)

        if 'music_extracted' in st.session_state.keys():
            all_tracks_uris = st.session_state['music_extracted']['all_tracks_uris']
            all_tracks_audio_features = st.session_state['music_extracted']['all_tracks_audio_features']
            genres = st.session_state['music_extracted']['genres']
            genres_labels = st.session_state['music_extracted']['genres_labels']

            st.subheader("Step 3: Customize it!")
            st.markdown("##### Which genres?")
            st.markdown("Check boxes to select genres, see how many tracks were selected below. Note: to check all, first uncheck all (bug).")
            columns = st.columns(np.ones(5))
            with columns[1]:
                check_all = st.button('Check all')
            with columns[3]:
                uncheck_all = st.button('Uncheck all')

            if 'checkboxes' not in st.session_state.keys():
                st.session_state['checkboxes'] = [True] * len(genres_labels)

            empty_checkboxes = wall_of_checkboxes(genres_labels, max_width=5)
            if check_all:
                st.session_state['checkboxes'] = [True] * len(genres_labels)
            if uncheck_all:
                st.session_state['checkboxes'] = [False] * len(genres_labels)
            for i_emc, emc in enumerate(empty_checkboxes):
                st.session_state['checkboxes'][i_emc] = emc.checkbox(genres_labels[i_emc], value=st.session_state['checkboxes'][i_emc])


            # filter songs by genres
            selected_labels = [genres_labels[i] for i in range(len(genres_labels)) if st.session_state['checkboxes'][i]]
            genre_selected_indexes = []
            for label in selected_labels:
                genre_selected_indexes += genres[label]
            genre_selected_indexes = np.array(sorted(set(genre_selected_indexes)))
            if len(genre_selected_indexes) < 10:
                st.warning('Please select more genres or add more music sources.')
            else:
                st.success(f'{len(genre_selected_indexes)} candidate tracks selected.')

                st.markdown("##### What's the mood?")
                valence = st.slider('Valence (0 negative, 100 positive)', min_value=0, max_value=100, value=100, step=1) / 100
                energy = st.slider('Energy (0 low, 100 high)', min_value=0, max_value=100, value=100, step=1) / 100
                danceability = st.slider('Danceability (0 low, 100 high)', min_value=0, max_value=100, value=100, step=1) / 100

                target_mood = np.array([valence, energy, danceability]).reshape(1, 3)
                candidate_moods = np.array([np.array(all_tracks_audio_features[feature])[genre_selected_indexes] for feature in ['valence', 'energy', 'danceability']]).T

                distances = np.sqrt(((candidate_moods - target_mood) ** 2).sum(axis=1))
                min_dist_indexes = np.argsort(distances)

                n_candidates = distances.shape[0]
                if n_candidates < 25:
                    st.warning('Please add more music sources or select more genres.')
                else:
                    playlist_length = st.number_input(f'Pick a playlist length, given {n_candidates} candidates.', min_value=5,
                                                      value=min(10, n_candidates//5), max_value=n_candidates//5)

                    selected_tracks_indexes = genre_selected_indexes[min_dist_indexes[:playlist_length]]
                    selected_tracks_uris = all_tracks_uris[selected_tracks_indexes]

                    playlist_name = st.text_input('Playlist name', value='Mood Playlist')
                    if playlist_name == '':
                        st.warning('Please enter a playlist name.')
                    else:
                        generation_button = st.button('Generate playlist')
                        if generation_button:
                            description = f'Emotion Playlist for Valence: {valence}, Energy: {energy}, Danceability: {danceability}). ' \
                                          f'Playlist generated by the EmotionPlaylist app: https://huggingface.co/spaces/ccolas/EmotionPlaylist.'
                            playlist_info = sp.user_playlist_create(user_id, playlist_name, public=True, collaborative=False, description=description)
                            playlist_uri = playlist_info['uri'].split(':')[-1]
                            sp.playlist_add_items(playlist_uri, selected_tracks_uris)

                            st.success(f'The playlist has been generated, find it [here](https://open.spotify.com/playlist/{playlist_uri}).')


                    stop = 1


if __name__ == '__main__':
    setup_streamlite()