├── .gitignore ├── Readme.md ├── github └── Screenshot.png ├── package.json ├── public ├── alan-walker-glitch.gif ├── music.m4a └── music_2.mp3 ├── script.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | .dump 2 | node_modules 3 | package-lock.json -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Leetcode Music Player 2 | 3 |
4 | 5 |
Turn Leetcode into a cool music visualizer
6 |
7 | 8 | ## How To Run 9 | 10 | - open your profile page 11 | - open dev console after page is properly loaded \[IMPORTANT\] 12 | - copy [script.js](./script.js) and paste on dev console 13 | 14 | > Thats it Enjoy 😀. 15 | 16 | ## Changing Music 17 | - There's a little proxy server [server.js](./server.js) that serves the music file enabling cors to any origin. 18 | - just spin up that server and change line 1 in [script.js](./script.js) to point to the music file. 19 | 20 | > [!WARNING] 21 | > If in near future leetcode changes its layout this script would not work. 22 | -------------------------------------------------------------------------------- /github/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrashanthKumar0/leetcode-music-player/d493e680184f5a3ec9734b59d4b595a2b6bad171/github/Screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lc", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "paths.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "node server.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "cors": "^2.8.5", 16 | "express": "^4.19.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /public/alan-walker-glitch.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrashanthKumar0/leetcode-music-player/d493e680184f5a3ec9734b59d4b595a2b6bad171/public/alan-walker-glitch.gif -------------------------------------------------------------------------------- /public/music.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrashanthKumar0/leetcode-music-player/d493e680184f5a3ec9734b59d4b595a2b6bad171/public/music.m4a -------------------------------------------------------------------------------- /public/music_2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrashanthKumar0/leetcode-music-player/d493e680184f5a3ec9734b59d4b595a2b6bad171/public/music_2.mp3 -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | const server_base_url = 'https://leetcode-music-player-1.onrender.com'; 2 | // const server_base_url = 'http://localhost:8080'; 3 | 4 | 5 | 6 | // Rating Graph --------------------------------------- 7 | const ratingGraph = (arr) => { 8 | const line_con = document.querySelector("g.highcharts-series.highcharts-series-0.highcharts-line-series > path.highcharts-graph"); 9 | const lines = document.querySelectorAll("g.highcharts-series.highcharts-series-0.highcharts-line-series > path.highcharts-graph .highcharts-point"); 10 | let x_min = 3.578431372549; 11 | let x_max = 361.42156862745; 12 | let n = 200; 13 | 14 | function makeD() { 15 | let d = ''; 16 | let x_range = x_max - x_min; 17 | let bias = x_range / 2; 18 | // let max_h = 50; 19 | let idx = 0; 20 | const emp_factor = 4; // used to nudge y a little (relative to its neighbour) 21 | let prev_y = -100; 22 | for (let x = x_min; x <= (x_max + bias); x += x_range / n) { 23 | // let y = Math.floor(Math.random() * max_h); // tweak this according to music 24 | let y = (arr[idx * 3] / 255) * 50; // tweak this according to music 25 | let y_emp = y; 26 | if(prev_y < 0) { 27 | prev_y = y; 28 | } else { 29 | let dy = y - prev_y; 30 | prev_y = y; 31 | y_emp += dy * emp_factor; 32 | } 33 | y = y_emp; 34 | y = 50 - y; // invert y 35 | idx++; 36 | if (x == x_min) { 37 | d += `M ${x} ${y}`; 38 | } else { 39 | d += `L ${x} ${y}`; 40 | } 41 | const color = `rgb(255, 161, 22, ${Math.max(0, y / 50 - 0.5)})`; 42 | // if(idx in lines) { 43 | // // lines[idx].setAttribute('fill', color); 44 | // } 45 | } 46 | 47 | return d; 48 | } 49 | line_con.setAttribute('d', makeD()) 50 | }; 51 | // ----------------------------------------------------- 52 | 53 | 54 | // Heatmap -------------------------------------------- 55 | const heatmap = () => { 56 | // const fill_colors = ['var(--green-10)','var(--green-20)','var(--green-40)','var(--green-60)','var(--green-80)','var(--green-100)','var(--fill-tertiary)']; 57 | const fill_colors = ['var(--green-10)', 'var(--green-20)', 'var(--green-40)', 'var(--green-60)', 'var(--green-80)']; 58 | const sel = document.querySelectorAll('g.week > rect'); 59 | // console.log(sel); 60 | sel.forEach(el => el.setAttribute('fill', fill_colors[Math.floor(Math.random() * fill_colors.length)])) 61 | }; 62 | //----------------------------------------------------- 63 | 64 | 65 | 66 | // Height charts -------------------------------------- 67 | const heightCharts = (arr) => { 68 | const bars = document.querySelectorAll(".highcharts-series-0 > .highcharts-point"); 69 | // let start_x = -16.5 * 2; 70 | let start_x = -18; 71 | let incs = [17, 16]; 72 | let bottom = 115.5; 73 | let min_h = 10; 74 | let max_h = 100; 75 | // 1 2 3 76 | // 17 1*17 + 1*16 2*17 + 1 * 16 77 | const makeD = (n, h) => { 78 | let a = Math.ceil(n / 2); 79 | let b = n - a; 80 | let base_x = start_x + a * incs[0] + b * incs[1]; 81 | let base_y = bottom - h; 82 | return `M ${5.5 + base_x} ${base_y} 83 | L ${16.5 + base_x} ${base_y} 84 | A 2 2 0 0 1 ${18.5 + base_x} ${base_y + 2} 85 | L ${18.5 + base_x} 115.5 86 | A 0 0 0 0 1 ${18.5 + base_x} 115.5 87 | L ${3.5 + base_x} 115.5 88 | A 0 0 0 0 1 ${3.5 + base_x} 115.5 89 | L ${3.5 + base_x} ${base_y + 2} 90 | A 2 2 0 0 1 ${5.5 + base_x} ${base_y} 91 | Z` 92 | .split('\n') 93 | .join(' '); 94 | } 95 | 96 | bars.forEach((bar, idx) => { 97 | if (idx == 0) bar.setAttribute('fill', 'transparent'); 98 | if (idx == 0) bar.setAttribute('stroke', 'transparent'); 99 | bar.setAttribute('d', makeD(idx, min_h + (arr[idx * 5] / 255) * (max_h - min_h))); 100 | }) 101 | }; 102 | // ----------------------------------------------------- 103 | 104 | 105 | 106 | const setupMeter = () => { 107 | const el = ` 108 | 109 | 110 | 111 | 112 | 113 | 114 | `; 115 | const meter_container = document.querySelector('g[clip-path="url(#bar-mask)"]'); 116 | meter_container.innerHTML = el; 117 | 118 | } 119 | 120 | const meter = (arr) => { 121 | const meter_el = document.getElementById('IM_METER_BUDDY'); 122 | // meter_el.style.strokeDashoffset = (315 - (arr[0] / 255) * 260); 123 | // meter_el.style.strokeDasharray = (315 - (arr[0] / 255) * 260); 124 | meter_el.setAttribute('class', `fill-transparent qa_6R stroke-sd-${arr[0] < 100 ? 'easy' : (arr[0] < 200 ? 'medium' : 'hard')}`); 125 | meter_el.setAttribute('style', `stroke-width: 3; stroke-linecap: round; stroke-dasharray: 315; stroke-dashoffset: ${315 - (arr[0] / 255) * 260};`); 126 | }; 127 | 128 | 129 | 130 | 131 | 132 | const meter2 = (arr) => { 133 | 134 | const el = ` 135 | 136 | 137 | 138 | 139 | 140 | 141 | `; 142 | const meter_container = document.querySelector('g[clip-path="url(#bar-mask)"]'); 143 | meter_container.innerHTML = el; 144 | }; 145 | 146 | 147 | 148 | 149 | // ----------------------------------------------------- 150 | 151 | const getGifBadgeUrl = ( month, year) =>{ 152 | month = +month; 153 | year = +year; 154 | return `https://assets.leetcode.com/static_assets/public/images/badges/${year}/gif/${year}-${month < 10 ? '0' : ''}${month}.gif`; 155 | } 156 | 157 | const getYearMonthFromBadgeUrl=(url) => { 158 | // url is something like /static/images/badges/dcc-2024-5.png 159 | if(url){ 160 | const img_name = url.split('/').pop(); // dcc-2024-5.png 161 | if(img_name){ 162 | const img_name_tok = img_name.split('-'); 163 | if(img_name_tok.length == 3){ 164 | const year = img_name_tok[1]; 165 | const month = img_name_tok[2].split('.')[0]; 166 | if(year && month){ 167 | return {year, month}; 168 | } 169 | } 170 | } 171 | } 172 | 173 | return null; 174 | } 175 | 176 | const animateBadges = () => { 177 | const heatmap_badges = document.querySelectorAll('image'); 178 | heatmap_badges.forEach((badge, idx)=>{ 179 | const attrib = badge.getAttribute('xlink:href'); 180 | const yearMonth = getYearMonthFromBadgeUrl(attrib); 181 | if(yearMonth){ 182 | const {year, month} = yearMonth; 183 | const gif_url = getGifBadgeUrl(month, year); 184 | setTimeout(()=>{ 185 | badge.setAttribute('xlink:href', gif_url); 186 | }, idx * 500); // add .5 seconds delay between each gif 187 | } 188 | }); 189 | 190 | 191 | 192 | const profile_badge = document.querySelector("div[class='ml-1'] img[class='h-\[12px\]']"); 193 | const attrib = profile_badge?.getAttribute('src'); 194 | if(attrib){ 195 | const yearMonth = getYearMonthFromBadgeUrl(attrib); 196 | if(yearMonth){ 197 | const {year, month} = yearMonth; 198 | const gif_url = getGifBadgeUrl(month, year); 199 | profile_badge.setAttribute('src', gif_url); 200 | } 201 | } 202 | } 203 | 204 | 205 | 206 | 207 | const animateOthers = () => { 208 | const streak_days = document.querySelector("#headlessui-popover-button-\\:r2\\:") 209 | const profile_img_top = document.querySelector('#navbar_user_avatar img'); 210 | const profile_img_left = document.querySelector('img[alt="Avatar"]'); 211 | streak_days?.classList.add('animate-pulse'); 212 | profile_img_top?.classList.add('animate-spin'); 213 | profile_img_left?.setAttribute('src', `${server_base_url}/alan-walker-glitch.gif`); 214 | } 215 | 216 | 217 | 218 | 219 | const audio_container = document.querySelector("#__next > div.flex.min-h-screen.min-w-\\[360px\\].flex-col.text-label-1.dark\\:text-dark-label-1.bg-layer-bg.dark\\:bg-dark-layer-bg > div.mx-auto.w-full.grow.p-4.md\\:max-w-\\[888px\\].md\\:p-6.lg\\:max-w-screen-xl > div > div.lc-lg\\:max-w-\\[calc\\(100\\%_-_316px\\)\\].w-full > div.lc-xl\\:flex-row.lc-xl\\:space-y-0.flex.w-full.flex-col.space-x-0.space-y-4.lc-xl\\:space-x-4 > div.bg-layer-1.dark\\:bg-dark-layer-1.shadow-down-01.dark\\:shadow-dark-down-01.rounded-lg.h-\\[180px\\].w-full.flex-1 > div > div"); 220 | 221 | // const audio_url = 'https://github.com/PrashanthKumar0/Fire-Audio-Visualizer/raw/master/music.mp3'; 222 | // const audio_url = 'http://localhost:8080/music.mp3'; 223 | const audio_url = `${server_base_url}/music.m4a`; 224 | const audio = new Audio(audio_url); 225 | audio.crossOrigin = "anonymous"; 226 | audio.controls = true; 227 | audio_container.innerHTML = ""; 228 | audio_container.prepend(audio); 229 | 230 | let audCtx = new AudioContext(); 231 | let analyser = audCtx.createAnalyser(); 232 | let audSrc = audCtx.createMediaElementSource(audio); 233 | audSrc.connect(analyser); 234 | analyser.connect(audCtx.destination); 235 | let arr = new Uint8Array(analyser.frequencyBinCount); 236 | 237 | 238 | 239 | 240 | setupMeter(); 241 | animateOthers(); 242 | animateBadges(); 243 | loop(); 244 | // setInterval(loop, 500) 245 | 246 | var g_time = performance.now(); 247 | var g_heatmap_step_size = 300; 248 | 249 | function loop() { 250 | let time = performance.now(); 251 | let dt = time - g_time; 252 | if(dt > g_heatmap_step_size) { 253 | g_time = time; 254 | } 255 | 256 | analyser.getByteFrequencyData(arr); 257 | meter(arr); 258 | // meter2(arr); 259 | ratingGraph(arr); 260 | heightCharts(arr); 261 | 262 | if(dt > g_heatmap_step_size){ 263 | heatmap(); 264 | } 265 | requestAnimationFrame(loop); 266 | } 267 | 268 | 269 | 270 | 271 | 272 | 273 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import e from 'express'; 2 | import cors from 'cors'; 3 | 4 | const app = e(); 5 | app.use(cors()); 6 | app.use(e.static('public')); 7 | 8 | 9 | app.listen(8080, ()=>{ 10 | console.log('file server started at http://localhost:8080'); 11 | }); --------------------------------------------------------------------------------