├── .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 | });
--------------------------------------------------------------------------------