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.