File size: 7,499 Bytes
e4f4d5f
dc5eb33
e4f4d5f
dbdd344
9faaa03
 
dbdd344
dc5eb33
 
52a6c11
9faaa03
dbdd344
dc5eb33
e4f4d5f
 
dc5eb33
 
 
 
 
 
 
e4f4d5f
dbdd344
 
 
 
52a6c11
 
dbdd344
7507bae
52a6c11
9faaa03
 
b414958
a8c78b0
b414958
866595f
dc5eb33
 
 
 
 
 
2082b5c
f8df622
e4f4d5f
 
dc5eb33
 
e4f4d5f
 
dc5eb33
 
 
 
e4f4d5f
dc5eb33
e4f4d5f
 
866595f
dc5eb33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52a6c11
dc5eb33
e4f4d5f
dc5eb33
671016b
52a6c11
dc5eb33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e4f4d5f
dc5eb33
 
 
 
e4f4d5f
 
dc5eb33
e4f4d5f
dc5eb33
e4f4d5f
 
 
 
 
f8df622
2082b5c
866595f
dc5eb33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a8c78b0
dc5eb33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e4f4d5f
 
dc5eb33
e4f4d5f
866595f
2082b5c
dc5eb33
 
 
 
 
 
 
 
 
e4f4d5f
dc5eb33
f8df622
866595f
 
dc5eb33
e4f4d5f
 
 
dc5eb33
 
 
 
 
52a6c11
9faaa03
e4f4d5f
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
import os
import json
import random
import gradio as gr
import spotipy
from spotipy.oauth2 import SpotifyOAuth

# β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
# 1) Spotify credentials (stored in HF Secrets)
CLIENT_ID     = os.environ["SPOTIFY_CLIENT_ID"]
CLIENT_SECRET = os.environ["SPOTIFY_CLIENT_SECRET"]

# 2) Your Space’s URL must match this Redirect URI exactly
REDIRECT_URI = "https://jisaacso219-rng-shuffle.hf.space/"

# 3) Include `streaming` for Web Playback SDK + playback scopes
SCOPE = (
    "streaming "
    "user-read-playback-state "
    "user-modify-playback-state "
    "playlist-read-private"
)

sp_oauth = SpotifyOAuth(
    client_id=CLIENT_ID,
    client_secret=CLIENT_SECRET,
    redirect_uri=REDIRECT_URI,
    scope=SCOPE,
    show_dialog=True,
)

sp = None
user_playlists = {}

def get_auth_url():
    return sp_oauth.get_authorize_url()

def check_login(code: str):
    """
    Called on page‐load via demo.load(). Once we have a `code`:
     - exchange it for an access token
     - fetch playlists
     - inject the Web Playback SDK + token + debug hooks
    """
    global sp, user_playlists
    if not code:
        return (
            gr.update(visible=False),  # status
            gr.update(visible=False),  # dropdown
            gr.update(visible=False),  # sdk_html
        )

    # 1) Exchange code for token
    tokinfo      = sp_oauth.get_access_token(code, as_dict=True)
    access_token = tokinfo["access_token"]
    sp = spotipy.Spotify(auth=access_token)

    # 2) Fetch your playlists
    items = sp.current_user_playlists(limit=50)["items"]
    user_playlists = {p["name"]: p["id"] for p in items}

    # 3) Build SDK + injection scripts
    sdk_js = f"""
    <script>
      // If still sandboxed in an iframe, break out:
      if (window.self !== window.top) {{
        window.top.location.href = window.location.href;
      }}
      console.log("[SDK] Setting ACCESS_TOKEN");
      window.ACCESS_TOKEN = "{access_token}";
    </script>
    <script src="https://sdk.scdn.co/spotify-player.js"></script>
    <script>
      window.onSpotifyWebPlaybackSDKReady = () => {{
        console.log("[SDK] SDK Ready");
        const player = new Spotify.Player({{
          name: 'RNG Web Player',
          getOAuthToken: cb => cb(window.ACCESS_TOKEN),
          volume: 0.5
        }});
        ['initialization_error','authentication_error','account_error','playback_error']
          .forEach(evt => player.addListener(evt, e => console.error(`[SDK ${evt}]`, e)));
        player.addListener('ready', ({ device_id }) => {{
          console.log('[SDK] Player ready, device_id:', device_id);
          window._webDeviceId = device_id;
        }});
        player.connect().then(success => console.log('[SDK] connect()', success));
      }};
    </script>
    """

    return (
        gr.update(visible=True, value="βœ… Logged in! Select a playlist below."),
        gr.update(visible=True, choices=list(user_playlists.keys())),
        gr.update(visible=True, value=sdk_js),
    )

def load_playlist_info(playlist_name: str):
    """
    When a playlist is selected, show its artwork, name, and description.
    """
    pid  = user_playlists[playlist_name]
    data = sp.playlist(pid)
    img   = data["images"][0]["url"] if data["images"] else ""
    owner = data["owner"]["display_name"]
    desc  = data.get("description", "")
    html  = f"""
      <img src="{img}" width="300" style="border-radius:8px;"/><br/>
      <strong>{playlist_name}</strong> by {owner}<br/>{desc}
    """
    return html, gr.update(visible=True)

def shuffle_and_play(playlist_name: str):
    """
    Gather all track URIs, RNG-shuffle them, then fire off the Web Playback SDK
    to play them in-browser on window._webDeviceId.
    """
    pid = user_playlists.get(playlist_name)
    if not pid:
        return gr.update(value="⚠️ No playlist selected.", visible=True)

    # fetch + shuffle
    tracks, results = [], sp.playlist_tracks(pid)
    tracks.extend(results["items"])
    while results["next"]:
        results = sp.next(results)
        tracks.extend(results["items"])
    uris = [t["track"]["uri"] for t in tracks if t["track"]]
    random.shuffle(uris)

    # in-page JS to start playback
    uris_json = json.dumps(uris)
    play_js = f"""
    <div id="player_debug" style="color:white;font-family:monospace;"></div>
    <script>
      console.log("[JS] Starting playback with", window._webDeviceId);
      console.log("[JS] URIs count:", {len(uris)});
      (async () => {{
        try {{
          const resp = await fetch(
            'https://api.spotify.com/v1/me/player/play?device_id=' + window._webDeviceId,
            {{
              method: 'PUT',
              headers: {{
                'Authorization': 'Bearer ' + window.ACCESS_TOKEN,
                'Content-Type': 'application/json'
              }},
              body: JSON.stringify({{ uris: {uris_json} }})
            }}
          );
          const text = await resp.text();
          console.log("[JS] fetch status:", resp.status, text);
          document.getElementById("player_debug").innerText =
            `[JS] status: ${{resp.status}} – ${{text}}`;
        }} catch(e) {{
          console.error("[JS] Playback error:", e);
          document.getElementById("player_debug").innerText =
            "[JS] Playback error: " + e;
        }}
      }})();
    </script>
    """
    return gr.update(value="▢️ Now playing in-browser!" + play_js, visible=True)

# β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
with gr.Blocks() as demo:
    # 0) Auto-bust from any huggingface.co iframe
    gr.HTML("""
    <script>
      if (window.self !== window.top) {
        window.top.location.href = window.location.href;
      }
    </script>
    """)
    # 0b) Fallback standalone link
    gr.HTML('<a href="https://jisaacso219-rng-shuffle.hf.space/" '
            'target="_blank">πŸ”— Open in standalone tab</a>')

    gr.Markdown("**1)** Login &nbsp;&nbsp; **2)** Select a playlist &nbsp;&nbsp; **3)** Shuffle & Play in-browser")

    # Step 1: Login to Spotify in SAME tab
    login_btn = gr.Button("πŸ” Step 1: Login to Spotify")
    login_btn.click(
        fn=None, inputs=None, outputs=None,
        js=f"() => window.location.href = '{get_auth_url()}'"
    )

    # These will be revealed after login
    code_box   = gr.Textbox(visible=False, elem_id="auth_code")
    status     = gr.Markdown(visible=False)
    playlist_dd= gr.Dropdown(choices=[], label="Select Playlist", visible=False)
    sdk_html   = gr.HTML(visible=False)

    info_html  = gr.HTML(visible=False)
    shuffle_btn= gr.Button("πŸ”€ Step 3: Shuffle & Play", visible=False)
    result     = gr.HTML(visible=False)

    # On page-load (or after redirect with ?code), run check_login
    demo.load(
        fn=check_login,
        inputs=[code_box],
        outputs=[status, playlist_dd, sdk_html],
        js="() => new URLSearchParams(window.location.search).get('code') || ''"
    )

    # Show playlist info, then reveal shuffle button
    playlist_dd.change(load_playlist_info, [playlist_dd], [info_html, shuffle_btn])

    # Shuffle & play via Web Playback SDK
    shuffle_btn.click(shuffle_and_play, [playlist_dd], [result])

if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", ssr_mode=False)