<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nityanand Thakur</title>
    <description>The latest articles on DEV Community by Nityanand Thakur (@lsnnt).</description>
    <link>https://dev.to/lsnnt</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3957880%2Fe9d0ea91-ba16-41b0-80d6-0ffb2b4afd2d.png</url>
      <title>DEV Community: Nityanand Thakur</title>
      <link>https://dev.to/lsnnt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lsnnt"/>
    <language>en</language>
    <item>
      <title>I built a Spotify recently-played banner for GitHub — without registering an OAuth app</title>
      <dc:creator>Nityanand Thakur</dc:creator>
      <pubDate>Fri, 29 May 2026 06:38:58 +0000</pubDate>
      <link>https://dev.to/lsnnt/i-built-a-spotify-recently-played-banner-for-github-without-registering-an-oauth-app-1ob8</link>
      <guid>https://dev.to/lsnnt/i-built-a-spotify-recently-played-banner-for-github-without-registering-an-oauth-app-1ob8</guid>
      <description>&lt;p&gt;Most "Spotify for GitHub README" projects share the same setup story: go to the Spotify developer dashboard, register an app, grab a client ID and secret, plug them into some hosted service, authorize it, and hope the maintainer keeps the server running.&lt;/p&gt;

&lt;p&gt;I wanted something self-hosted, with no developer app registration at all. So I dug into how the Spotify web player authenticates — and it turns out there's a cleaner path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick: &lt;code&gt;sp_dc&lt;/code&gt; + PKCE
&lt;/h2&gt;

&lt;p&gt;When you log into &lt;a href="https://open.spotify.com" rel="noopener noreferrer"&gt;open.spotify.com&lt;/a&gt;, your browser gets an &lt;code&gt;sp_dc&lt;/code&gt; session cookie. The web player uses this cookie to silently drive the full &lt;a href="https://developer.spotify.com/documentation/web-api/tutorials/code-pkce-flow" rel="noopener noreferrer"&gt;PKCE (Proof Key for Code Exchange)&lt;/a&gt; authorization flow and obtain a short-lived bearer token — without any client secret.&lt;/p&gt;

&lt;p&gt;The key endpoint is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://accounts.spotify.com/oauth2/v2/auth
  ?response_type=code
  &amp;amp;client_id=&amp;lt;spotify_web_player_client_id&amp;gt;
  &amp;amp;scope=user-read-recently-played ...
  &amp;amp;redirect_uri=https://developer.spotify.com
  &amp;amp;code_challenge=&amp;lt;sha256_of_verifier&amp;gt;
  &amp;amp;code_challenge_method=S256
  &amp;amp;response_mode=web_message
  &amp;amp;prompt=none
Cookie: sp_dc=&amp;lt;your_cookie&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;prompt=none&lt;/code&gt; and a valid &lt;code&gt;sp_dc&lt;/code&gt;, Spotify returns an authorization code directly in the response body — no browser redirect, no user interaction. You then exchange that code (plus the PKCE verifier) for a bearer token, and you're in.&lt;/p&gt;

&lt;p&gt;The whole auth chain in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sp_dc cookie → PKCE flow → bearer token → /v1/me/player/recently-played → SVG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The implementation
&lt;/h2&gt;

&lt;p&gt;The server is written in Go. A few things worth pointing out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token caching with mutex safety&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hitting the auth endpoint on every request would be slow and rate-limitable. The token is cached globally and protected with a &lt;code&gt;sync.Mutex&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;cachedToken&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;tokenMu&lt;/span&gt;     &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;getCachedToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;tokenMu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;tokenMu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cachedToken&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"getting token: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;cachedToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cachedToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Auto-refresh on non-200&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bearer tokens expire. Rather than tracking expiry times, the server just invalidates the cache whenever the Spotify API returns a non-200 and retries once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;getCachedToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c"&gt;// ... make the API call ...&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusOK&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;invalidateToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// decode and return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple and works well in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bypassing GitHub's Camo proxy cache&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GitHub proxies all images through &lt;a href="https://github.blog/2014-01-28-proxying-user-images/" rel="noopener noreferrer"&gt;Camo&lt;/a&gt;, its image CDN, which aggressively caches responses. Without the right headers, your banner would show stale data for hours. The fix is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cache-Control"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"max-age=0, no-cache, no-store, must-revalidate"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Camo not to cache the response, so every README load fetches a fresh SVG.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;The current SVG design is intentionally minimal — a dark Spotify-green card listing your 20 most recently played tracks with numbered rows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fspotify.fustin.top%2F%3Ftemp%3Dfoo" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fspotify.fustin.top%2F%3Ftemp%3Dfoo" alt="banner preview" width="700" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's room to make it much better: album art, artist names, play counts, theme variants. Contributions welcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/lsnnt/spotify-banner-for-github
&lt;span class="nb"&gt;cd &lt;/span&gt;spotify-banner-for-github
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get your &lt;code&gt;sp_dc&lt;/code&gt; cookie from DevTools → Application → Cookies on open.spotify.com, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'SPDC="your_cookie_here"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
go build &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./spotify-banner-for-github
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;http://localhost:8080/&lt;/code&gt; — you should see your recently played tracks rendered as an SVG.&lt;/p&gt;

&lt;p&gt;To embed it in your GitHub README, deploy it to any publicly reachable server and add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Spotify recently played&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://your-server.example.com/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A note on &lt;code&gt;sp_dc&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;sp_dc&lt;/code&gt; cookie is a long-lived session credential. Treat it like a password — don't commit your &lt;code&gt;.env&lt;/code&gt;, and rotate it by logging out and back into the web player. This approach is unofficial and intended for personal, non-commercial use.&lt;/p&gt;




&lt;p&gt;The full source is on GitHub: &lt;strong&gt;&lt;a href="https://github.com/lsnnt/spotify-banner-for-github" rel="noopener noreferrer"&gt;lsnnt/spotify-banner-for-github&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you like it do star the repo.&lt;/p&gt;

&lt;p&gt;If you want to improve the SVG design or add album art support, open a PR — that's the part that needs the most work right now.&lt;/p&gt;

</description>
      <category>go</category>
      <category>github</category>
      <category>opensource</category>
      <category>spotify</category>
    </item>
  </channel>
</rss>
