1+ <!DOCTYPE html>
2+ < html >
3+ < head >
4+ < title > Spotify Music Quiz Cards</ title >
5+ < script src ="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js "> </ script >
6+ < script src ="https://cdn.tailwindcss.com "> </ script >
7+ < style >
8+ @media print {
9+ body * {
10+ visibility : hidden;
11+ }
12+ # printArea , # printArea * {
13+ visibility : visible;
14+ }
15+ # printArea {
16+ position : absolute;
17+ left : 0 ;
18+ top : 0 ;
19+ }
20+ }
21+
22+ .card {
23+ width : 6cm ;
24+ height : 6cm ;
25+ border : 1px solid # e5e7eb ;
26+ border-radius : 8px ;
27+ background : white;
28+ padding : 10px ;
29+ margin : 10px ;
30+ page-break-inside : avoid !important ;
31+ break-inside : avoid !important ;
32+ }
33+
34+ .page {
35+ page-break-after : always !important ;
36+ page-break-before : always !important ;
37+ margin-bottom : 2cm ;
38+ }
39+
40+ .card-container {
41+ display : grid;
42+ grid-template-columns : repeat (3 , 1fr );
43+ gap : 20px ;
44+ padding : 20px ;
45+ page-break-inside : avoid !important ;
46+ break-inside : avoid !important ;
47+ }
48+
49+ .card-page {
50+ display : grid;
51+ grid-template-columns : repeat (3 , 1fr );
52+ gap : 20px ;
53+ padding : 20px ;
54+ page-break-after : always;
55+ }
56+
57+ .qr-container {
58+ display : flex;
59+ justify-content : center;
60+ height : 100% ;
61+ align-items : center;
62+ }
63+
64+ .metadata {
65+ text-align : center;
66+ height : 100% ;
67+ display : flex;
68+ flex-direction : column;
69+ justify-content : center;
70+ }
71+
72+ .metadata .year {
73+ font-size : 24px ;
74+ margin-top : 8px ;
75+ }
76+
77+ # printArea {
78+ display : block;
79+ padding : 1cm ;
80+ }
81+
82+ .hidden {
83+ display : none;
84+ }
85+
86+ # pauseResumeButton {
87+ width : 60vmin ;
88+ height : 60vmin ;
89+ font-size : 30vmin ;
90+ border : none;
91+ border-radius : 50% ;
92+ background : linear-gradient (145deg , # 1DB954, # 169c45 );
93+ color : white;
94+ cursor : pointer;
95+ transition : all 0.3s ease;
96+ box-shadow : 0 10px 20px rgba (29 , 185 , 84 , 0.3 );
97+ display : flex;
98+ justify-content : center;
99+ align-items : center;
100+ }
101+
102+ # pauseResumeButton : hover {
103+ transform : scale (1.02 );
104+ box-shadow : 0 15px 30px rgba (29 , 185 , 84 , 0.4 );
105+ }
106+
107+ # pauseResumeButton : active {
108+ transform : scale (0.98 );
109+ box-shadow : 0 5px 15px rgba (29 , 185 , 84 , 0.2 );
110+ }
111+
112+ # player {
113+ display : flex;
114+ justify-content : center;
115+ align-items : center;
116+ height : 100vh ;
117+ margin : 0 ;
118+ flex-direction : column;
119+ }
120+
121+ .button-container {
122+ display : flex;
123+ flex-direction : column;
124+ align-items : center;
125+ gap : 2rem ;
126+ }
127+
128+ .scan-button {
129+ padding : 1.5rem 3rem ;
130+ font-size : 1.5rem ;
131+ font-weight : 600 ;
132+ color : white;
133+ background : linear-gradient (145deg , # 1e1e1e, # 2d2d2d );
134+ border : none;
135+ border-radius : 15px ;
136+ cursor : pointer;
137+ text-decoration : none;
138+ transition : all 0.3s ease;
139+ box-shadow : 0 10px 20px rgba (0 , 0 , 0 , 0.2 );
140+ text-transform : uppercase;
141+ letter-spacing : 1px ;
142+ }
143+
144+ .scan-button : hover {
145+ transform : translateY (-2px );
146+ box-shadow : 0 15px 30px rgba (0 , 0 , 0 , 0.3 );
147+ }
148+
149+ .scan-button : active {
150+ transform : translateY (1px );
151+ box-shadow : 0 5px 15px rgba (0 , 0 , 0 , 0.1 );
152+ }
153+ </ style >
154+ </ head >
155+ < body class ="bg-gray-100 min-h-screen ">
156+ < div id ="setup " class ="max-w-md mx-auto p-6 ">
157+ < div class ="bg-white rounded-lg shadow-xl p-8 ">
158+ < input type ="text " id ="clientId " placeholder ="Enter Spotify Client ID " class ="w-full px-4 py-2 mb-4 border rounded ">
159+ < button onclick ="startAuth() " class ="w-full bg-green-500 text-white py-2 px-4 rounded "> Initialize</ button >
160+ </ div >
161+ </ div >
162+
163+ < div id ="player " class ="hidden ">
164+ < div class ="button-container ">
165+ < button id ="pauseResumeButton "> ⏸️</ button >
166+ < a href ="https://www.gptgames.dev/tools/spotify_qr_scanner " class ="scan-button ">
167+ Scan for next song
168+ </ a >
169+ </ div >
170+ < button id ="createQRButton " class ="fixed top-4 right-4 bg-green-500 text-white py-2 px-4 rounded ">
171+ Create QR codes from Playlist
172+ </ button >
173+ </ div >
174+
175+ < div id ="printArea "> </ div >
176+
177+ < script >
178+ let playerInstance = null ;
179+ let isPlaying = false ;
180+ const redirectUri = 'http://gptgames.dev/tools/spotify_music_quiz_cards.html' ;
181+
182+ function togglePauseResume ( ) {
183+ if ( isPlaying ) {
184+ playerInstance . pause ( ) ;
185+ } else {
186+ playerInstance . resume ( ) ;
187+ }
188+ isPlaying = ! isPlaying ;
189+ pauseResumeButton . textContent = isPlaying ? '⏸️' : '▶️' ;
190+ }
191+
192+ function getUrlParameter ( name ) {
193+ return new URLSearchParams ( window . location . search ) . get ( name ) ;
194+ }
195+
196+ window . onSpotifyWebPlaybackSDKReady = ( ) => {
197+ const token = localStorage . getItem ( 'spotify_access_token' ) || getHashParams ( ) . access_token ;
198+ if ( token ) initializePlayer ( token ) ;
199+ } ;
200+
201+ function startAuth ( ) {
202+ const clientId = document . getElementById ( 'clientId' ) . value ;
203+ localStorage . setItem ( 'spotify_client_id' , clientId ) ;
204+ const scope = 'streaming user-read-email user-read-private user-modify-playback-state playlist-read-private' ;
205+ window . location . href = `https://accounts.spotify.com/authorize?client_id=${ clientId } &response_type=token&redirect_uri=${ encodeURIComponent ( redirectUri ) } &scope=${ encodeURIComponent ( scope ) } ` ;
206+ }
207+
208+ function getHashParams ( ) {
209+ const hashParams = { } ;
210+ window . location . hash . substring ( 1 ) . split ( '&' ) . forEach ( param => {
211+ const [ key , value ] = param . split ( '=' ) ;
212+ hashParams [ key ] = decodeURIComponent ( value ) ;
213+ } ) ;
214+ return hashParams ;
215+ }
216+
217+ async function getPlaylistTracks ( input ) {
218+ let playlistId = input . match ( / p l a y l i s t \/ ( [ a - z A - Z 0 - 9 ] + ) / ) ?. [ 1 ] ;
219+ if ( ! playlistId ) {
220+ throw new Error ( 'Could not extract playlist ID from URL' ) ;
221+ }
222+ const response = await fetch ( `https://api.spotify.com/v1/playlists/${ playlistId } /tracks` , {
223+ headers : {
224+ 'Authorization' : `Bearer ${ localStorage . getItem ( 'spotify_access_token' ) } `
225+ }
226+ } ) ;
227+ if ( ! response . ok ) {
228+ throw new Error ( `API request failed with status ${ response . status } ` ) ;
229+ }
230+ const data = await response . json ( ) ;
231+ return data . items
232+ . filter ( item => item ?. track && item . track . album ?. release_date )
233+ . map ( item => ( {
234+ id : item . track . id || '' ,
235+ name : item . track . name || 'Unknown Title' ,
236+ artist : item . track . artists ?. [ 0 ] ?. name || 'Unknown Artist' ,
237+ year : item . track . album . release_date . split ( '-' ) [ 0 ] || 'Unknown Year'
238+ } ) ) ;
239+ }
240+
241+ function createQRCard ( track , isQRSide ) {
242+ const card = document . createElement ( 'div' ) ;
243+ card . className = 'card' ;
244+
245+ if ( isQRSide ) {
246+ const qrContainer = document . createElement ( 'div' ) ;
247+ qrContainer . className = 'qr-container' ;
248+ qrContainer . id = `qr-${ track . id } ` ;
249+ card . appendChild ( qrContainer ) ;
250+
251+ new QRCode ( qrContainer , {
252+ text : `https://www.gptgames.dev/tools/spotify_test.html?track=${ track . id } ` ,
253+ width : 220 ,
254+ height : 220 ,
255+ colorDark : "#000000" ,
256+ colorLight : "#ffffff" ,
257+ correctLevel : QRCode . CorrectLevel . H
258+ } ) ;
259+ } else {
260+ const metadata = document . createElement ( 'div' ) ;
261+ metadata . className = 'metadata' ;
262+ metadata . innerHTML = `
263+ <h3 class="title">${ track . name } </h3>
264+ <p class="artist">${ track . artist } </p>
265+ <p class="year">${ track . year } </p>
266+ ` ;
267+ card . appendChild ( metadata ) ;
268+ }
269+
270+ return card ;
271+ }
272+
273+ async function createQRCodesFromPlaylist ( ) {
274+ const input = prompt ( "Enter Spotify playlist URL:" ) ;
275+ if ( ! input ) return ;
276+
277+ try {
278+ const tracks = await getPlaylistTracks ( input ) ;
279+ if ( tracks . length === 0 ) {
280+ throw new Error ( 'No valid tracks found in playlist' ) ;
281+ }
282+
283+ const printArea = document . getElementById ( 'printArea' ) ;
284+ printArea . innerHTML = '' ;
285+
286+ // Create wrapper divs
287+ const qrPage = document . createElement ( 'div' ) ;
288+ qrPage . className = 'page' ;
289+ const qrContainer = document . createElement ( 'div' ) ;
290+ qrContainer . className = 'card-container' ;
291+
292+ // Create QR codes page
293+ const selectedTracks = tracks . slice ( 0 , 21 ) ;
294+ selectedTracks . forEach ( track => {
295+ qrContainer . appendChild ( createQRCard ( track , true ) ) ;
296+ } ) ;
297+
298+ qrPage . appendChild ( qrContainer ) ;
299+ printArea . appendChild ( qrPage ) ;
300+
301+ // Create metadata page
302+ const metadataPage = document . createElement ( 'div' ) ;
303+ metadataPage . className = 'page' ;
304+ const metadataContainer = document . createElement ( 'div' ) ;
305+ metadataContainer . className = 'card-container' ;
306+
307+ selectedTracks . forEach ( track => {
308+ metadataContainer . appendChild ( createQRCard ( track , false ) ) ;
309+ } ) ;
310+
311+ metadataPage . appendChild ( metadataContainer ) ;
312+ printArea . appendChild ( metadataPage ) ;
313+
314+ setTimeout ( ( ) => {
315+ window . print ( ) ;
316+ } , 1000 ) ;
317+
318+ } catch ( error ) {
319+ alert ( 'Error processing tracks: ' + error . message ) ;
320+ console . error ( 'Detailed error:' , error ) ;
321+ }
322+ }
323+
324+ function initializePlayer ( token ) {
325+ document . getElementById ( 'setup' ) . classList . add ( 'hidden' ) ;
326+ document . getElementById ( 'player' ) . classList . remove ( 'hidden' ) ;
327+ localStorage . setItem ( 'spotify_access_token' , token ) ;
328+
329+ playerInstance = new Spotify . Player ( {
330+ name : 'Development Player' ,
331+ getOAuthToken : cb => {
332+ cb ( token ) ;
333+ }
334+ } ) ;
335+
336+ playerInstance . addListener ( 'ready' , async ( { device_id } ) => {
337+ await fetch ( 'https://api.spotify.com/v1/me/player' , {
338+ method : 'PUT' ,
339+ headers : {
340+ 'Authorization' : `Bearer ${ token } ` ,
341+ 'Content-Type' : 'application/json'
342+ } ,
343+ body : JSON . stringify ( {
344+ device_ids : [ device_id ] ,
345+ play : true
346+ } )
347+ } ) ;
348+
349+ const trackId = getUrlParameter ( 'track' ) ;
350+ if ( trackId ) {
351+ await fetch ( `https://api.spotify.com/v1/me/player/play?device_id=${ device_id } ` , {
352+ method : 'PUT' ,
353+ headers : {
354+ 'Authorization' : `Bearer ${ token } ` ,
355+ 'Content-Type' : 'application/json'
356+ } ,
357+ body : JSON . stringify ( {
358+ uris : [ `spotify:track:${ trackId } ` ]
359+ } )
360+ } ) ;
361+ }
362+ } ) ;
363+
364+ document . getElementById ( 'pauseResumeButton' ) . onclick = togglePauseResume ;
365+ document . getElementById ( 'createQRButton' ) . onclick = createQRCodesFromPlaylist ;
366+ playerInstance . connect ( ) ;
367+ }
368+ </ script >
369+ < script src ="https://sdk.scdn.co/spotify-player.js "> </ script >
370+ </ body >
371+ </ html >
0 commit comments