1/*! 2 * Kast - Extraordinary SHOUTcast HTML5 Radio Player for jQuery - Material Design 3 * Copyrights: 2015-2016 Manar Kamel (manarkamel.github.io) 4 */ 5 6(function ($, window, document, undefined) { 7 8 // Plugin defaults 9 // ======================================================== // 10 var defaults = { 11 host: '', // Server IP or domain name @string 12 port: 80, // Server port @integer 13 protocol: 'http', // Server protocol @string 14 version: 2, // SHOUTcast Version @integer 15 sid: 1, // Server stream ID (If you have multiple SHOUTcast V2 stations) @integer 16 statsPath: 'stats', // Path to stats @string 17 playedPath: 'played', // Path to played @string 18 directStreamURL: false, // Add custom direct audio stream URL 19 ui: 'colored', // Transparent or colored and kast-nowplaying @string 20 theme: 'light', // Dark, light or dynamic: based on the current album artwork @string 21 colors: { 22 primary: 'cyan', // Material Design primary color - Material name @string 23 accent: 'yellow' // Material Design accent color - Material name @string 24 }, // set fixed colors or set 'dynamic': based on the current album artwork @object 25 startTemplate: 'maximized', // Start minimized or maximized on load @string 26 position: 'right', // Player position (Bottom right or left) @string 27 container: 'body', // Player container element, class or ID. everything other than 'body' isn't fixed @string 28 autoPlay: false, // Autoplay radio on load @boolean 29 autoUpdate: true, // Autoupdate (Boolean or string: 'all') (current info / Played info / album artwork) @boolean @string 30 artwork: true, // Pull album artwork (or artist artwork as a fallback) from Spotify API or set a custom one @boolean @array 31 statusBar: true, // Show status bar at the top @boolean 32 minimizeMaximize: true, // Show minimize/maximize button in status bar @boolean 33 muteUnmute: true, // Show mute/unmute button in status bar 34 startMuted: false, // Start audio muted on load @boolean
35 serverInfo: false, // Show serverInfo in status bar or set custom info @string @array @false 36 played: true, // Show played tracks/artists @boolean 37 currentTrack: true, // Show current track name, or set a custom track name @boolean @string 38 currentArtist: true, // Show the current artist name, or set a custom artist name @boolean @string 39 playedTracks: true, // Show the played track names @boolean 40 playedArtists: true, // Show the played artist names @boolean 41 offlineCheck: true, // On offline, show offline status @boolean 42 language: { 43 offlineText: 'Temporarily Offline', // On offline: status bar text @string 44 playedText: 'Played', // Played title (player bottom) @string 45 unknownTrackText: 'Unknown Track', // Set unknown track text when error 46 unknownArtistText: 'Unknown Artist' // Set unknown artist text when error 47 }, // Set text (Multi language support) @object 48 mobileCare: true, // low (aka true), medium, high, very high, ultra @boolean @array 49 irrelevantWords: ['feat.', 'ft.', 'Feat.', 'Ft.'], // irrelvant words in SHOUTcast full song title @false or @array 50 skipInfo: false, // Skip info (metadata) @false or @array of objects {track,artist} 51 defaultArtwork: false, // Set default album artwork shows when artwork not found. @false @array 52 customCheckerInterval: false, // Set custom stats checker interval @false @integer 53 popup: false, // Enable popup player @boolean 54 popupCSSLink: '', // set Kast CSS file for popup @string 55 popupWidth: 380, // Set popup width @integer 56 popupHeight: 570, // Set popup height @integer 57 overHTTPS: false, // Run SHOUTcast stream over an HTTPS proxy for HTTPS websites (experimental) (false) @boolean 58 direct: false, // Fetch SHOUTcast metadata direclty from the servers (Supported from >=2.2 DNAS via JSONP) (Note: non compatible with mixed content) 59 betaProxies: false, // Use Beta Proxies to fetch SHOUTcast metadata 60 continuous: true, // Enable continuous play (Play/Pause radio) 61 isMedia:false, 62 onReady: function () {}, // Kast event 63 onAudioLoad: function () {}, // Kast event 64 onMobile: function () {}, // Kast event 65 onPlay: function () {}, // Kast event 66 onPause: function () {}, // Kast event 67 onStop: function () {}, // Kast event 68 onMinimize: function () {}, // Kast event 69 onMaximize: function () {}, // Kast event 70 onMute: function () {}, // Kast event 71 onUnmute: function () {}, // Kast event 72 onUpdate: function () {}, // Kast event 73 onUpdateAll: function () {}, // Kast event 74 onOffline: function () {}, // Kast event 75 onOnline: function () {}, // Kast event 76 onCurrentArtwork: function () {}, // Kast event 77 onPlayedArtworks: function () {}, // Kast event 78 onCurrentInfo: function () {}, // Kast event 79 onPlayedInfo: function () {}, // Kast event 80 onDynamicColors: function () {}, // Kast event 81 onDynamicColorsContrast: function () {}, // Kast event 82 onDynamicTheme: function () {} // Kast event 83 }; 84 85 // Plugin constructor 86 // ======================================================== // 87 function Plugin(element, options) { 88 89 this.opt = $.extend({}, defaults, options); 90 91 // -- Validation 92 var HTML5Audio = function () { 93 var a = new Audio(); 94 return !!((a.canPlayType) && (a.canPlayType('audio/mpeg;').replace(/^no$/, '') || a.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '') || a.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, '') || a.canPlayType('audio/aac;').replace(/^no$/, ''))); 95 } 96 if (!this.opt.host || !HTML5Audio || !window.DOMParser || !document.implementation.createHTMLDocument) { // Essential features for Kast 97 $.data(window, 'plugin_kast', null) 98 99 throw Error("Kast couldn't load") 100 } 101 102 // -- Extend extra objects 103 if (options.language) { 104 this.opt.language = $.extend({}, defaults.language, options.language); 105 } 106 if (typeof options.colors === 'object') { 107 this.opt.colors = $.extend({}, defaults.colors, options.colors); 108 } 109 110 // -- Options correction (idiot-proof) 111 if (this.opt.startTemplate === 'minimized' && (!this.opt.statusBar || !this.opt.minimizeMaximize)) { 112 this.opt.played = false;
113 } 114 115 116 var iot = this.opt; 117 118 // -- Protocol 119 var protocol = (document.location.protocol === 'https:') ? 'https' : 'http'; // Document not window for wider browser support 120 121 // -- Protocol correction 122 // ** Force protocol 'https' to be compatiable with Direct metadata, 123 // ** if SHOUTcast doesn't support HTTPS protocol and client website was secured, 124 // ** then direct metadata won't work (mixed content). 125 // ** anyway i'm forcing HTTPS protocol, if user didn't set it 126 if (iot.direct && protocol === 'https') { 127 iot.protocol = protocol 128 } 129 130 // -- Host 131 //this.host = iot.protocol + '://' + iot.host + ':' + iot.port + '/'; 132 //this.host = iot.protocol + '://' + iot.host; 133 this.host = iot.host; 134 135 this.hostCORS = [ 136 protocol + '://crossorigin.me/' + this.host, 137 'https://cors-anywhere.herokuapp.com/' + this.host, 138 protocol + '://kastproxy-us.herokuapp.com/' + this.host, 139 protocol + '://kastproxy-eu.herokuapp.com/' + this.host 140 ]; 141 142 //this.hostCORS = [ 143 // protocol + '://kastdemo-us.herokuapp.com/' + this.host, 144 // protocol + '://kastdemo-eu.herokuapp.com/' + this.host 145 //]; 146 147 if (iot.overHTTPS && protocol === 'https:') { 148 this.host = this.hostCORS[0]; 149 } 150 151 152 // -- Audio 153 var sc2sid = iot.sid, 154 //audioSrc = iot.directStreamURL || ((sc2sid > 1) ? this.host + 'stream/' + sc2sid + '/;' : this.host + ';'); 155 audioSrc = iot.directStreamURL || ((sc2sid > 1) ? this.host + 'stream/' + sc2sid : this.host); 156 157 this.audio = document.createElement('audio'); 158 //this.audio.src = audioSrc + '?_=' + Math.random(); 159 this.audio.src = audioSrc; 160 this.audio.id = 'AudioPlayer'; 161 //if(this.opt.isMedia == true) { 162 this.audio.preload = 'none'; 163 //} 164 this.audio.load(); 165 166 // -- Event: on audio load 167 iot.onAudioLoad(this.audio) 168 169 // -- Mobile Care 170 this.mobile = false; 171 this.mobileMedium = false; 172 //this.mobileHigh = false; 173 //this.mobileVeryHigh = false; 174 this.mobileUltra = false; 175 var mC = iot.mobileCare, 176 mCMW = mC[1] || '599px'; 177 178 179 if (mC && window.matchMedia && window.matchMedia('(max-width: ' + mCMW + ')').matches) { 180 this.mobile = true; 181 182 iot.autoPlay = false; 183 iot.startTemplate = 'minimized'; 184 iot.offlineCheck = false; 185 186 // -- Inside plugin 187 // Checker Interval 12s 188 189 if (mC[0] === 'medium' || mC[0] === 'high' || mC[0] === 'very high' || mC[0] === 'ultra') { 190 this.mobileMedium = true; 191 if (iot.autoUpdate === 'all') { 192 iot.autoUpdate = true 193 } 194 if (iot.theme === 'dynamic') { 195 iot.theme = 'light' 196 } 197 if (iot.colors === 'dynamic') { 198 iot.colors = false; 199 } 200 // -- Inside plugin 201 // Medium quality artwork images 202 // Checker Interval 16s 203 } 204 if (mC[0] === 'high' || mC[0] === 'very high' || mC[0] === 'ultra') { 205 iot.artwork = iot.played = false; 206 } 207 if (mC[0] === 'very high' || mC[0] === 'ultra') { 208 iot.autoUpdate = iot.minimizeMaximize = false; 209 } 210 if (mC[0] === 'ultra') { 211 this.mobileUltra = true; 212 iot.statusBar = iot.serverInfo = iot.currentArtist = iot.currentTrack = false; 213 } 214 215 iot.onMobile() // Event 216 } 217 218 219 // -- Initialize 220 this.init(); 221 222 223 } 224 225 // Return Hour Seconde 226 // ======================================================== // 227 var getDurationHours = function(totalSeconds) { 228 totalSeconds = Math.round(totalSeconds); 229 hours = Math.floor(totalSeconds / 3600); 230 totalSeconds %= 3600; 231 minutes = Math.floor(totalSeconds / 60); 232 seconds = totalSeconds % 60; 233 return (hours > 0) ? hours + ':' + minutes + ':' + seconds : minutes + ':' + seconds; 234 }; 235 236 // Plugin dependencies, helpers and feature detection 237 // ======================================================== // 238 /* 239 * Color Thief v2.0 - Customized for Kast needs only 240 * by Lokesh Dhakar - http://www.lokeshdhakar.com 241 * 242 * Thanks 243 * ------ 244 * Nick Rabinowitz - For creating quantize.js. 245 * John Schulz - For clean up and optimization. @JFSIII 246 * Nathan Spady - For adding drag and drop support to the demo page. 247 * 248 * License 249 * ------- 250 * Copyright 2011, 2015 Lokesh Dhakar 251 * Released under the MIT license 252 * https://raw.githubusercontent.com/lokesh/color-thief/master/LICENSE 253 * 254 * Note: Lokesh's comments are removed here 255 */ 256 257 var CanvasImage = function (image) { 258 this.canvas = document.createElement('canvas'); 259 this.context = this.canvas.getContext('2d'); 260 261 document.body.appendChild(this.canvas); 262 263 this.width = this.canvas.width = image.width; 264 this.height = this.canvas.height = image.height; 265 266 this.context.drawImage(image, 0, 0, this.width, this.height); 267 }, 268 ColorThief = function () {}; 269 270 CanvasImage.prototype = { 271 clear: function () { 272 this.context.clearRect(0, 0, this.width, this.height); 273 }, 274 update: function (imageData) { 275 this.context.putImageData(imageData, 0, 0); 276 }, 277 getPixelCount: function () { 278 return this.width * this.height; 279 }, 280 getImageData: function () { 281 return this.context.getImageData(0, 0, this.width, this.height); 282 }, 283 removeCanvas: function () { 284 this.canvas.parentNode.removeChild(this.canvas); 285 } 286 } 287 ColorThief.prototype.getPalette = function (sourceImage, colorCount) { 288 289 // Create custom CanvasImage object 290 var image = new CanvasImage(sourceImage); 291 var imageData = image.getImageData(); 292 var pixels = imageData.data; 293 var pixelCount = image.getPixelCount(); 294 295 // Store the RGB values in an array format suitable for quantize function 296 var pixelArray = []; 297 for (var i = 0, offset, r, g, b, a; i < pixelCount; i = i + 10) { // ** 10 is quality 298 offset = i * 4; 299 r = pixels[offset + 0]; 300 g = pixels[offset + 1]; 301 b = pixels[offset + 2]; 302 a = pixels[offset + 3]; 303 // If pixel is mostly opaque and not white 304 if (a >= 125) { 305 if (!(r > 250 && g > 250 && b > 250)) { 306 pixelArray.push([r, g, b]); 307 } 308 } 309 } 310 311 // Send array to quantize function which clusters values 312 // using median cut algorithm 313 var cmap = MMCQ.quantize(pixelArray, colorCount); 314 var palette = cmap ? cmap.palette() : null; 315 316 // Clean up 317 image.removeCanvas(); 318 319 return palette; 320 }; 321 322 if (!pv) { 323 var pv = { 324 map: function (array, f) { 325 var o = {}; 326 return f ? array.map(function (d, i) { 327 o.index = i; 328 return f.call(o, d); 329 }) : array.slice(); 330 }, 331 naturalOrder: function (a, b) { 332 return (a < b) ? -1 : ((a > b) ? 1 : 0); 333 }, 334 sum: function (array, f) { 335 var o = {}; 336 return array.reduce(f ? function (p, d, i) { 337 o.index = i; 338 return p + f.call(o, d); 339 } : function (p, d) { 340 return p + d; 341 }, 0); 342 }, 343 max: function (array, f) { 344 return Math.max.apply(null, f ? pv.map(array, f) : array); 345 } 346 }; 347 } 348 var MMCQ = (function () { 349 // private constants 350 var sigbits = 5, 351 rshift = 8 - sigbits, 352 maxIterations = 1000, 353 fractByPopulations = 0.75; 354 355 // get reduced-space color index for a pixel 356 function getColorIndex(r, g, b) { 357 return (r << (2 * sigbits)) + (g << sigbits) + b; 358 } 359 360 // Simple priority queue 361 function PQueue(comparator) { 362 var contents = [], 363 sorted = false;
364 365 function sort() { 366 contents.sort(comparator); 367 sorted = true; 368 } 369 370 return { 371 push: function (o) { 372 contents.push(o); 373 sorted = false; 374 }, 375 peek: function (index) { 376 if (!sorted) sort(); 377 if (index === undefined) index = contents.length - 1; 378 return contents[index]; 379 }, 380 pop: function () { 381 if (!sorted) sort(); 382 return contents.pop(); 383 }, 384 size: function () { 385 return contents.length; 386 }, 387 map: function (f) { 388 return contents.map(f); 389 }, 390 debug: function () { 391 if (!sorted) sort(); 392 return contents; 393 } 394 }; 395 } 396 397 // 3d color space box 398 function VBox(r1, r2, g1, g2, b1, b2, histo) { 399 var vbox = this; 400 vbox.r1 = r1; 401 vbox.r2 = r2; 402 vbox.g1 = g1; 403 vbox.g2 = g2; 404 vbox.b1 = b1; 405 vbox.b2 = b2; 406 vbox.histo = histo; 407 } 408 VBox.prototype = { 409 volume: function (force) { 410 var vbox = this; 411 if (!vbox._volume || force) { 412 vbox._volume = ((vbox.r2 - vbox.r1 + 1) * (vbox.g2 - vbox.g1 + 1) * (vbox.b2 - vbox.b1 + 1)); 413 } 414 return vbox._volume; 415 }, 416 count: function (force) { 417 var vbox = this, 418 histo = vbox.histo; 419 if (!vbox._count_set || force) { 420 var npix = 0, 421 i, j, k; 422 for (i = vbox.r1; i <= vbox.r2; i++) { 423 for (j = vbox.g1; j <= vbox.g2; j++) { 424 for (k = vbox.b1; k <= vbox.b2; k++) { 425 index = getColorIndex(i, j, k); 426 npix += (histo[index] || 0); 427 } 428 } 429 } 430 vbox._count = npix; 431 vbox._count_set = true; 432 } 433 return vbox._count; 434 }, 435 copy: function () { 436 var vbox = this; 437 return new VBox(vbox.r1, vbox.r2, vbox.g1, vbox.g2, vbox.b1, vbox.b2, vbox.histo); 438 }, 439 avg: function (force) { 440 var vbox = this, 441 histo = vbox.histo; 442 if (!vbox._avg || force) { 443 var ntot = 0, 444 mult = 1 << (8 - sigbits), 445 rsum = 0, 446 gsum = 0, 447 bsum = 0, 448 hval, 449 i, j, k, histoindex; 450 for (i = vbox.r1; i <= vbox.r2; i++) { 451 for (j = vbox.g1; j <= vbox.g2; j++) { 452 for (k = vbox.b1; k <= vbox.b2; k++) { 453 histoindex = getColorIndex(i, j, k); 454 hval = histo[histoindex] || 0; 455 ntot += hval; 456 rsum += (hval * (i + 0.5) * mult); 457 gsum += (hval * (j + 0.5) * mult); 458 bsum += (hval * (k + 0.5) * mult); 459 } 460 } 461 } 462 if (ntot) { 463 vbox._avg = [~~(rsum / ntot), ~~(gsum / ntot), ~~(bsum / ntot)]; 464 } else { 465 // console.log('empty box'); 466 vbox._avg = [ 467 ~~(mult * (vbox.r1 + vbox.r2 + 1) / 2), 468 ~~(mult * (vbox.g1 + vbox.g2 + 1) / 2), 469 ~~(mult * (vbox.b1 + vbox.b2 + 1) / 2) 470 ]; 471 } 472 } 473 return vbox._avg; 474 }, 475 contains: function (pixel) { 476 var vbox = this, 477 rval = pixel[0] >> rshift; 478 gval = pixel[1] >> rshift; 479 bval = pixel[2] >> rshift; 480 return (rval >= vbox.r1 && rval <= vbox.r2 && 481 gval >= vbox.g1 && gval <= vbox.g2 && 482 bval >= vbox.b1 && bval <= vbox.b2); 483 } 484 }; 485 486 // Color map 487 function CMap() { 488 this.vboxes = new PQueue(function (a, b) { 489 return pv.naturalOrder(
490 a.vbox.count() * a.vbox.volume(), 491 b.vbox.count() * b.vbox.volume() 492 ); 493 }); 494 } 495 CMap.prototype = { 496 push: function (vbox) { 497 this.vboxes.push({ 498 vbox: vbox, 499 color: vbox.avg() 500 }); 501 }, 502 palette: function () { 503 return this.vboxes.map(function (vb) { 504 return vb.color; 505 }); 506 }, 507 size: function () { 508 return this.vboxes.size(); 509 }, 510 map: function (color) { 511 var vboxes = this.vboxes; 512 for (var i = 0; i < vboxes.size(); i++) { 513 if (vboxes.peek(i).vbox.contains(color)) { 514 return vboxes.peek(i).color; 515 } 516 } 517 return this.nearest(color); 518 }, 519 nearest: function (color) { 520 var vboxes = this.vboxes, 521 d1, d2, pColor; 522 for (var i = 0; i < vboxes.size(); i++) { 523 d2 = Math.sqrt( 524 Math.pow(color[0] - vboxes.peek(i).color[0], 2) + 525 Math.pow(color[1] - vboxes.peek(i).color[1], 2) + 526 Math.pow(color[2] - vboxes.peek(i).color[2], 2) 527 ); 528 if (d2 < d1 || d1 === undefined) { 529 d1 = d2; 530 pColor = vboxes.peek(i).color; 531 } 532 } 533 return pColor; 534 }, 535 forcebw: function () { 536 // XXX: won't work yet 537 var vboxes = this.vboxes; 538 vboxes.sort(function (a, b) { 539 return pv.naturalOrder(pv.sum(a.color), pv.sum(b.color)); 540 }); 541 542 // force darkest color to black if everything < 5 543 var lowest = vboxes[0].color; 544 if (lowest[0] < 5 && lowest[1] < 5 && lowest[2] < 5) 545 vboxes[0].color = [0, 0, 0]; 546 547 // force lightest color to white if everything > 251 548 var idx = vboxes.length - 1, 549 highest = vboxes[idx].color; 550 if (highest[0] > 251 && highest[1] > 251 && highest[2] > 251) 551 vboxes[idx].color = [255, 255, 255]; 552 } 553 }; 554 555 // histo (1-d array, giving the number of pixels in 556 // each quantized region of color space), or null on error 557 function getHisto(pixels) { 558 var histosize = 1 << (3 * sigbits), 559 histo = new Array(histosize), 560 index, rval, gval, bval; 561 pixels.forEach(function (pixel) { 562 rval = pixel[0] >> rshift; 563 gval = pixel[1] >> rshift; 564 bval = pixel[2] >> rshift; 565 index = getColorIndex(rval, gval, bval); 566 histo[index] = (histo[index] || 0) + 1; 567 }); 568 return histo; 569 } 570 571 function vboxFromPixels(pixels, histo) { 572 var rmin = 1000000, 573 rmax = 0, 574 gmin = 1000000, 575 gmax = 0, 576 bmin = 1000000, 577 bmax = 0, 578 rval, gval, bval; 579 // find min/max 580 pixels.forEach(function (pixel) { 581 rval = pixel[0] >> rshift; 582 gval = pixel[1] >> rshift; 583 bval = pixel[2] >> rshift; 584 if (rval < rmin) rmin = rval; 585 else if (rval > rmax) rmax = rval; 586 if (gval < gmin) gmin = gval; 587 else if (gval > gmax) gmax = gval; 588 if (bval < bmin) bmin = bval; 589 else if (bval > bmax) bmax = bval; 590 }); 591 return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo); 592 } 593 594 function medianCutApply(histo, vbox) { 595 if (!vbox.count()) return; 596 597 var rw = vbox.r2 - vbox.r1 + 1, 598 gw = vbox.g2 - vbox.g1 + 1, 599 bw = vbox.b2 - vbox.b1 + 1, 600 maxw = pv.max([rw, gw, bw]); 601 // only one pixel, no split 602 if (vbox.count() == 1) { 603 return [vbox.copy()]; 604 } 605 /* Find the partial sum arrays along the selected axis. */ 606 var total = 0, 607 partialsum = [], 608 lookaheadsum = [], 609 i, j, k, sum, index; 610 if (maxw == rw) { 611 for (i = vbox.r1; i <= vbox.r2; i++) { 612 sum = 0; 613 for (j = vbox.g1; j <= vbox.g2; j++) { 614 for (k = vbox.b1; k <= vbox.b2; k++) { 615 index = getColorIndex(i, j, k); 616 sum += (histo[index] || 0); 617 } 618 } 619 total += sum; 620 partialsum[i] = total; 621 } 622 } else if (maxw == gw) { 623 for (i = vbox.g1; i <= vbox.g2; i++) { 624 sum = 0; 625 for (j = vbox.r1; j <= vbox.r2; j++) { 626 for (k = vbox.b1; k <= vbox.b2; k++) { 627 index = getColorIndex(j, i, k); 628 sum += (histo[index] || 0); 629 } 630 } 631 total += sum; 632 partialsum[i] = total; 633 } 634 } else { /* maxw == bw */ 635 for (i = vbox.b1; i <= vbox.b2; i++) { 636 sum = 0; 637 for (j = vbox.r1; j <= vbox.r2; j++) { 638 for (k = vbox.g1; k <= vbox.g2; k++) { 639 index = getColorIndex(j, k, i); 640 sum += (histo[index] || 0); 641 } 642 } 643 total += sum; 644 partialsum[i] = total; 645 } 646 } 647 partialsum.forEach(function (d, i) { 648 lookaheadsum[i] = total - d; 649 }); 650 651 function doCut(color) { 652 var dim1 = color + '1', 653 dim2 = color + '2', 654 left, right, vbox1, vbox2, d2, count2 = 0; 655 for (i = vbox[dim1]; i <= vbox[dim2]; i++) { 656 if (partialsum[i] > total / 2) { 657 vbox1 = vbox.copy(); 658 vbox2 = vbox.copy(); 659 left = i - vbox[dim1]; 660 right = vbox[dim2] - i; 661 if (left <= right) 662 d2 = Math.min(vbox[dim2] - 1, ~~(i + right / 2)); 663 else d2 = Math.max(vbox[dim1], ~~(i - 1 - left / 2)); 664 // avoid 0-count boxes 665 while (!partialsum[d2]) d2++; 666 count2 = lookaheadsum[d2]; 667 while (!count2 && partialsum[d2 - 1]) count2 = lookaheadsum[--d2]; 668 // set dimensions 669 vbox1[dim2] = d2; 670 vbox2[dim1] = vbox1[dim2] + 1; 671 // console.log('vbox counts:', vbox.c
671ount(), vbox1.count(), vbox2.count()); 672 return [vbox1, vbox2]; 673 } 674 } 675 676 } 677 // determine the cut planes 678 return maxw == rw ? doCut('r') : 679 maxw == gw ? doCut('g') : 680 doCut('b'); 681 } 682 683 function quantize(pixels, maxcolors) { 684 // short-circuit 685 if (!pixels.length || maxcolors < 2 || maxcolors > 256) { 686 // console.log('wrong number of maxcolors'); 687 return false; 688 } 689 690 // XXX: check color content and convert to grayscale if insufficient 691 692 var histo = getHisto(pixels), 693 histosize = 1 << (3 * sigbits); 694 695 // check that we aren't below maxcolors already 696 var nColors = 0; 697 histo.forEach(function () { 698 nColors++; 699 }); 700 if (nColors <= maxcolors) { 701 // XXX: generate the new colors from the histo and return 702 } 703 704 // get the beginning vbox from the colors 705 var vbox = vboxFromPixels(pixels, histo), 706 pq = new PQueue(function (a, b) { 707 return pv.naturalOrder(a.count(), b.count()); 708 }); 709 pq.push(vbox); 710 711 // inner function to do the iteration 712 function iter(lh, target) { 713 var ncolors = 1, 714 niters = 0, 715 vbox; 716 while (niters < maxIterations) { 717 vbox = lh.pop(); 718 if (!vbox.count()) { /* just put it back */ 719 lh.push(vbox); 720 niters++; 721 continue; 722 } 723 // do the cut 724 var vboxes = medianCutApply(histo, vbox), 725 vbox1 = vboxes[0], 726 vbox2 = vboxes[1]; 727 728 if (!vbox1) { 729 // console.log("vbox1 not defined; shouldn't happen!"); 730 return; 731 } 732 lh.push(vbox1); 733 if (vbox2) { /* vbox2 can be null */ 734 lh.push(vbox2); 735 ncolors++; 736 } 737 if (ncolors >= target) return; 738 if (niters++ > maxIterations) { 739 // console.log("infinite loop; perhaps too few pixels!"); 740 return; 741 } 742 } 743 } 744 745 // first set of colors, sorted by population 746 iter(pq, fractByPopulations * maxcolors); 747 748 // Re-sort by the product of pixel occupancy times the size in color space. 749 var pq2 = new PQueue(function (a, b) { 750 return pv.naturalOrder(a.count() * a.volume(), b.count() * b.volume()); 751 }); 752 while (pq.size()) { 753 pq2.push(pq.pop()); 754 } 755 756 // next set - generate the median cuts using the (npix * vol) sorting. 757 iter(pq2, maxcolors - pq2.size()); 758 759 // calculate the actual colors 760 var cmap = new CMap(); 761 while (pq2.size()) { 762 cmap.push(pq2.pop()); 763 } 764 765 return cmap; 766 } 767 768 return { 769 quantize: quantize 770 }; 771 })(); 772 773 /* 774 * getColorContrast - Customized for Material Design and RGB usage only 775 * Return suggested contrast color (dark or light) for the color (rgba/rgb) given. 776 * Takes advantage of YIQ: https://en.wikipedia.org/wiki/YIQ 777 * dark = background is light, use dark colors for text, images, etc.. 778 * light = background is dark, use light colors for text, images, etc.. 779 * Inspired by: http://24ways.org/2010/calculating-color-contrast/ 780 * 781 * @param color string A valid rgb value 782 * @return string dark|light 783 * @copyright (c) 2015 Julian Richen 784 * @license Licensed under MIT License https://raw.githubusercontent.com/julianrichen/javascript-dynamic-contrast/master/LICENSE 785 * 786 */ 787 788 var getColorContrast = function (color) { 789 if (!color) { 790 return null; 791 } 792 var rgbExp = /^rgba?[\s+]?\(\s*(([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5]))\s*,\s*([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\s*,\s*([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\s*,?(?:\s*([\d.]+))?\s*\)?\s*/im, 793 rgb = color.match(rgbExp), 794 r, 795 g, 796 b, 797 yiq; 798 if (rgb) { 799 r = parseInt(rgb[1], 10); 800 g = parseInt(rgb[2], 10); 801 b = parseInt(rgb[3], 10); 802 } else { 803 return null; 804 } 805 yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000; 806 807 // 128~150 and not 128, based on Material Design colors 808 // Unfortunately there's no specific/right number 809 return (yiq >= 146) ? 'dark' : 'light'; 810 811 } 812 813 // -- -- Initialize color thief once 814 var colorThiefNew = new ColorThief(); 815 816 817 818 /* 819 * Helpers 820 */ 821 822 // -- -- Process Server Info
823 // ** ** Will be called in init() and autoUpdate() 824 var processServerInfo = function (ot, sI, data, KA) { 825 var sIL = sI.length, 826 sIFrag = document.createDocumentFragment(), 827 serverInfo; 828 for (var i = 0; i < sIL; i++) { 829 if (ot.direct) { 830 serverInfo = data[sI[i]]; 831 } else if (!data.body) { 832 serverInfo = data.getElementsByTagName(sI[i].toUpperCase())[0].textContent; 833 } else { 834 var serverInfoV1 = data.body.textContent.split(','); // Data here are not XML 835 836 if (sI[i] === 'currentlisteners') { 837 serverInfo = serverInfoV1[0] 838 } else if (sI[i] === 'streamstatus') { 839 serverInfo = serverInfoV1[1] 840 } else if (sI[i] === 'peaklisteners') { 841 serverInfo = serverInfoV1[2] 842 } else if (sI[i] === 'maxlisteners') { 843 serverInfo = serverInfoV1[3] 844 } else if (sI[i] === 'uniquelisteners') { 845 serverInfo = serverInfoV1[4] 846 } else if (sI[i] === 'bitrate') { 847 serverInfo = serverInfoV1[5] 848 } else if (sI[i] === 'songtitle') { 849 serverInfo = serverInfoV1[6] 850 } 851 } 852 853 if (i === sIL - 1) { 854 sIFrag.appendChild(document.createTextNode(serverInfo)); 855 } else { 856 sIFrag.appendChild(document.createTextNode(serverInfo + ' - ')); 857 } 858 } 859 860 KA.appendChild(sIFrag); // Now append to DOM at last, like a BOSS 861 } 862 863 864 // -- Custom smart Cross-browser ajax request designed for SHOUTcast servers 865 // -- options @object: url, timeout, arraybuffer, load, error 866 var XHR = function (op) { 867 var req; 868 req = (window.XMLHttpRequest) ? new XMLHttpRequest() : new XDomainRequest(); 869 870 try { // ** for IE (Access is denied) *bug in proxies 871 req.open('GET', op.url, true); 872 873 if (op.arraybuffer) { 874 if (typeof req.responseType === 'string') { // XHR2 check first 875 req.responseType = 'arraybuffer'; 876 } else { 877 if (op.error) { 878 op.error() 879 } 880 return // kill request 881 } 882 883 } 884 885 req.timeout = op.timeout || 3000; 886 } catch (e) { 887 if (op.error) { 888 op.error() 889 } 890 return // kill request 891 } 892 893 req.onload = function () { 894 895 if (req.readyState === 4) { 896 897 if (req.status === 200) { // For safety! According to Mozilla docs 898 var data = (op.arraybuffer) ? req.response : req.responseText; 899 op.load(data, req) 900 } else { 901 if (op.error) { 902 op.error() 903 } 904 } 905 906 } 907 908 } 909 910 if (op.error) { 911 req.onerror = op.error 912 } 913 914 915 req.ontimeout = req.onerror; 916 917 setTimeout(function () { // ** for IE9 (Multi requests) 918 req.send(); 919 }, 0); 920 921 // Global it, so I could abort any active requests when I destroy Kast 922 window.kastXHR = req 923 } 924 925 // -- Remove brackets + Remove invaild characters 926 var chaRe = function (strie) { 927 return strie.replace(/(\[.*?\]|\(.*?\))/g, '').replace(/[|&^'`;:$%@"#<>+,]/g, '') 928 }; 929 930 // -- Create HTML Document 931 function createHTMLDocument(str) { 932 var doc = document.implementation.createHTMLDocument('html'); 933 doc.documentElement.innerHTML = str; 934 return doc 935 } 936 937 938 /* 939 * Feature Detection (ONCE) 940 */ 941 942 // -- -- Document clasList support (Extremely fast and easy Add/Remove/Toggle classes) 943 var classL = document.documentElement.classList; 944 945 946 var _popup; 947 var _document; 948 949 950 951 // Plugin prototypes 952 // ======================================================== // 953 Plugin.prototype = { 954 init: function () { 955 // -- load options (theme, colors, etc...) 956 // -- Place player elements here 957 // -- Call prototypes 958 959 // -- that 960 var that = this, 961 ot = that.opt; 962
963 // -- Cache container 964 if (ot.popup) { 965 _popup = window.open('','kast','width='+ot.popupWidth+',height='+ot.popupHeight+',toolbar=0,menubar=0,location=0,directories=0,status=0,scrollbars=0,resizable=1,left=0,top=0'); 966 967 // Redefine and write document. 968 document = _popup.document; 969 document.write('<html><head><title>Kast</title><link href="'+ot.popupCSSLink+'" rel="stylesheet" type="text/css"></head><body></body></html>'); 970 971 _popup.onbeforeunload = function(){ 972 $.kast('destroy'); 973 } 974 975 ot.container = 'body'; 976 } 977 978 var docContainer = document.querySelector(ot.container); 979 980 // -- Some classes 981 var clrs = ot.colors, 982 primaryColor = (typeof clrs === 'object') ? 'kast-primary-' + clrs.primary.replace(' ', '') : '', // Primary color 983 accentColor = (typeof clrs === 'object') ? 'kast-accent-' + clrs.accent.replace(' ', '') : '', // Accent color 984 dynamicColors = (typeof clrs === 'string') ? 'kast-colors-' + clrs : '', // Dynamic colors 985 ultra = (that.mobile && that.mobileUltra) ? 'kast-ultra' : '', // Mobile ultra mode 986 notFullBar = (!ot.statusBar || !ot.muteUnmute || !ot.minimizeMaximize) ? 'kast-not-full-statusbar' : '', // So i could make text longer 987 played = (ot.played) ? ' kast-played ' : ' kast-current ', // Played or current 988 container = (ot.container !== 'body') ? 'kast-custom-container' : '', // Custom container (Not sticky if not body) 989 thin = (docContainer.offsetWidth < 380) ? 'kast-thin-container' : '', // Thin container just to fix scrollbar in CSS 990 dynamicTheme = (ot.theme === 'dynamic') ? 'kast-light' : '', // light theme if dynamic 991 statusBarHtml = '', // Status bar HTML 992 playedHtml = ''; 993 994 // -- Status bar HTML 995 if (ot.statusBar) { 996 var muteUnmuteHtml = (ot.muteUnmute) ? '<i id="kast-volume" class="kast-mdi kast-mdi-volume-high"></i>' : '', 997 minMaxHtml = (ot.minimizeMaximize) ? '<i id="kast-minmax" class="kast-mdi kast-mdi-chevron-down"></i>' : ''; 998 999 statusBarHtml = '<div id="kast-bar"><p class="kast-offline-status">' + ot.language.offlineText + '</p><p id="kast-server"></p>' + muteUnmuteHtml + minMaxHtml + '</div>'; 1000 } 1001 1002 // -- Played/Bottom HTML 1003 if (ot.played) { 1004 playedHtml = '<div id="kast-bottom" class="kast-clearfix"><p>' + ot.language.playedText + '</p><ul id="kast-playedlist" class="kast-clearfix"></ul></div>'; 1005 } 1006 1007 var kastPlayClass = ot.continuous ? 'kast-paused kast-continuous ' : 'kast-stopped '; 1008 1009 // -- Kast HTML 1010 var html = ''; 1011 html += '<div id="kast" class="kast-recss kast-' + ot.theme + ' kast-' + ot.ui + ' ' + primaryColor + ' ' + accentColor + ' ' + dynamicColors + ' kast-' + ot.startTemplate + ' kast-' + ot.position + ' ' + ultra + ' ' + notFullBar + played + ' ' + container + ' ' + dynamicTheme + ' ' + thin + '">' 1012 html += '<div id="kast-wrapper">'; 1013 html += statusBarHtml; // isn't better than appending it later ? 1014 html += '<div id="kast-top" class="kast-default-art">'; 1015 html += '<div class="kast-album-wrapper"><i class="kast-mdi kast-mdi-album"></i></div>'; 1016 html += '<div id="kast-top-content" class="kast-top-content">'; 1017 html += '<div id="kast-nowplaying"><p id="kast-np-title"></p><p id="kast-np-artist"></p></div>'; 1018 html += '<div id="kast-linear"></div>'; 1019 html += '</div>'; 1020 html += '<div id="kast-play" class="' + kastPlayClass + '">'; 1021 html += '<i class="kast-mdi kast-mdi-play"></i>'; 1022 html += '<i class="kast-mdi kast-mdi-pause"></i>'; 1023 html += '<i class="kast-mdi kast-mdi-stop"></i>'; 1024 html += '</div></div>' + playedHtml + '</div></div>'; 1025 1026 1027 docContainer.insertAdjacentHTML("beforeend", html); // DOM access: Insert last 1028 ot.onReady(); // Event 1029 1030 1031 // -- Status bar, CurrentTrack, CurrentArtist 1032 if (ot.statusBar || ot.currentTrack || ot.currentArtist) { 1033 1034 // -- -- Stats (CurrentTrack, CurrentArtist) 1035 this.stats(function (data) { 1036 1037 // -- Set Server Info once 1038 var sI = ot.serverInfo; 1039 1040 if (sI && ot.statusBar) { 1041 var kastServer = document.getElementById('kast-server'); 1042 if (typeof sI === 'string') { // Custom Server info 1043 kastServer.textContent = sI; 1044 } else { 1045 processServerInfo(ot, sI, data, kastServer) 1046 } 1047 } 1048 1049 // -- Passing data 1050 // ** that.stats(data) --> processStats(data) #Without_repulling 1051 that.stats(data); 1052 1053 }); 1054 1055 } 1056 1057 // -- Played 1058 if (ot.played) { 1059 1060 // -- Set played tracks/artists 1061 if (Array.isArray && Array.isArray(ot.artwork)) { 1062 that.played(false, true) 1063 } else { 1064 that.played() 1065 } 1066 1067 } 1068 1069 // -- Auto Update 1070 var aU = ot.autoUpdate; 1071 if (aU === 'all') { 1072 that.autoUpdate('all'); 1073 } else if (aU) { 1074 that.autoUpdate(true); 1075 } 1076 1077 1078 1079 1080 // -- Binding (addEventListener isn't necessary, beside onclick is extremely fast) 1081 document.getElementById('kast-play').onclick = function () { 1082 if (this.className.indexOf('playing') === -1) { 1083 that.play(!ot.continuous) // ot.continuous for reload 1084 } else { 1085 ot.continuous ? that.pause() : that.stop() 1086 } 1087 }; 1088 1089 if (ot.statusBar) { 1090 1091 if (ot.muteUnmute) { 1092 document.getElementById('kast-volume').onclick = function () { 1093 if (this.className.indexOf('high') !== -1) { 1094 that.mute() 1095 } else { 1096 that.unmute() 1097 } 1098 }; 1099 } 1100 1101 if (ot.minimizeMaximize) { 1102 document.getElementById('kast-minmax').onclick = function () { 1103 if (document.getElementById('kast').className.indexOf('minimized') !== -1) { 1104 that.maximize() 1105 } else { 1106 that.minimize() 1107 } 1108 }; 1109 } 1110 1111 } 1112 1113 1114 // -- Finally 1115 ot.autoPlay && that.play(); 1116 ot.startMuted && that.mute(); 1117 1118 1119 1120 return this; 1121 }, 1122 play: function (reload) { 1123 // -- Play the radio 1124 // -- addClass/removeClass 1125 1126 var k = document.getElementById('kast'), 1127 kP = document.getElementById('kast-play'); 1128 if (classL) { 1129 var kPC = kP.classList; 1130 kPC.remove('kast-paused') 1131 kPC.remove('kast-stopped') 1132 kPC.add('kast-playing') 1133 } else { 1134 $(kP).removeClass('kast-paused kast-stopped').addClass('kast-playing') 1135 } 1136 1137 var wasOffline = (this.opt.offlineCheck && k.getAttribute('data-offline') === 'false'); 1138 1139 // ** Safe Play() for dumb browsers: I force reload the audio, if was Offline, and back online again 1140 // ** + Continuous 1141 if (reload || wasOffline) { 1142 this.audio.load(); 1143 wasOffline && k.removeAttribute('data-offline') // here we remove it not set it (important) 1144 } 1145 1146 this.audio.play(); 1147 this.opt.onPlay(this.audio) // Event 1148 1149 // DURATION 1150 // BNJ AddOn
1151 // Play Live at the end 1152 if(this.opt.isMedia == true) { 1153 1154 document.getElementsByTagName('body')[0].appendChild(this.audio); 1155 1156 var AudioPlayer = document.getElementById('AudioPlayer'); 1157 1158 //var Duration = AudioPlayer.duration; 1159 AudioPlayer.addEventListener('loadedmetadata', function() { 1160 var Duration = AudioPlayer.duration; 1161 $('#pg-duration').text(getDurationHours(Duration)); 1162 }); 1163 1164 AudioPlayer.addEventListener('timeupdate', function(){ 1165 1166 var CurrentTime = Math.round(AudioPlayer.currentTime); 1167 var PercentTime = CurrentTime*100/AudioPlayer.duration; 1168 var PercentTime = PercentTime + '%'; 1169 1170 $('.pg-current').css('width', PercentTime); 1171 1172 }); 1173 1174 $('#pg-progress-bar').click(function(e){ 1175 1176 var $this = $(this); 1177 1178 var posX = $(this).offset().left, 1179 posY = $(this).offset().top; 1180 var posX = e.pageX - posX; 1181 var wBar = $this.width(); 1182 1183 var PercentPosition = posX*100/wBar; 1184 1185 // Calculate Percent of duration 1186 var newTime = PercentPosition * AudioPlayer.duration / 100; 1187 AudioPlayer.currentTime = newTime; 1188 1189 1190 }); 1191 1192 // Replay Live 1193 AudioPlayer.addEventListener('ended', function(){ 1194 $('.pg-current').css('width', '0px'); 1195 $('.pg-current').remove(); 1196 $('#PlayGrrif').click(); 1197 }); 1198 1199 } 1200 1201 return this; 1202 }, 1203 pause: function () { 1204 // -- pause the radio 1205 // -- addClass/removeClass 1206 1207 var kP = document.getElementById('kast-play'); 1208 if (classL) { 1209 var kPC = kP.classList; 1210 kPC.remove('kast-playing') 1211 kPC.remove('kast-stopped') 1212 kPC.add('kast-paused') 1213 kPC.add('kast-continuous') 1214 } else { 1215 $(kP).removeClass('kast-playing kast-stopped').addClass('kast-paused kast-continuous') 1216 } 1217 1218 this.audio.pause(); 1219 1220 this.opt.onPause(this.audio) // Event 1221 1222 $('#pg-progress-bar').off('click'); 1223 1224 return this; 1225 }, 1226 stop: function () { 1227 // -- pause the radio 1228 // -- addClass/removeClass 1229 1230 var kP = document.getElementById('kast-play'); 1231 if (classL) { 1232 var kPC = kP.classList; 1233 kPC.remove('kast-playing') 1234 kPC.remove('kast-paused') // For caution when user uses API 1235 kPC.remove('kast-continuous') // For caution when user uses API 1236 kPC.add('kast-stopped') 1237 } else { 1238 $(kP).removeClass('kast-playing kast-paused kast-continuous').addClass('kast-stopped') 1239 } 1240 1241 // Stop is same as pause, play() takes care of the rest 1242 this.audio.pause(); 1243 1244 this.opt.onStop(this.audio) // Event 1245 1246 return this; 1247 1248 }, 1249 mute: function () { 1250 // -- mute audio 1251 // -- className 1252 1253 document.getElementById('kast-volume').className = 'kast-mdi kast-mdi-volume-off'; 1254 1255 this.audio.muted = true; 1256 1257 this.opt.onMute(this.audio) // Event 1258 1259 return this 1260 }, 1261 unmute: function () { 1262 // -- unmute audio 1263 // -- className 1264 1265 document.getElementById('kast-volume').className = 'kast-mdi kast-mdi-volume-high'; 1266 1267 this.audio.muted = false; 1268 1269 this.opt.onUnmute(this.audio) // Event 1270 1271 return this 1272 }, 1273 minimize: function () { 1274 // -- minimize the radio 1275 // -- addClass/removeClass 1276 1277 var k = document.getElementById('kast'); 1278 if (classL) { 1279 var kC = k.classList; 1280 kC.remove('kast-maximized') 1281 kC.add('kast-minimized') 1282 } else { 1283 $(k).removeClass('kast-maximized').addClass('kast-minimized'); 1284 } 1285 1286 this.opt.onMinimize() // Event 1287 1288 return this; 1289 }, 1290 maximize: function () { 1291 // -- maximize the radio 1292 // -- addClass/removeClass 1293 1294 var k = document.getElementById('kast'); 1295 if (classL) { 1296 var kC = k.classList; 1297 kC.remove('kast-minimized') 1298 kC.add('kast-maximized') 1299 } else { 1300 $(k).removeClass('kast-minimized').addClass('kast-maximized'); 1301 } 1302 1303 this.opt.onMaximize() // Event 1304 1305 return this; 1306 }, 1307 stats: function (callback) { 1308 // -- Pull current info 1309 // -- return it. callback contains data 1310 // -- data: 1311 // -- {averagetime, backupstatus, bitrate, content, 1312 // -- currentlisteners, maxlisteners, peaklisteners, 1313 // -- servergenre[ -1-5], servertitle, serverurl, 1314 // -- songtitle, streamhits, streamlisted,
1315 // -- streamlistederror, streampath, streamstatus, 1316 // -- streamuptime, uniquelisteners, version} 1317 1318 var that = this, 1319 ot = that.opt; 1320 1321 1322 // -- Cached selectors 1323 var kast = document.getElementById('kast'), 1324 kastTitle = document.getElementById('kast-np-title'), 1325 kastArtist = document.getElementById('kast-np-artist'), 1326 kastTop = document.getElementById('kast-top'), 1327 kastBar = document.getElementById('kast-bar'), 1328 kastTopC = document.getElementById('kast-top-content'), 1329 kastPlay = document.getElementById('kast-play'); 1330 1331 var processStats = function (data) { 1332 // -- Mutual data for current track/artist and current artist artwork 1333 var songTitle, currentTrack, currentArtist, _titleSplit; 1334 1335 var unTrack = ot.language.unknownTrackText, 1336 unArtist = ot.language.unknownArtistText; 1337 1338 1339 try { 1340 songTitle = (ot.direct ? data.songtitle : (data.body) ? data.body.textContent.split(',')[6] : data.getElementsByTagName('SONGTITLE')[0].textContent) || unArtist + ' - ' + unTrack; // V2 || V1 1341 } catch (e) { // aka 'error' 1342 songTitle = unArtist + ' - ' + unTrack; 1343 } 1344 1345 // -- Remove CDATA if found 1346 var songTitleHasCDATA = songTitle.indexOf('//<![CDATA'); 1347 if (songTitleHasCDATA > -1) { 1348 songTitle = songTitle.slice(0, songTitleHasCDATA - 1); 1349 } 1350 1351 1352 // -- Current track/artist 1353 // ** when custom track/artist is set, everything is still normall, i just set them 1354 1355 _titleSplit = songTitle.split(' - '); 1356 1357 if (typeof ot.currentTrack === 'string') { 1358 currentTrack = ot.currentTrack; 1359 } else { 1360 currentTrack = _titleSplit[1] || songTitle || unTrack; 1361 } 1362 1363 if (typeof ot.currentArtist === 'string') { 1364 currentArtist = ot.currentArtist; 1365 } else { 1366 currentArtist = _titleSplit[1] ? _titleSplit[0] : unArtist; 1367 } 1368 1369 if (ot.skipInfo) { 1370 var i = 0; 1371 var len = ot.skipInfo.length; 1372 1373 for (; i < len; i++) { 1374 1375 if (ot.skipInfo[i].track && ot.skipInfo[i].artist) { 1376 1377 if (ot.skipInfo[i].track === currentTrack && ot.skipInfo[i].artist === currentArtist) return; 1378 1379 } else if ((ot.skipInfo[i].track && ot.skipInfo[i].track === currentTrack) || (ot.skipInfo[i].artist && ot.skipInfo[i].artist === currentArtist)) { 1380 return; 1381 } 1382 1383 } 1384 } 1385 1386 if (ot.currentTrack) { 1387 kastTitle.textContent = currentTrack; 1388 } 1389 if (ot.currentArtist) { 1390 kastArtist.textContent = currentArtist; 1391 } 1392 1393 1394 // -- Pull Current artist artwork 1395 if (ot.artwork) { 1396 that.artwork(currentArtist, currentTrack, function (imgAr) { 1397 1398 if (!imgAr) { 1399 // -- -- Remove default art 1400 // -- -- Reset backgrounds/colors 1401 // -- -- Reset theme 1402 // ** ** kast addClass/removeClass 1403 // ** ** kastTop className 1404 // ** ** kastTopC className 1405 // ** ** kastPlay addClass/removeClass 1406 // ** ** kastBar className 1407 1408 if (classL) { 1409 1410 var kC = kast.classList, 1411 kPC = kastPlay.classList; 1412 kPC.remove('kast-accent-light') 1413 kPC.remove('kast-accent-dark') 1414 1415 if (ot.theme === 'dark') { 1416 kC.remove('kast-light') 1417 kC.add('kast-dark') 1418 } else { 1419 kC.remove('kast-dark') 1420 kC.add('kast-light') 1421 } 1422 } else { 1423 $(kastPlay).removeClass('kast-accent-light kast-accent-dark'); 1424 1425 if (ot.theme === 'dark') { 1426 $(kast).removeClass('kast-light').addClass('kast-dark'); 1427 } else { 1428 $(kast).removeClass('kast-dark').addClass('kast-light'); 1429 } 1430 } 1431 kastTop.className = 'kast-default-art'; 1432 if (ot.statusBar) { 1433 kastBar.removeAttribute('class') // removeAttr works here 1434 } 1435 kastPlay.removeAttribute('style') 1436 kastTop.removeAttribute('style'); 1437 kastTopC.removeAttribute('style'); 1438 kastTopC.className = 'kast-top-content'; 1439 1440 1441 1442 } else { 1443 1444 var imgURL = imgAr[0], 1445 imgLite = imgAr[1] || imgURL; // the small one to work with Color Thief (Faster and lighter on the CPU/GPU) 1446 1447 kastTop.style.backgroundImage = 'url(' + imgURL + ')'; 1448 kastTop.removeAttribute('class') 1449 1450 1451 ot.onCurrentArtwork(imgURL) // Event 1452 1453 // -- Dynamic colors & Dynamic theme 1454 // -- both requires artwork obviously 1455 1456 // ** XHR: is already smart and checks for XHR2 support 1457 // ** so we just need to check Blob constractor support 1458 if ((ot.theme === 'dynamic' || ot.colors === 'dynamic') && window.Blob) { 1459 1460 // -- Golden method for getting colors from XHR images 1461 // ** other methods: http://jsfiddle.net/manarkamel/bmvqavjf/ 1462 XHR({ 1463 url: imgLite, 1464 arraybuffer: true, 1465 load: function (data, req) { 1466 window.URL = window.URL || window.webkitURL; // support for Safari/Old Chrome 1467 var img = document.createElement('img'), 1468 contentType = req.getResponseHeader('content-type'), 1469 uData = new Uint8Array(data), 1470 blob = new Blob([uData], { 1471 type: contentType 1472 }); 1473 img.src = window.URL.createObjectURL(blob); 1474 img.onload = function () { 1475 var color = colorThiefNew.getPalette(img, 5), 1476 primaryBG = 'rgb(' + color[0] + ')', 1477 accentBG = 'rgb(' + color[1] + ')', 1478 primaryContrast = getColorContrast(primaryBG) || 'light', 1479 primaryCC = 'kast-primary-' + primaryContrast, 1480 accentContrast = getColorContrast(accentBG) || 'dark', 1481 accentCC = 'kast-accent-' + accentContrast; 1482 1483 1484 // -- Dynamic colors 1485 if (ot.colors === 'dynamic') { 1486 1487 var kastPlayClass = ot.continuous ? 'kast-paused ' : 'kast-stopped '; 1488 var kPC = kastPlay.className; 1489 if (kPC.indexOf('playing') !== -1) { 1490 kastPlay.className = 'kast-playing ' + accentCC 1491 } else { 1492 kastPlay.className = kastPlayClass + accentCC 1493 } 1494 1495 if (ot.continuous) { 1496 kastPlay.className += ' kast-continuous '; 1497 } 1498 1499 kastTopC.className = 'kast-top-content ' + primaryCC; 1500 if (ot.statusBar) { 1501 kastBar.className = primaryCC; 1502 } 1503 kastPlay.style.backgroundColor = accentBG; 1504 1505 if (ot.ui === 'colored') { 1506 kastTopC.style.backgroundColor = primaryBG; 1507 } 1508 1509 ot.onDynamicColors(primaryBG, accentBG) // Event 1510 ot.onDynamicColorsContrast(primaryContrast, accentContrast) // Event 1511 } 1512 1513 1514 // -- Dynamic theme 1515 if (ot.theme === 'dynamic') { 1516 if (primaryContrast === 'light') { 1517 if (classL) { 1518 var kC = kast.classList; 1519 kC.remove('kast-dark') 1520 kC.add('kast-light') 1521 } else { 1522 $(kast).removeClass('kast-dark').addClass('kast-light') 1523 } 1524 1525 ot.onDynamicTheme('light') // Event 1526 } else { 1527 if (classL) { 1528 var kC = kast.classList; 1529 kC.remove('kast-light') 1530 kC.add('kast-dark') 1531 } else { 1532 $(kast).removeClass('kast-light').addClass('kast-dark') 1533 } 1534 1535 ot.onDynamicTheme('dark') // Event 1536 } 1537 } 1538 1539 // -- objectURL is no longer needed 1540 // ** now we should release it to free cache/memory :) 1541 1542 // -- So... FINISH HIM! (him = objectURL) 1543 window.URL.revokeObjectURL(img.src) 1544 img = null 1545 } 1546 } 1547 }) 1548 1549 1550 } 1551 1552 } 1553 1554 }) 1555 } 1556 1557 ot.onCurrentInfo(currentTrack, currentArtist) // Event 1558 }; 1559 1560 if (typeof callback === 'object') { 1561 processStats(callback) // because it's not a callback, it's DATA 1562 } else { 1563 var ajaxStats = function (hostCORSproxy, showError) { 1564 var url = hostCORSproxy + ot.statsPath + '?sid=' + ot.sid, 1565 urlParm = (ot.version === 1) ? '#' : '?_='; // V1 servers are dumb 1566 if (ot.version === 1) { 1567 url = hostCORSproxy + '7.html'; 1568 } 1569 XHR({
1570 url: url + urlParm + Math.random(), 1571 timeout: 8000, 1572 load: function (data) { 1573 1574 var dataDOM = data.indexOf('<?xml') > -1 ? new DOMParser().parseFromString(data, 'text/xml') : createHTMLDocument(data); 1575 1576 if (typeof callback === 'function') { 1577 callback(dataDOM); 1578 } else { 1579 processStats(dataDOM) 1580 } 1581 }, 1582 error: function () { 1583 if (showError) { 1584 var isOffline = kast.hasAttribute('data-offline'), 1585 kastNT = document.getElementById('kast-np-title'); // artist could work too 1586 1587 // ** ** OfflineCheck requires autoUpdate 1588 if (ot.offlineCheck && ot.autoUpdate && !isOffline) { 1589 if (classL) { 1590 kast.classList.add('kast-offline'); 1591 } else { 1592 $(kast).addClass('kast-offline'); 1593 } 1594 1595 1596 kast.setAttribute('data-offline', 'true') 1597 ot.onOffline(); 1598 } 1599 1600 // ** ** If empty add unknown 1601 if (!kastNT.hasChildNodes()) { 1602 processStats('error') 1603 } 1604 1605 } else { 1606 // -- -- fallback to another proxy and show error this time 1607 ajaxStats(that.hostCORS[ot.betaProxies ? 3 : 1], true) 1608 } 1609 } 1610 }) 1611 1612 } 1613 var directStats = function () { 1614 1615 $.ajax({ 1616 url: that.host + ot.statsPath + '?sid=' + ot.sid + '&json=1&_=' + Math.random(), 1617 dataType: 'jsonp', 1618 timeout: 8000, 1619 success: function (data) { 1620 1621 if (typeof callback === 'function') { 1622 callback(data); 1623 } else { 1624 processStats(data) 1625 } 1626 1627 }, 1628 error: function () { 1629 1630 var isOffline = kast.hasAttribute('data-offline'), 1631 kastNT = document.getElementById('kast-np-title'); // artist could work too 1632 1633 // ** ** OfflineCheck requires autoUpdate 1634 if (ot.offlineCheck && ot.autoUpdate && !isOffline) { 1635 if (classL) { 1636 kast.classList.add('kast-offline'); 1637 } else { 1638 $(kast).addClass('kast-offline'); 1639 } 1640 1641 1642 kast.setAttribute('data-offline', 'true') 1643 ot.onOffline(); 1644 } 1645 1646 // ** ** If empty add unknown 1647 if (!kastNT.hasChildNodes()) { 1648 processStats('error') 1649 } 1650 1651 } 1652 }) 1653 1654 } 1655 1656 if (ot.direct) { 1657 // directStats() 1658 } else { 1659 // ajaxStats(that.hostCORS[ot.betaProxies ? 2 : 0], false) 1660 } 1661 1662 1663 } 1664 1665 1666 return this 1667 }, 1668 played: function (callback, woArtwork) { 1669 // -- Pull played info 1670 // -- return it. callback contains data 1671 1672 var that = this, 1673 ot = that.opt; 1674 1675 // Cached selectors 1676 var kastPL = document.getElementById('kast-playedlist'); 1677 1678 var processPlayed = function (data) { 1679 1680 // -- Empty 1681 while (kastPL.firstChild) { 1682 kastPL.removeChild(kastPL.firstChild); 1683 } 1684 1685 var song, songL, _tables, 1686 songsFrag = document.createDocumentFragment(), 1687 unTrack = ot.language.unknownTrackText, 1688 unArtist = ot.language.unknownArtistText; 1689 1690 // Note: SHOUTcast V2.5x has 2 tables only, 1691 // others have 3 tables. 1692 // Played list is usually the last table, but not always. 1693 // So to get the played list table, we must loop and search for 'played @' text for that. 1694 try { 1695 if (ot.direct) { 1696 song = data; 1697 } else if (data.body) { 1698 _tables = data.getElementsByTagName('table'); 1699 1700 var i = 0; 1701 var len = _tables.length; 1702 1703 // find table index that has songs 1704 for (;i < len; i++) if (_tables[i].textContent.toLowerCase().indexOf('played @') > -1) break; 1705 1706 song = _tables[i].getElementsByTagName('tr'); 1707 } else { 1708 song = data.getElementsByTagName('SONG'); 1709 } 1710 songL = song.length; 1711 } catch (e) { 1712 songL = 3; 1713 } 1714 1715 var processPL = function (j, song, songL, songsFrag) { 1716 1717 var playedTrack, playedArtist, title, _titleSplit; 1718 1719 try { 1720 title = (ot.direct ? song[j].title : (data.body) ? song[j].getElementsByTagName('td')[1].textContent : song[j].getElementsByTagName('TITLE')[0].textContent) || unArtist + ' - ' + unTrack; 1721 } catch (e) { 1722 title = unArtist + ' - ' + unTrack; 1723 } 1724 1725 _titleSplit = title.split(' - '); 1726 1727 if (ot.playedTracks) { 1728 playedTrack = _titleSplit[1] || title || unTrack; 1729 } 1730 if (ot.playedArtists) { 1731 playedArtist = _titleSplit[1] ? _titleSplit[0] : unArtist; 1732 } 1733 1734 if (ot.skipInfo) { 1735 var i = 0; 1736 var len = ot.skipInfo.length; 1737 1738 for (; i < len; i++) { 1739 1740 if (ot.skipInfo[i].track && ot.skipInfo[i].artist) { 1741 1742 if (ot.skipInfo[i].track === playedTrack && ot.skipInfo[i].artist === playedArtist) return; 1743 1744 } else if ((ot.skipInfo[i].track && ot.skipInfo[i].track === playedTrack) || (ot.skipInfo[i].artist && ot.skipInfo[i].artist === playedArtist)) { 1745 return; 1746 } 1747 1748 } 1749 } 1750 1751 // -- Track Element 1752 var li = document.createElement('li'), 1753 divA = document.createElement('div'), 1754 i = document.createElement('i'), 1755 divI = document.createElement('div'), 1756 pTitle = document.createElement('p'), 1757 pArtist = document.createElement('p'); 1758 divA.className = 'kast-p-art'; 1759 i.className = 'kast-mdi kast-mdi-album'; 1760 divI.className = 'kast-p-info'; 1761 pTitle.className = 'kast-p-title'; 1762 pTitle.textContent = playedTrack; 1763 pArtist.className = 'kast-p-artist'; 1764 pArtist.textContent = playedArtist; 1765 1766 li.appendChild(divA); 1767 li.appendChild(divI); 1768 divA.appendChild(i); 1769 divI.appendChild(pTitle); 1770 divI.appendChild(pArtist); 1771 1772 1773 // -- Played artworks 1774 if (ot.artwork && !woArtwork) { 1775 1776 that.artwork(playedArtist, playedTrack, function (imgAr) { 1777 1778 if (imgAr) { 1779 divA.style.backgroundImage = 'url(' + (imgAr[1] || imgAr[0]) + ')'; // 0 here for defaultArtwork fix 1780 1781 ot.onPlayedArtworks(imgAr[1] || imgAr[0]) // Event 1782 1783 } else { 1784 1785 divA.className = 'kast-p-art kast-p-default-art'; 1786 } 1787 1788 }) 1789 } else { 1790 divA.className = 'kast-p-art kast-p-default-art'; 1791 } 1792 1793 ot.onPlayedInfo(playedTrack, playedArtist) // Event 1794 1795 songsFrag.appendChild(li) 1796 1797 } 1798 var jV = (!data.body) ? 1 : 2; // // i = 1; Not first child. P.S. V1 must skip the first two childs 1799 for (var j = jV; j < songL; j++) { 1800 processPL(j, song, songL, songsFrag) 1801 } 1802 1803 kastPL.appendChild(songsFrag); // Append at last, like a boss 1804 }, 1805 ajaxPlayed = function (hostCORSproxy, showError) { 1806 var url = hostCORSproxy + ot.playedPath + '?sid=' + ot.sid + '&type=xml', 1807 urlParm = (ot.version === 1) ? '#' : '&_='; // V1 servers are dumb 1808 if (ot.version === 1) { 1809 url = hostCORSproxy + ot.playedPath + '.html'; 1810 } 1811 1812 1813 XHR({
1814 url: url + urlParm + Math.random(), 1815 timeout: 8000, 1816 load: function (data) { 1817 // ** Bug in crossorigin.me, data sometimes is string 'Error: Error: Parse Error' (500 Error) 1818 // ** reported https://github.com/technoboy10/crossorigin.me/issues/28 1819 1820 // ** becuz text/html has lower browser support 1821 1822 1823 var dataDOM = (data.indexOf('<?xml') > -1) ? new DOMParser().parseFromString(data, 'text/xml') : createHTMLDocument(data); 1824 1825 if (callback) { 1826 callback(dataDOM) 1827 } else { 1828 processPlayed(dataDOM) 1829 } 1830 }, 1831 error: function (data) { 1832 if (showError) { 1833 if (!kastPL.hasChildNodes()) { 1834 processPlayed('error') 1835 } 1836 } else { 1837 // -- -- fallback to another proxy and show error this time 1838 ajaxPlayed(that.hostCORS[ot.betaProxies ? 3 : 1], true) 1839 } 1840 } 1841 }) 1842 1843 1844 }, 1845 directPlayed = function () { 1846 1847 $.ajax({ 1848 url: that.host + ot.playedPath + '?sid=' + ot.sid + '&type=json', 1849 timeout: 8000, 1850 dataType: 'jsonp', 1851 success: function (data) { 1852 if (callback) { 1853 callback(data) 1854 } else { 1855 processPlayed(data) 1856 } 1857 }, 1858 error: function () { 1859 if (!kastPL.hasChildNodes()) { 1860 processPlayed('error') 1861 } 1862 } 1863 }) 1864 1865 } 1866 1867 if (ot.direct) { 1868 directPlayed() 1869 } else { 1870 ajaxPlayed(that.hostCORS[ot.betaProxies ? 2 : 0], false) 1871 } 1872 1873 return this 1874 }, 1875 autoUpdate: function (uKey) { 1876 var that = this, 1877 ot = that.opt; 1878 1879 // -- Cached selectors 1880 var kast = document.getElementById('kast'), 1881 kastServer = document.getElementById('kast-server'), 1882 kastPlay = document.getElementById('kast-play'), 1883 kastC; 1884 1885 if (classL) { 1886 kastC = kast.classList; 1887 } 1888 1889 // -- Turn off 1890 if (!uKey) { 1891 clearInterval(kast.getAttribute('data-interval')) 1892 kast.removeAttribute('data-interval') 1893 return 1894 } 1895 1896 // -- Checker fn to check for songtitle change 1897 var checker = function () { 1898 that.stats(function (data) { 1899 1900 var isPlaying = (kastPlay.className.indexOf('playing') !== -1); 1901 1902 // -- Offline check 1903 if (ot.offlineCheck) { 1904 1905 var SS = ot.direct ? data.streamstatus : (data.body) ? data.body.textContent.split(',')[1] : data.getElementsByTagName('STREAMSTATUS')[0].textContent; 1906 1907 var isOffline = kast.hasAttribute('data-offline'); 1908 1909 if (!data || SS != 1) { // Yes! String not integer 1910 1911 if (!isOffline) { 1912 if (classL) { 1913 kastC.add('kast-offline') // I already have classList cached 1914 } else { 1915 $(kast).addClass('kast-offline') 1916 } 1917 1918 kast.setAttribute('data-offline', 'true'); 1919 ot.onOffline(); 1920 } 1921 1922 } else { 1923 1924 if (isOffline) { 1925 if (classL) { 1926 kastC.remove('kast-offline') 1927 } else { 1928 $(kast).removeClass('kast-offline') 1929 } 1930 1931 //isPlaying && that.play(true); // we need true 1932 1933 kast.setAttribute('data-offline', 'false'); 1934 ot.onOnline() 1935 } 1936 } 1937 1938 } 1939 1940 1941 // ** When offline->online back again, we call play() again 1942 // ** and there's bug in some SHOUTcast servers (AAC+) or maybe in Chrome,
1943 // ** sometimes the audio goes off for no reason, 1944 // ** so in order to play, we need to pause->play to play audio, 1945 // ** that's no longer necessary 1946 isPlaying && that.play(); 1947 1948 // -- update server info 1949 if (uKey === 'all') { 1950 var sI = ot.serverInfo, 1951 sB = ot.statusBar; 1952 if (typeof sI !== 'string' && sB) { // I don't update custom server info 1953 kastServer.textContent = ''; 1954 processServerInfo(ot, sI, data, kastServer) 1955 } 1956 } 1957 1958 // ** If both are string, and played is false 1959 // ** then there's absolutely no need to check for songtitle and continue 1960 var customTA = (typeof ot.currentTrack === 'string' && typeof ot.currentArtist === 'string'); 1961 if (customTA && !ot.played) { 1962 ot.onUpdate(data) // Event 1963 ot.onUpdateAll(data) // Event 1964 return 1965 } 1966 1967 var streamSTitle = ot.direct ? data.songtitle : (data.body) ? data.body.textContent.split(',')[6] : data.getElementsByTagName('SONGTITLE')[0].textContent, // V2 || V1 1968 currentSTitle = kast.getAttribute('data-current'); 1969 1970 // ** on start, data is undefined, so let's define it 1971 if (!currentSTitle) { 1972 return kast.setAttribute('data-current', streamSTitle); 1973 } 1974 1975 if (currentSTitle !== streamSTitle) { 1976 // ** These functions will only be called on SHOUTcast songtitle change, not every 5 seconds, 1977 // ** So if there's some heavy DOM manipulation here, no problem at all 1978 1979 if (!customTA) { // I don't update custom T/A *if both are string 1980 that.stats(data); 1981 } 1982 1983 if (ot.played) { 1984 var isArtArr = (Array.isArray && Array.isArray(ot.artwork)); 1985 if (isArtArr) { 1986 that.played(false, true) 1987 } else { 1988 that.played() 1989 } 1990 } 1991 1992 1993 kast.setAttribute('data-current', streamSTitle); 1994 1995 1996 ot.onUpdate(data) // Event 1997 } 1998 1999 ot.onUpdateAll(data) // Event 2000 }) 2001 }, 2002 setTimer = function (fn, time) { 2003 if (!kast.hasAttribute('data-interval')) { 2004 var setInterID = setInterval(fn, time); 2005 kast.setAttribute('data-interval', setInterID) 2006 } 2007 }, 2008 timeMS = 8000; 2009 2010 if (that.mobile) { 2011 timeMS = (that.mobileMedium) ? 16000 : 12000; 2012 } 2013 2014 setTimer(checker, ot.customCheckerInterval || timeMS) 2015 2016 return this 2017 }, 2018 artwork: function (artist, track, callback) { 2019 // -- Pull album artwork from spotify API 2020 // -- or artist artwork as fallback 2021 // -- or Default art as fallback 2022 // -- return it 2023 2024 var that = this, 2025 ot = this.opt; 2026 2027 var defaultArtwork = (Array.isArray && Array.isArray(ot.defaultArtwork)) ? ot.defaultArtwork : null; 2028 2029 if (Array.isArray && Array.isArray(ot.artwork)) { 2030 return callback(ot.artwork) 2031 } 2032 2033 if (artist === ot.language.unknownArtistText) { // If artist is unknown 2034 return callback(defaultArtwork) 2035 } 2036 2037 var artistC = chaRe(artist), 2038 trackC = chaRe(track), 2039 processArtworkX = function (data) { // @retuns images array if found or null 2040 var imgAr = null; 2041 if (data.tracks && data.tracks.items[0]) { 2042 var imagesArrayT = data.tracks.items[0].album.images || '', 2043 imgLarge, 2044 imgSmall = imagesArrayT[2].url || ''; 2045 2046 if (that.mobileMedium) { // ** Mobile first :) 2047 imgLarge = imagesArrayT[1].url || ''; // ** Medium quailty 2048 } else { 2049 imgLarge = imagesArrayT[0].url || ''; 2050 } 2051 2052 if (imgLarge && imgSmall) { 2053 imgAr = [imgLarge, imgSmall]; 2054 } 2055 } else if (data.artists && data.artists.items[0] && data.artists.items[0].images[0]) { 2056 var imagesArrayA = data.artists.items[0].images || '', 2057 imgLarge = null, 2058 imgSmall = null; 2059 2060 if (data.artists.items[0].images.length === 4) { // ** Not normal artist images 2061 if (that.mobileMedium) { // ** Mobile first :) 2062 imgLarge = imagesArrayA[2].url || ''; // ** Medium quailty 2063 } else { 2064 imgLarge = imagesArrayA[1].url || ''; 2065 } 2066 2067 imgSmall = imagesArrayA[3].url || ''; 2068 2069 } else { 2070 if (that.mobileMedium) { // ** Mobile first :) 2071 imgLarge = imagesArrayA[1].url || ''; // ** Medium quailty 2072 } else { 2073 imgLarge = imagesArrayA[0].url || ''; 2074 } 2075 2076 imgSmall = imagesArrayA[2].url || ''; 2077 } 2078 if (imgLarge && imgSmall) { 2079 imgAr = [imgLarge, imgSmall]; 2080 } 2081 } 2082 2083 return imgAr 2084 }, 2085 ajaxArtwork = function (artistN, trackN, noLuck) { 2086 var spotifyTrackAPI = 'https://api.spotify.com/v1/search?q=artist:' + artistN + '%20track:' + trackN + '&limit=1&type=track', 2087 spotifyArtistAPI = 'https://api.spotify.com/v1/search?q=artist:' + artistN + '&limit=1&type=artist', 2088 spotifyAPI = noLuck ? spotifyArtistAPI : spotifyTrackAPI; 2089 2090 // Smart pulling 2091 XHR({ 2092 url: spotifyAPI, 2093 load: function (data) { 2094 var dataJSON = JSON.parse(data); 2095 var imgAr = processArtworkX(dataJSON); // @returns artwork array or null 2096 if (imgAr) { 2097 callback(imgAr) 2098 } else { 2099 2100 if (noLuck) { 2101 callback(defaultArtwork) 2102 } else { 2103 ajaxArtwork(artistN, trackN, true) // ** this time get artist art and if !imgAr return null 2104 } 2105 2106 } 2107 }, 2108 error: function () { 2109 callback(defaultArtwork) 2110 } 2111 }) 2112 }, 2113 iW = ot.irrelevantWords; 2114 2115 if (iW) { 2116 var iWL = iW.length; 2117 for (var i = 0; i < iWL; i++) { 2118 artistC = artistC.replace(iW[i], '') 2119 trackC = trackC.replace(iW[i], '') 2120 } 2121 2122 ajaxArtwork(artistC, trackC) // smart artwork pulling 2123 2124 } else { 2125 ajaxArtwork(artistC, trackC) // smart artwork pulling 2126 } 2127 2128 2129 2130 return this 2131 }, 2132 destroy: function () { 2133 2134 // -- Destroy the plugin
2135 window.kastXHR && window.kastXHR.abort(); // Terminate any active XHR 2136 2137 this.autoUpdate(false); // Clear AutoUpdate data 2138 this.audio.pause(); // Pause audio 2139 2140 $.data(window, 'plugin_kast', null); // Clear Kast data 2141 var k = document.getElementById('kast'); 2142 k && k.parentNode.removeChild(k) // Remove Kast element 2143 2144 _popup && _popup.close(); 2145 2146 // remove BNJ AddOn 2147 $('#pg-progress-bar').unbind('click'); 2148 $('#pg-current').width(0); 2149 $('#pg-current').remove(); 2150 //$('#pg-current').css('width', '0px'); 2151 2152 // redefine document, if touched when using popup option 2153 document = window.document; 2154 2155 return this 2156 } 2157 } 2158 2159 // Plugin wrapper 2160 // ======================================================== // 2161 $.kast = function (options, keys) { 2162 2163 if (!$.data(window, 'plugin_kast')) { 2164 2165 if (options === 'destroy') { 2166 return 2167 } 2168 2169 $.data(window, 'plugin_kast', new Plugin(this, options)); 2170 } else if (typeof Plugin.prototype[options] === 'function') { 2171 $.data(window, 'plugin_kast')[options](keys); 2172 } 2173 2174 } 2175 2176 2177})(jQuery, window, document);
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.