PageSourceSearch

https://soundfm.netlify.app/script.js

js soundfm.netlify.app collected 2026-10-03 11:25:40 UTC 23,884 bytes, 790 lines download raw bytes

1let now_playing = document.querySelector(".now-playing");
2let track_art = document.querySelector(".track-art");
3let track_name = document.querySelector(".track-name");
4let track_artist = document.querySelector(".track-artist");
5
6let playpause_btn = document.querySelector(".playpause-track");
7let next_btn = document.querySelector(".next-track");
8let prev_btn = document.querySelector(".prev-track");
9
10let seek_slider = document.querySelector(".seek_slider");
11let volume_slider = document.querySelector(".volume_slider");
12let curr_time = document.querySelector(".current-time");
13let total_duration = document.querySelector(".total-duration");
14
15let track_index = 0;
16let isPlaying = false;
17let updateTimer;
18let artworkRequestId = 0;
19let artworkPromiseCache = new Map();
20let artworkRunSeed =
21  Date.now().toString(36) + Math.random().toString(36).slice(2);
22let trackVisitCounts = new Map();
23let activeArtworkVisitId = "";
24
25let curr_track = document.createElement("audio");
26// Set this once in the browser console if you want live Unsplash artwork:
27// localStorage.setItem("UNSPLASH_ACCESS_KEY", "your_access_key_here")
28let UNSPLASH_ACCESS_KEY =
29  window.localStorage.getItem("UNSPLASH_ACCESS_KEY") || "";
30let artwork_cache = new Map();
31let hasPromptedForUnsplashKey = false;
32document.body.dataset.loadingStatus = "Loading artwork...";
33let TITLE_OVERRIDES = {
34  "Absence_of_Field_Focus_Learning_LoFi_30_87bpm_MedNEL_Nrmlzd2_VBR5.mp3":
35    "Absence of Field",
36  "SpringStep_Focus_DeepWork_LoFi_30_90bpm_MedNEL_Nrmlzd2_VBR5.mp3":
37    "Spring Step",
38  "TrickyLayup_Focus_Creativity_Lofi_30_100BPM_MediumNEL_Nrmlzd2_VBR5.mp3":
39    "Tricky Layup",
40  "Ctrl+Z_Focus_Learning_Lofi_30_90BPM_MediumNEL_Nrmlzd2_VBR5.mp3":
41    "Ctrl + Z",
42  "DialTone_Focus_Learning_Lofi_30_90BPM_MediumNEL_Nrmlzd2_VBR5.mp3":
43    "Dial Tone",
44  "LockIn_Focus_Learning_Lofi_30_100BPM_MediumNEL_Nrmlzd2_VBR5.mp3":
45    "Lock In",
46  "TwilightIdols_Focus_DeepWork_LoFi_30_85bpm_MedNEL_Nrmlzd2_VBR5.mp3":
47    "Twilight Idols",
48  "AncientSecrets_Focus_DeepWork_Lofi_30_90bpm_MedNEL_Nrmlzd2_VBR5.mp3":
49    "Ancient Secrets",
50  "Drawing_Circles_Lo_Fi_Study_Focus_Conor_100bpm_1.2_Nrmlzd2_VBR5.mp3":
51    "Drawing Circles",
52};
53let ARTWORK_QUERY_OVERRIDES = {
54  "absence of": [
55    "minimal empty room moody photo",
56    "quiet empty landscape atmospheric",
57    "solitary minimal interior photo",
58  ],
59  "ancient secrets": [
60    "ancient ruins moody cinematic",
61    "mystic temple interior photo",
62    "ancient symbols dramatic light",
63  ],
64  "astral amber": [
65    "amber nebula space art",
66    "golden cosmos stars photo",
67    "astral sky orange glow",
68  ],
69  "ctrl + z": [
70    "computer keyboard moody lighting",
71    "coding screen neon close up",
72    "glitch digital workstation photo",
73  ],
74  "dial tone": [
75    "retro telephone neon photo",
76    "telephone handset moody lighting",
77    "minimal sound wave neon",
78  ],
79  "drawing circles": [
80    "abstract concentric circles artwork",
81    "minimal geometric circles art",
82    "circular line pattern abstract",
83  ],
84  expressions: [
85    "abstract expressive portrait",
86    "colorful emotional abstract art",
87    "paint texture portrait moody",
88  ],
89  grains: [
90    "grain texture macro photo",
91    "field grass close up golden grain",
92    "particles texture abstract photo",
93  ],
94  kyanite: [
95    "kyanite crystal blue mineral close up",
96    "blue gemstone crystal macro",
97    "blue mineral texture crystal",
98  ],
99  "lock in": [
100    "steel vault lock close up",
101    "safe door combination lock photo",
102    "metal security vault close up",
103  ],
104  "spring step": [
105    "spring path landscape photo",
106    "fresh green stairs or pathway",
107    "blooming landscape cinematic",
108  ],
109  "tricky layup": [
110    "basketball layup hoop action",
111    "street basketball hoop close up",
112    "basketball in motion by the rim",
113  ],
114  "twilight idols": [
115    "twilight statue dramatic sky",
116    "mythic statue dusk cinematic",
117    "purple sunset sculpture photo",
118  ],
119  "vhs dream": [
120    "retro vhs neon room",
121    "analog screen dreamscape photo",
122    "surreal retro haze aesthetic",
123  ],
124};
125
126function toTitleCase(value) {
127  return value.replace(/\b\w/g, (char) => char.toUpperCase());
128}
129
130function hashString(value) {
131  let hash = 0;
132
133  for (let index = 0; index < value.length; index += 1) {
134    hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
135  }
136
137  return hash;
138}
139
140function hslToRgb(h, s, l) {
141  s /= 100;
142  l /= 100;
143
144  let c = (1 - Math.abs(2 * l - 1)) * s;
145  let x = c * (1 - Math.abs(((h / 60) % 2) - 1));
146  let m = l - c / 2;
147  let r = 0;
148  let g = 0;
149  let b = 0;
150
151  if (h < 60) {
152    r = c;
153    g = x;
154  } else if (h < 120) {
155    r = x;
156    g = c;
157  } else if (h < 180) {
158    g = c;
159    b = x;
160  } else if (h < 240) {
161    g = x;
162    b = c;
163  } else if (h < 300) {
164    r = x;
165    b = c;
166  } else {
167    r = c;
168    b = x;
169  }
170
171  return {
172    r: Math.round((r + m) * 255),
173    g: Math.round((g + m) * 255),
174    b: Math.round((b + m) * 255),
175  };
176}
177
178function rgbToCss(color) {
179  return `rgb(${color.r}, ${color.g}, ${color.b})`;
180}
181
182function getRelativeLuminance(color) {
183  let channels = [color.r, color.g, color.b].map((value) => {
184    let normalized = value / 255;
185
186    if (normalized <= 0.03928) {
187      return normalized / 12.92;
188    }
189
190    return Math.pow((normalized + 0.055) / 1.055, 2.4);
191  });
192
193  return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
194}
195
196function getContrastTextColor(color) {
197  return getRelativeLuminance(color) < 0.42 ? "white" : "black";
198}
199
200function buildFallbackArtwork(track, index) {
201  let hash = hashString(`${track.name}-${track.artist}-${index}`);
202  let hueA = hash % 360;
203  let hueB = (hueA + 42) % 360;
204  let colorA = hslToRgb(hueA, 72, 58);
205  let colorB = hslToRgb(hueB, 74, 42);
206  let initial = track.name.charAt(0).toUpperCase();
207  let svg = `
208    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
209      <defs>
210        <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
211          <stop offset="0%" stop-color="${rgbToCss(colorA)}"/>
212          <stop offset="100%" stop-color="${rgbToCss(colorB)}"/>
213        </linearGradient>
214      </defs>
215      <rect width="300" height="300" rx="40" fill="url(#bg)"/>
216      <circle cx="150" cy="150" r="82" fill="rgba(255,255,255,0.12)"/>
217      <circle cx="150" cy="150" r="24" fill="rgba(255,255,255,0.88)"/>
218      <text x="50%" y="86%" text-anchor="middle" fill="rgba(255,255,255,0.92)"
219        font-family="Poppins, Arial, sans-serif" font-size="28" font-weight="700">${initial}</text>
220    </svg>
221  `.trim();
222
223  return {
224    url: `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`,
225    themeColor: colorA,
226    attribution: "Generated fallback artwork",
227  };
228}
229
230function uniqueTerms(values) {
231  return [...new Set(values.filter(Boolean))];
232}
233
234function splitWords(value) {
235  return value
236    .replace(/([a-z])([A-Z])/g, "$1 $2")
237    .replace(/[+]/g, " ")
238    .replace(/[_-]+/g, " ")
239    .replace(/\s+/g, " ")
240    .trim();
241}
242
243function getVisualHints(title) {
244  let normalized = title.toLowerCase();
245  let hints = [];
246
247  if (normalized.includes("ancient")) hints.push("ancient ruins", "temple");
248  if (normalized.includes("secret")) hints.push("mystic symbols", "hidden chamber");
249  if (normalized.includes("astral")) hints.push("cosmos", "stars", "nebula");
250  if (normalized.includes("amber")) hints.push("amber light", "golden glow");
251  if (normalized.includes("dial")) hints.push("retro telephone", "neon communication");
252  if (normalized.includes("tone")) hints.push("sound waves", "minimal neon");
253  if (normalized.includes("drawing")) hints.push("abstract circles", "geometric lines");
254  if (normalized.includes("circle")) hints.push("circular pattern", "abstract rings");
255  if (normalized.includes("expression")) hints.push("abstract portrait", "emotional color");
256  if (normalized.includes("grain")) hints.push("grain texture", "field", "particles");
257  if (normalized.includes("kyanite")) hints.push("blue crystal", "gemstone", "mineral");
258  if (normalized.includes("lock")) hints.push("lock", "vault", "cyber security");
259  if (normalized.includes("spring")) hints.push("spring landscape", "fresh green");
260  if (normalized.includes("step")) hints.push("stairway", "path");
261  if (normalized.includes("tricky")) hints.push("basketball court", "street motion");
262  if (normalized.includes("layup")) hints.push("basketball", "hoop");
263  if (normalized.includes("twilight")) hints.push("twilight sky", "dusk", "purple sunset");
264  if (normalized.includes("idol")) hints.push("statue", "mythic figure");
265  if (normalized.includes("vhs")) hints.push("vhs texture", "retro screen", "analog");
266  if (normalized.includes("dream")) hints.push("dreamscape", "surreal", "haze");
267  if (normalized.includes("absence")) hints.push("empty room", "minimal landscape", "void");
268  if (normalized.includes("field")) hints.push("field", "wide landscape");
269  if (normalized.includes("ctrl")) hints.push("computer keyboard", "code screen", "glitch");
270  if (normalized.includes("z")) hints.push("digital", "cyber");
271
272  return uniqueTerms(hints);
273}
274
275function buildSearchQueries(track) {
276  let overrideQueries = ARTWORK_QUERY_OVERRIDES[track.name.toLowerCase()] || [];
277  let moodWords = track.mood.map((value) => value.toLowerCase());
278  let base = [track.name, ...track.visualHints].join(" ").trim();
279  let thematic = [...track.visualHints, ...moodWords].join(" ").trim();
280
281  return uniqueTerms([
282    ...overrideQueries,
283    track.name,
284    thematic,
285    base,
286    `${track.name} ${thematic}`.trim(),
287    `${thematic} abstract art`.trim(),
288    `${track.name} photo`.trim(),
289    `${track.name} artwork`.trim(),
290    `${track.name} album art`.trim(),
291    `${track.name} aesthetic`.trim(),
292    `${moodWords.join(" ")} ambient photo`.trim(),
293    `${track.name} cinematic scene`.trim(),
294    `${track.name} digital art`.trim(),
295    `${thematic} moody aesthetic`.trim(),
296  ]);
297}
298
299function normalizeUnsplashPhoto(photo, fallback) {
300  if (!photo?.urls?.regular) {
301    return null;
302  }
303
304  let photoUrl = new URL(photo.urls.regular);
305  photoUrl.searchParams.set("w", "700");
306  photoUrl.searchParams.set("h", "700");
307  photoUrl.searchParams.set("fit", "crop");
308  photoUrl.searchParams.set("crop", "entropy");
309  photoUrl.searchParams.set("auto", "format");
310
311  return {
312    url: photoUrl.toString(),
313    attribution: `Photo by ${photo.user?.name || "Unsplash"} on Unsplash`,
314    downloadLocation: photo.links?.download_location,
315    fallbackThemeColor: fallback.themeColor,
316  };
317}
318
319function formatTrack(file, index) {
320  let clean = splitWords(file.replace(/\.mp3$/i, ""));
321  let tokens = clean.split(/[_ ]+/).filter(Boolean);
322  let titleTokens = [];
323
324  for (const token of tokens) {
325    if (
326      /focus|deepwork|learning|creativity|study|work|lofi|lo|fi|mednel|mediumnel|nrmlzd2|vbr5|expanded|conor|\d+(\.\d+)?|\d+bpm/i.test(
327        token
328      )
329    ) {
330      break;
331    }
332    titleTokens.push(token);
333  }
334
335  let moodTokens = tokens.filter((token) =>
336    /focus|deepwork|learning|creativity|study|work/i.test(token)
337  );
338  let bpm = tokens.find((token) => /\d+\s*bpm|\d+bpm/i.test(token));
339  let title =
340    TITLE_OVERRIDES[file] ||
341    (
342      titleTokens.join(" ").replace(/\+/g, " + ") || `Track ${index + 1}`
343    )
344      .replace(/\s+/g, " ")
345      .trim();
346  let artist =
347    [
348      moodTokens
349        .map((token) => toTitleCase(token.replace(/deepwork/i, "Deep Work")))
350        .join(" • "),
351      bpm ? bpm.toUpperCase().replace(/\s+/g, "") : "",
352    ]
353      .filter(Boolean)
354      .join(" • ") || "Brain.fm";
355  let mood = moodTokens.map((token) =>
356    toTitleCase(token.replace(/deepwork/i, "Deep Work"))
357  );
358  let visualHints = getVisualHints(title);
359
360  return {
361    name: title,
362    artist,
363    mood,
364    visualHints,
365    searchQueries: buildSearchQueries({
366      name: title,
367      mood,
368      visualHints,
369    }),
370    path: file,
371  };
372}
373
374let track_list = [
375  "Absence_of_Field_Focus_Learning_LoFi_30_87bpm_MedNEL_Nrmlzd2_VBR5.mp3",
376  "AncientSecrets_Focus_DeepWork_Lofi_30_90bpm_MedNEL_Nrmlzd2_VBR5.mp3",
377  "Astral_Amber_Focus_Creativity_LoFi_30_87bpm_MedNEL_Nrmlzd2_VBR5.mp3",
378  "Ctrl+Z_Focus_Learning_Lofi_30_90BPM_MediumNEL_Nrmlzd2_VBR5.mp3",
379  "DialTone_Focus_Learning_Lofi_30_90BPM_MediumNEL_Nrmlzd2_VBR5.mp3",
380  "Drawing Circles Lo Fi Study Focus Conor 100bpm_1.2_Nrmlzd2_VBR5.mp3",
381  "Expressions_Focus_DeepWork_LoFi_30_90bpm_MedNEL_Nrmlzd2_VBR5.mp3",
382  "Grains_Focus_Learning_Lofi_30_85bpm_Nrmlzd2_expanded_VBR5.mp3",
383  "Kyanite_Focus_DeepWork_LoFi_30_80bpm_Nrmlzd2_VBR5.mp3",
384  "LockIn_Focus_Learning_Lofi_30_100BPM_MediumNEL_Nrmlzd2_VBR5.mp3",
385  "SpringStep_Focus_DeepWork_LoFi_30_90bpm_MedNEL_Nrmlzd2_VBR5.mp3",
386  "TrickyLayup_Focus_Creativity_Lofi_30_100BPM_MediumNEL_Nrmlzd2_VBR5.mp3",
387  "TwilightIdols_Focus_DeepWork_LoFi_30_85bpm_MedNEL_Nrmlzd2_VBR5.mp3",
388  "VHS Dream Lo Fi Light Work Focus Conor 86bpm_1.1_Nrmlzd2_VBR5.mp3",
389].map(formatTrack);
390
391function pickStartupTrackIndex() {
392  if (track_list.length <= 1) {
393    return 0;
394  }
395
396  let lastIndex = Number(window.localStorage.getItem("LAST_START_TRACK_INDEX"));
397  let nextIndex = Math.floor(Math.random() * track_list.length);
398
399  while (nextIndex === lastIndex) {
400    nextIndex = Math.floor(Math.random() * track_list.length);
401  }
402
403  window.localStorage.setItem("LAST_START_TRACK_INDEX", String(nextIndex));
404  return nextIndex;
405}
406
407track_index = pickStartupTrackIndex();
408
409function applyThemeColor(color) {
410  document.body.style.backgroundColor = rgbToCss(color);
411  document.body.style.color = getContrastTextColor(color);
412  document.documentElement.style.setProperty(
413    "--slider-track",
414    getContrastTextColor(color) === "white" ? "white" : "black"
415  );
416  document.documentElement.style.setProperty(
417    "--slider-thumb",
418    getContrastTextColor(color) === "white" ? "black" : "white"
419  );
420}
421
422function sampleImageColor(url) {
423  return new Promise((resolve, reject) => {
424    let image = new Image();
425    image.crossOrigin = "anonymous";
426
427    image.onload = () => {
428      let canvas = document.createElement("canvas");
429      let context = canvas.getContext("2d", { willReadFrequently: true });
430      let size = 24;
431      let buckets = new Map();
432      let topBucket = null;
433
434      canvas.width = size;
435      canvas.height = size;
436      context.drawImage(image, 0, 0, size, size);
437
438      let pixels = context.getImageData(0, 0, size, size).data;
439
440      for (let index = 0; index < pixels.length; index += 4) {
441        let alpha = pixels[index + 3];
442
443        if (alpha < 200) {
444          continue;
445        }
446
447        let r = pixels[index];
448        let g = pixels[index + 1];
449        let b = pixels[index + 2];
450        let bucketKey = `${Math.round(r / 32) * 32}-${Math.round(
451          g / 32
452        ) * 32}-${Math.round(b / 32) * 32}`;
453        let bucket =
454          buckets.get(bucketKey) || { r: 0, g: 0, b: 0, count: 0 };
455
456        bucket.r += r;
457        bucket.g += g;
458        bucket.b += b;
459        bucket.count += 1;
460        buckets.set(bucketKey, bucket);
461
462        if (!topBucket || bucket.count > topBucket.count) {
463          topBucket = bucket;
464        }
465      }
466
467      if (!topBucket) {
468        reject(new Error("No usable pixels"));
469        return;
470      }
471
472      resolve({
473        r: Math.round(topBucket.r / topBucket.count),
474        g: Math.round(topBucket.g / topBucket.count),
475        b: Math.round(topBucket.b / topBucket.count),
476      });
477    };
478
479    image.onerror = () => reject(new Error("Image load failed"));
480    image.src = url;
481  });
482}
483
484async function trackUnsplashDownload(downloadLocation) {
485  if (!downloadLocation || !UNSPLASH_ACCESS_KEY) {
486    return;
487  }
488
489  try {
490    let url = new URL(downloadLocation);
491    url.searchParams.set("client_id", UNSPLASH_ACCESS_KEY);
492    await fetch(url.toString(), { mode: "cors" });
493  } catch (error) {
494    console.warn("Unsplash download tracking failed", error);
495  }
496}
497
498function ensureUnsplashAccessKey() {
499  if (UNSPLASH_ACCESS_KEY || hasPromptedForUnsplashKey) {
500    return UNSPLASH_ACCESS_KEY;
501  }
502
503  hasPromptedForUnsplashKey = true;
504
505  let providedKey = window.prompt(
506    "Unsplash images need an Unsplash Access Key. Paste it here to enable live artwork for each song."
507  );
508
509  if (providedKey && providedKey.trim()) {
510    UNSPLASH_ACCESS_KEY = providedKey.trim();
511    window.localStorage.setItem("UNSPLASH_ACCESS_KEY", UNSPLASH_ACCESS_KEY);
512  }
513
514  return UNSPLASH_ACCESS_KEY;
515}
516
517function getArtworkCacheKey(track, visitId) {
518  return `${artworkRunSeed}:${visitId}:${track.path}`;
519}
520
521async function fetchArtwork(track, index, visitId) {
522  let cacheKey = getArtworkCacheKey(track, visitId);
523
524  if (artworkPromiseCache.has(cacheKey)) {
525    return artworkPromiseCache.get(cacheKey);
526  }
527
528  let promise = (async () => {
529  if (artwork_cache.has(cacheKey)) {
530    return artwork_cache.get(cacheKey);
531  }
532
533  let fallback = buildFallbackArtwork(track, index);
534
535  ensureUnsplashAccessKey();
536
537  if (!UNSPLASH_ACCESS_KEY) {
538    artwork_cache.set(cacheKey, fallback);
539    return fallback;
540  }
541
542  try {
543    for (const query of track.searchQueries) {
544      let searchUrl = new URL("https://api.unsplash.com/search/photos");
545      searchUrl.searchParams.set("query", query);
546      searchUrl.searchParams.set("orientation", "squarish");
547      searchUrl.searchParams.set("content_filter", "high");
548      searchUrl.searchParams.set("per_page", "10");
549      searchUrl.searchParams.set("client_id", UNSPLASH_ACCESS_KEY);
550
551      let response = await fetch(searchUrl.toString(), {
552        headers: {
553          "Accept-Version": "v1",
554        },
555      });
556
557      if (!response.ok) {
558        throw new Error(`Unsplash request failed: ${response.status}`);
559      }
560
561      let payload = await response.json();
562      let results = payload.results || [];
563      let normalizedPhoto = normalizeUnsplashPhoto(
564        results[Math.floor(Math.random() * results.length)],
565        fallback
566      );
567
568      if (!normalizedPhoto) {
569        let randomUrl = new URL("https://api.unsplash.com/photos/random");
570        randomUrl.searchParams.set("query", query);
571        randomUrl.searchParams.set("orientation", "squarish");
572        randomUrl.searchParams.set("content_filter", "high");
573        randomUrl.searchParams.set("client_id", UNSPLASH_ACCESS_KEY);
574
575        let randomResponse = await fetch(randomUrl.toString(), {
576          headers: {
577            "Accept-Version": "v1",
578          },
579        });
580
581        if (!randomResponse.ok) {
582          continue;
583        }
584
585        normalizedPhoto = normalizeUnsplashPhoto(
586          await randomResponse.json(),
587          fallback
588        );
589      }
590
591      if (!normalizedPhoto) {
592        continue;
593      }
594
595      let themeColor;
596
597      try {
598        themeColor = await sampleImageColor(normalizedPhoto.url);
599      } catch (error) {
600        themeColor = normalizedPhoto.fallbackThemeColor;
601      }
602
603      let artwork = {
604        url: normalizedPhoto.url,
605        themeColor,
606        attribution: normalizedPhoto.attribution,
607      };
608
609      artwork_cache.set(cacheKey, artwork);
610      trackUnsplashDownload(normalizedPhoto.downloadLocation);
611      return artwork;
612    }
613
614    artwork_cache.set(cacheKey, fallback);
615    return fallback;
616  } catch (error) {
617    console.warn("Artwork fetch failed, using fallback", error);
618    artwork_cache.set(cacheKey, fallback);
619    return fallback;
620  }
621
622  })();
623
624  artworkPromiseCache.set(cacheKey, promise);
625  return promise;
626}
627
628function applyArtworkObject(artwork) {
629  track_art.style.backgroundImage = `url("${artwork.url}")`;
630  track_art.title = artwork.attribution;
631  applyThemeColor(artwork.themeColor);
632}
633
634async function updateArtwork(track, index) {
635  let requestId = ++artworkRequestId;
636  let visitId = activeArtworkVisitId;
637  let cacheKey = getArtworkCacheKey(track, visitId);
638
639  if (artwork_cache.has(cacheKey)) {
640    applyArtworkObject(artwork_cache.get(cacheKey));
641    return;
642  }
643
644  let fallback = buildFallbackArtwork(track, index);
645
646  applyArtworkObject(fallback);
647
648  let artwork = await fetchArtwork(track, index, visitId);
649
650  if (
651    requestId !== artworkRequestId ||
652    index !== track_index ||
653    visitId !== activeArtworkVisitId
654  ) {
655    return;
656  }
657
658  applyArtworkObject(artwork);
659}
660
661function loadTrack(track_index, options = {}) {
662  clearInterval(updateTimer);
663  resetValues();
664  curr_track.src = encodeURI(track_list[track_index].path);
665  curr_track.load();
666
667  track_name.textContent = track_list[track_index].name;
668  track_artist.textContent = track_list[track_index].artist;
669  now_playing.textContent =
670    "PLAYING " + (track_index + 1) + " OF " + track_list.length;
671
672  updateTimer = setInterval(seekUpdate, 1000);
673  let visitCount = trackVisitCounts.get(track_list[track_index].path) || 0;
674  let nextVisitCount = options.usePreloadedArtwork
675    ? visitCount
676    : visitCount + 1;
677
678  trackVisitCounts.set(track_list[track_index].path, nextVisitCount);
679  activeArtworkVisitId = `visit-${nextVisitCount}`;
680  updateArtwork(track_list[track_index], track_index);
681}
682
683function resetValues() {
684  curr_time.textContent = "00:00";
685  total_duration.textContent = "00:00";
686  seek_slider.value = 0;
687}
688
689async function preloadAllArtwork() {
690  let loadedCount = 0;
691
692  await Promise.all(
693    track_list.map(async (track, index) => {
694      await fetchArtwork(track, index, "visit-0").catch((error) => {
695        console.warn("Artwork preload failed", error);
696      });
697      loadedCount += 1;
698      document.body.dataset.loadingStatus =
699        "Loading artwork... " + loadedCount + " / " + track_list.length;
700    })
701  );
702}
703
704async function initializePlayer() {
705  document.body.classList.remove("is-ready");
706  document.body.dataset.loadingStatus = "Loading artwork... 0 / " + track_list.length;
707  await preloadAllArtwork();
708  loadTrack(track_index, { usePreloadedArtwork: true });
709  curr_track.addEventListener("ended", nextTrack);
710  setVolume();
711  document.body.classList.add("is-ready");
712  playTrack();
713}
714
715initializePlayer();
716
717function playpauseTrack() {
718  if (!isPlaying) playTrack();
719  else pauseTrack();
720}
721
722function playTrack() {
723  curr_track
724    .play()
725    .then(() => {
726      isPlaying = true;
727      playpause_btn.innerHTML = '<i class="fa fa-pause-circle fa-5x"></i>';
728    })
729    .catch((error) => {
730      isPlaying = false;
731      playpause_btn.innerHTML = '<i class="fa fa-play-circle fa-5x"></i>';
732      console.warn("Autoplay was blocked by the browser", error);
733    });
734}
735
736function pauseTrack() {
737  curr_track.pause();
738  isPlaying = false;
739  playpause_btn.innerHTML = '<i class="fa fa-play-circle fa-5x"></i>';
740}
741
742function nextTrack() {
743  if (track_index < track_list.length - 1) track_index += 1;
744  else track_index = 0;
745  loadTrack(track_index);
746  playTrack();
747}
748
749function prevTrack() {
750  if (track_index > 0) track_index -= 1;
751  else track_index = track_list.length - 1;
752  loadTrack(track_index);
753  playTrack();
754}
755
756function seekTo() {
757  let seekto = curr_track.duration * (seek_slider.value / 100);
758  curr_track.currentTime = seekto;
759}
760
761function setVolume() {
762  curr_track.volume = volume_slider.value / 100;
763}
764
765function seekUpdate() {
766  let seekPosition = 0;
767
768  if (!isNaN(curr_track.duration)) {
769    seekPosition = curr_track.currentTime * (100 / curr_track.duration);
770
771    seek_slider.value = seekPosition;
772
773    let currentMinutes = Math.floor(curr_track.currentTime / 60);
774    let currentSeconds = Math.floor(
775      curr_track.currentTime - currentMinutes * 60
776    );
777    let durationMinutes = Math.floor(curr_track.duration / 60);
778    let durationSeconds = Math.floor(
779      curr_track.duration - durationMinutes * 60
780    );
781
782    if (currentSeconds < 10) currentSeconds = "0" + currentSeconds;
783    if (durationSeconds < 10) durationSeconds = "0" + durationSeconds;
784    if (currentMinutes < 10) currentMinutes = "0" + currentMinutes;
785    if (durationMinutes < 10) durationMinutes = "0" + durationMinutes;
786
787    curr_time.textContent = currentMinutes + ":" + currentSeconds;
788    total_duration.textContent = durationMinutes + ":" + durationSeconds;
789  }
790}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.