1/* 2 * 3 * Copyright (c) 2008-2010, Felix Bruns <[email protected]> 4 * 5 */ 6 7function LastFM(options){ 8 /* Set default values for required options. */ 9 var apiKey = options.apiKey || ''; 10 var apiSecret = options.apiSecret || ''; 11 var apiUrl = options.apiUrl || 'https://ws.audioscrobbler.com/2.0/'; 12 var cache = options.cache || undefined; 13 14 /* Set API key. */ 15 this.setApiKey = function(_apiKey){ 16 apiKey = _apiKey; 17 }; 18 19 /* Set API key. */ 20 this.setApiSecret = function(_apiSecret){ 21 apiSecret = _apiSecret; 22 }; 23 24 /* Set API URL. */ 25 this.setApiUrl = function(_apiUrl){ 26 apiUrl = _apiUrl; 27 }; 28 29 /* Set cache. */ 30 this.setCache = function(_cache){ 31 cache = _cache; 32 }; 33 34 /* Set the JSONP callback identifier counter. This is used to ensure the callbacks are unique */ 35 var jsonpCounter = 0; 36 37 /* Internal call (POST, GET). */ 38 var internalCall = function(params, callbacks, requestMethod){ 39 /* Cross-domain POST request (doesn't return any data, always successful). */ 40 if(requestMethod == 'POST'){ 41 /* Create iframe element to post data. */ 42 var html = document.getElementsByTagName('html')[0]; 43 var iframe = document.createElement('iframe'); 44 var doc; 45 46 /* Set iframe attributes. */ 47 iframe.width = 1; 48 iframe.height = 1; 49 iframe.style.border = 'none'; 50 51 /* Append iframe. */ 52 html.appendChild(iframe); 53 iframe.onload = function(){ 54 /* Remove iframe element. */ 55 html.removeChild(iframe); 56 57 /* Call user callback. */ 58 if(typeof(callbacks.success) != 'undefined'){ 59 callbacks.success(); 60 } 61 }; 62 63 /* Get iframe document. */ 64 if(typeof(iframe.contentWindow) != 'undefined'){ 65 doc = iframe.contentWindow.document; 66 } 67 else if(typeof(iframe.contentDocument.document) != 'undefined'){ 68 doc = iframe.contentDocument.document.document; 69 } 70 else{ 71 doc = iframe.contentDocument.document; 72 } 73 74 /* Open iframe document and write a form. */ 75 doc.open(); 76 doc.clear(); 77 doc.write('<form method="post" action="' + apiUrl + '" id="form">'); 78 79 /* Write POST parameters as input fields. */ 80 for(var param in params){ 81 doc.write('<input type="text" name="' + param + '" value="' + params[param] + '">'); 82 } 83 84 /* Write automatic form submission code. */ 85 doc.write('</form>'); 86 doc.write('<script type="application/x-javascript">'); 87 doc.write('document.getElementById("form").submit();'); 88 doc.write('</script>'); 89 90 /* Close iframe document. */ 91 doc.close(); 92 } 93 /* Cross-domain GET request (JSONP). */ 94 else{ 95 /* Get JSONP callback name. */ 96 var jsonp = 'jsonp' + new Date().getTime() + jsonpCounter; 97
98 /* Update the unique JSONP callback counter */ 99 jsonpCounter += 1; 100 101 /* Calculate cache hash. */ 102 var hash = auth.getApiSignature(params); 103 104 /* Check cache. */ 105 if(typeof(cache) != 'undefined' && cache.contains(hash) && !cache.isExpired(hash)){ 106 if(typeof(callbacks.success) != 'undefined'){ 107 callbacks.success(cache.load(hash)); 108 } 109 110 return; 111 } 112 113 /* Set callback name and response format. */ 114 params.callback = jsonp; 115 params.format = 'json'; 116 117 /* Create JSONP callback function. */ 118 window[jsonp] = function(data){ 119 /* Is a cache available?. */ 120 if(typeof(cache) != 'undefined'){ 121 var expiration = cache.getExpirationTime(params); 122 123 if(expiration > 0){ 124 cache.store(hash, data, expiration); 125 } 126 } 127 128 /* Call user callback. */ 129 if(typeof(data.error) != 'undefined'){ 130 if(typeof(callbacks.error) != 'undefined'){ 131 callbacks.error(data.error, data.message); 132 } 133 } 134 else if(typeof(callbacks.success) != 'undefined'){ 135 callbacks.success(data); 136 } 137 138 /* Garbage collect. */ 139 window[jsonp] = undefined; 140 141 try{ 142 delete window[jsonp]; 143 } 144 catch(e){ 145 /* Nothing. */ 146 } 147 148 /* Remove script element. */ 149 if(head){ 150 head.removeChild(script); 151 } 152 }; 153 154 /* Create script element to load JSON data. */ 155 var head = document.getElementsByTagName("head")[0]; 156 var script = document.createElement("script"); 157 158 /* Build parameter string. */ 159 var array = []; 160 161 for(var param in params){ 162 array.push(encodeURIComponent(param) + "=" + encodeURIComponent(params[param])); 163 } 164 165 /* Set script source. */ 166 script.src = apiUrl + '?' + array.join('&').replace(/%20/g, '+'); 167 168 /* Append script element. */ 169 head.appendChild(script); 170 } 171 }; 172 173 /* Normal method call. */ 174 var call = function(method, params, callbacks, requestMethod){ 175 /* Set default values. */ 176 params = params || {}; 177 callbacks = callbacks || {}; 178 requestMethod = requestMethod || 'GET'; 179 180 /* Add parameters. */ 181 params.method = method; 182 params.api_key = apiKey; 183 184 /* Call method. */ 185 internalCall(params, callbacks, requestMethod); 186 }; 187 188 /* Signed method call. */ 189 var signedCall = function(method, params, session, callbacks, requestMethod){ 190 /* Set default values. */ 191 params = params || {}; 192 callbacks = callbacks || {}; 193 requestMethod = requestMethod || 'GET'; 194 195 /* Add parameters. */ 196 params.method = method; 197 params.api_key = apiKey; 198 199 /* Add session key. */ 200 if(session && typeof(session.key) != 'undefined'){ 201 params.sk = session.key; 202 } 203 204 /* Get API signature. */ 205 params.api_sig = auth.getApiSignature(params); 206 207 /* Call method. */ 208 internalCall(params, callbacks, requestMethod); 209 }; 210 211 /* Album methods. */ 212 this.album = { 213 addTags : function(params, session, callbacks){ 214 /* Build comma separated tags string. */ 215 if(typeof(params.tags) == 'object'){ 216 params.tags = params.tags.join(','); 217 } 218 219 signedCall('album.addTags', params, session, callbacks, 'POST'); 220 }, 221 222 getBuylinks : function(params, callbacks){ 223 call('album.getBuylinks', params, callbacks); 224 }, 225 226 getInfo : function(params, callbacks){ 227 call('album.getInfo', params, callbacks); 228 }, 229 230 getTags : function(params, session, callbacks){ 231 signedCall('album.getTags', params, session, callbacks); 232 }, 233 234 getTopTags: function(params, callbacks){ 235 signedCall('album.getTopTags', params, callbacks); 236 }, 237 238 removeTag : function(params, session, callbacks){ 239 signedCall('album.removeTag', params, session, callbacks, 'POST'); 240 }, 241 242 search : function(params, callbacks){ 243 call('album.search', params, callbacks); 244 }, 245 246 share : function(params, session, callbacks){ 247 /* Build comma separated recipients string. */ 248 if(typeof(params.recipient) == 'object'){ 249 params.recipient = params.recipient.join(','); 250 } 251 252 signedCall('album.share', params, callbacks); 253 } 254 }; 255 256 /* Artist methods. */ 257 this.artist = { 258 addTags : function(params, session, callbacks){ 259 /* Build comma separated tags string. */ 260 if(typeof(params.tags) == 'object'){ 261 params.tags = params.tags.join(','); 262 } 263 264 signedCall('artist.addTags', params, session, callbacks, 'POST'); 265 }, 266 267 getCorrection : function(params, callbacks){ 268 call('artist.getCorrection', params, callbacks); 269 }, 270 271 getEvents : function(params, callbacks){ 272 call('artist.getEvents', params, callbacks); 273 }, 274 275 getImages : function(params, callbacks){ 276 call('artist.getImages', params, callbacks); 277 }, 278 279 getInfo : function(params, callbacks){ 280 call('artist.getInfo', params, callbacks); 281 }, 282 283 getPastEvents : function(params, callbacks){ 284 call('artist.getPastEvents', params, callbacks); 285 }, 286 287 getPodcast : function(params, callbacks){ 288 call('artist.getPodcast', params, callbacks); 289 }, 290 291 getShouts : function(params, callbacks){ 292 call('artist.getShouts', params, callbacks); 293 }, 294 295 getSimilar : function(params, callbacks){ 296 call('artist.getSimilar', params, callbacks); 297 }, 298 299 getTags : function(params, session, callbacks){ 300 signedCall('artist.getTags', params, session, callbacks); 301 }, 302 303 getTopAlbums : function(params, callbacks){ 304 call('artist.getTopAlbums', params, callbacks); 305 }, 306 307 getTopFans : function(params, callbacks){ 308 call('artist.getTopFans', params, callbacks); 309 }, 310 311 getTopTags : function(params, callbacks){ 312 call('artist.getTopTags', params, callbacks); 313 }, 314 315 getTopTracks : function(params, callbacks){ 316 call('artist.getTopTracks', params, callbacks); 317 }, 318 319 removeTag : function(params, session, callbacks){ 320 signedCall('artist.removeTag', params, session, callbacks, 'POST'); 321 }, 322 323 search : function(params, callbacks){ 324 call('artist.search', params, callbacks); 325 }, 326 327 share : function(params, session, callbacks){ 328 /* Build comma separated recipients string. */ 329 if(typeof(params.recipient) == 'object'){ 330 params.recipient = params.recipient.join(','); 331 } 332 333 signedCall('artist.share', params, session, callbacks, 'POST'); 334 }, 335 336 shout : function(params, session, callbacks){ 337 signedCall('artist.shout', params, session, callbacks, 'POST'); 338 } 339 }; 340 341 /* Auth methods. */ 342 this.auth = { 343 getMobileSession : function(params, callbacks){ 344 /* Set new params object with authToken. */ 345 params = { 346 username : params.username, 347 authToken : md5(params.username + md5(params.password)) 348 }; 349 350 signedCall('auth.getMobileSession', params, null, callbacks); 351 }, 352 353 getSession : function(params, callbacks){ 354 signedCall('auth.getSession', params, null, callbacks); 355 }, 356 357 getToken : function(callbacks){ 358 signedCall('auth.getToken', null, null, callbacks); 359 }, 360 361 /* Deprecated. Security hole was fixed. */ 362 getWebSession : function(callbacks){ 363 /* Save API URL and set new one (needs to be done due to a cookie!). */ 364 var previuousApiUrl = apiUrl; 365 366 apiUrl = 'http://ext.last.fm/2.0/'; 367 368 signedCall('auth.getWebSession', null, null, callbacks); 369 370 /* Restore API URL. */ 371 apiUrl = previuousApiUrl; 372 } 373 }; 374 375 /* Chart methods. */ 376 this.chart = { 377 getHypedArtists : function(params, session, callbacks){ 378 call('chart.getHypedArtists', params, callbacks); 379 }, 380 381 getHypedTracks : function(params, session, callbacks){ 382 call('chart.getHypedTracks', params, callbacks); 383 }, 384 385 getLovedTracks : function(params, session, callbacks){ 386 call('chart.getLovedTracks', params, callbacks); 387 }, 388 389 getTopArtists : function(params, session, callbacks){ 390 call('chart.getTopArtists', params, callbacks); 391 }, 392 393 getTopTags : function(params, session, callbacks){ 394 call('chart.getTopTags', params, callbacks); 395 }, 396 397 getTopTracks : function(params, session, callbacks){ 398 call('chart.getTopTracks', params, callbacks); 399 } 400 }; 401 402 /* Event methods. */ 403 this.event = { 404 attend : function(params, session, callbacks){ 405 signedCall('event.attend', params, session, callbacks, 'POST'); 406 }, 407 408 getAttendees : function(params, session, callbacks){ 409 call('event.getAttendees', params, callbacks); 410 }, 411 412 getInfo : function(params, callbacks){ 413 call('event.getInfo', params, callbacks); 414 }, 415 416 getShouts : function(params, callbacks){ 417 call('event.getShouts', params, callbacks); 418 }, 419 420 share : function(params, session, callbacks){ 421 /* Build comma separated recipients string. */ 422 if(typeof(params.recipient) == 'object'){ 423 params.recipient = params.recipient.join(','); 424 } 425 426 signedCall('event.share', params, session, callbacks, 'POST'); 427 }, 428 429 shout : function(params, session, callbacks){ 430 signedCall('event.shout', params, session, callbacks, 'POST'); 431 } 432 }; 433 434 /* Geo methods. */ 435 this.geo = { 436 getEvents : function(params, callbacks){ 437 call('geo.getEvents', params, callbacks); 438 }, 439 440 getMetroArtistChart : function(params, callbacks){ 441 call('geo.getMetroArtistChart', params, callbacks); 442 }, 443
444 getMetroHypeArtistChart : function(params, callbacks){ 445 call('geo.getMetroHypeArtistChart', params, callbacks); 446 }, 447 448 getMetroHypeTrackChart : function(params, callbacks){ 449 call('geo.getMetroHypeTrackChart', params, callbacks); 450 }, 451 452 getMetroTrackChart : function(params, callbacks){ 453 call('geo.getMetroTrackChart', params, callbacks); 454 }, 455 456 getMetroUniqueArtistChart : function(params, callbacks){ 457 call('geo.getMetroUniqueArtistChart', params, callbacks); 458 }, 459 460 getMetroUniqueTrackChart : function(params, callbacks){ 461 call('geo.getMetroUniqueTrackChart', params, callbacks); 462 }, 463 464 getMetroWeeklyChartlist : function(params, callbacks){ 465 call('geo.getMetroWeeklyChartlist', params, callbacks); 466 }, 467 468 getMetros : function(params, callbacks){ 469 call('geo.getMetros', params, callbacks); 470 }, 471 472 getTopArtists : function(params, callbacks){ 473 call('geo.getTopArtists', params, callbacks); 474 }, 475 476 getTopTracks : function(params, callbacks){ 477 call('geo.getTopTracks', params, callbacks); 478 } 479 }; 480 481 /* Group methods. */ 482 this.group = { 483 getHype : function(params, callbacks){ 484 call('group.getHype', params, callbacks); 485 }, 486 487 getMembers : function(params, callbacks){ 488 call('group.getMembers', params, callbacks); 489 }, 490 491 getWeeklyAlbumChart : function(params, callbacks){ 492 call('group.getWeeklyAlbumChart', params, callbacks); 493 }, 494 495 getWeeklyArtistChart : function(params, callbacks){ 496 call('group.getWeeklyArtistChart', params, callbacks); 497 }, 498 499 getWeeklyChartList : function(params, callbacks){ 500 call('group.getWeeklyChartList', params, callbacks); 501 }, 502 503 getWeeklyTrackChart : function(params, callbacks){ 504 call('group.getWeeklyTrackChart', params, callbacks); 505 } 506 }; 507 508 /* Library methods. */ 509 this.library = { 510 addAlbum : function(params, session, callbacks){ 511 signedCall('library.addAlbum', params, session, callbacks, 'POST'); 512 }, 513 514 addArtist : function(params, session, callbacks){ 515 signedCall('library.addArtist', params, session, callbacks, 'POST'); 516 }, 517 518 addTrack : function(params, session, callbacks){ 519 signedCall('library.addTrack', params, session, callbacks, 'POST'); 520 }, 521 522 getAlbums : function(params, callbacks){ 523 call('library.getAlbums', params, callbacks); 524 }, 525 526 getArtists : function(params, callbacks){ 527 call('library.getArtists', params, callbacks); 528 }, 529 530 getTracks : function(params, callbacks){ 531 call('library.getTracks', params, callbacks); 532 } 533 }; 534 535 /* Playlist methods. */ 536 this.playlist = { 537 addTrack : function(params, session, callbacks){ 538 signedCall('playlist.addTrack', params, session, callbacks, 'POST'); 539 }, 540 541 create : function(params, session, callbacks){ 542 signedCall('playlist.create', params, session, callbacks, 'POST'); 543 }, 544 545 fetch : function(params, callbacks){ 546 call('playlist.fetch', params, callbacks); 547 } 548 }; 549 550 /* Radio methods. */ 551 this.radio = { 552 getPlaylist : function(params, session, callbacks){ 553 signedCall('radio.getPlaylist', params, session, callbacks); 554 }, 555 556 search : function(params, session, callbacks){ 557 signedCall('radio.search', params, session, callbacks); 558 }, 559 560 tune : function(params, session, callbacks){ 561 signedCall('radio.tune', params, session, callbacks); 562 } 563 }; 564 565 /* Tag methods. */ 566 this.tag = { 567 getInfo : function(params, callbacks){ 568 call('tag.getInfo', params, callbacks); 569 }, 570 571 getSimilar : function(params, callbacks){ 572 call('tag.getSimilar', params, callbacks); 573 }, 574 575 getTopAlbums : function(params, callbacks){ 576 call('tag.getTopAlbums', params, callbacks); 577 }, 578 579 getTopArtists : function(params, callbacks){ 580 call('tag.getTopArtists', params, callbacks); 581 }, 582 583 getTopTags : function(callbacks){ 584 call('tag.getTopTags', null, callbacks); 585 }, 586 587 getTopTracks : function(params, callbacks){ 588 call('tag.getTopTracks', params, callbacks); 589 }, 590 591 getWeeklyArtistChart : function(params, callbacks){ 592 call('tag.getWeeklyArtistChart', params, callbacks); 593 }, 594 595 getWeeklyChartList : function(params, callbacks){ 596 call('tag.getWeeklyChartList', params, callbacks); 597 }, 598 599 search : function(params, callbacks){ 600 call('tag.search', params, callbacks); 601 } 602 }; 603 604 /* Tasteometer method. */ 605 this.tasteometer = { 606 compare : function(params, callbacks){ 607 call('tasteometer.compare', params, callbacks); 608 }, 609 610 compareGroup : function(params, callbacks){ 611 call('tasteometer.compareGroup', params, callbacks); 612 } 613 }; 614 615 /* Track methods. */ 616 this.track = { 617 addTags : function(params, session, callbacks){ 618 signedCall('track.addTags', params, session, callbacks, 'POST'); 619 }, 620 621 ban : function(params, session, callbacks){ 622 signedCall('track.ban', params, session, callbacks, 'POST'); 623 }, 624 625 getBuylinks : function(params, callbacks){ 626 call('track.getBuylinks', params, callbacks); 627 }, 628 629 getCorrection : function(params, callbacks){ 630 call('track.getCorrection', params, callbacks); 631 }, 632 633 getFingerprintMetadata : function(params, callbacks){ 634 call('track.getFingerprintMetadata', params, callbacks); 635 }, 636 637 getInfo : function(params, callbacks){ 638 call('track.getInfo', params, callbacks); 639 }, 640 641 getShouts : function(params, callbacks){ 642 call('track.getShouts', params, callbacks); 643 }, 644 645 getSimilar : function(params, callbacks){ 646 call('track.getSimilar', params, callbacks); 647 }, 648 649 getTags : function(params, session, callbacks){ 650 signedCall('track.getTags', params, session, callbacks); 651 }, 652 653 getTopFans : function(params, callbacks){ 654 call('track.getTopFans', params, callbacks); 655 }, 656 657 getTopTags : function(params, callbacks){ 658 call('track.getTopTags', params, callbacks); 659 }, 660 661 love : function(params, session, callbacks){ 662 signedCall('track.love', params, session, callbacks, 'POST'); 663 }, 664 665 removeTag : function(params, session, callbacks){ 666 signedCall('track.removeTag', params, session, callbacks, 'POST'); 667 }, 668 669 scrobble : function(params, session, callbacks){ 670 /* Flatten an array of multiple tracks into an object with "array notation". */ 671 if(params.constructor.toString().indexOf("Array") != -1){ 672 var p = {}; 673 674 for(i in params){ 675 for(j in params[i]){ 676 p[j + '[' + i + ']'] = params[i][j];
677 } 678 } 679 680 params = p; 681 } 682 683 signedCall('track.scrobble', params, session, callbacks, 'POST'); 684 }, 685 686 search : function(params, callbacks){ 687 call('track.search', params, callbacks); 688 }, 689 690 share : function(params, session, callbacks){ 691 /* Build comma separated recipients string. */ 692 if(typeof(params.recipient) == 'object'){ 693 params.recipient = params.recipient.join(','); 694 } 695 696 signedCall('track.share', params, session, callbacks, 'POST'); 697 }, 698 699 unban : function(params, session, callbacks){ 700 signedCall('track.unban', params, session, callbacks, 'POST'); 701 }, 702 703 unlove : function(params, session, callbacks){ 704 signedCall('track.unlove', params, session, callbacks, 'POST'); 705 }, 706 707 updateNowPlaying : function(params, session, callbacks){ 708 signedCall('track.updateNowPlaying', params, session, callbacks, 'POST'); 709 } 710 }; 711 712 /* User methods. */ 713 this.user = { 714 getArtistTracks : function(params, callbacks){ 715 call('user.getArtistTracks', params, callbacks); 716 }, 717 718 getBannedTracks : function(params, callbacks){ 719 call('user.getBannedTracks', params, callbacks); 720 }, 721 722 getEvents : function(params, callbacks){ 723 call('user.getEvents', params, callbacks); 724 }, 725 726 getFriends : function(params, callbacks){ 727 call('user.getFriends', params, callbacks); 728 }, 729 730 getInfo : function(params, callbacks){ 731 call('user.getInfo', params, callbacks); 732 }, 733 734 getLovedTracks : function(params, callbacks){ 735 call('user.getLovedTracks', params, callbacks); 736 }, 737 738 getNeighbours : function(params, callbacks){ 739 call('user.getNeighbours', params, callbacks); 740 }, 741 742 getNewReleases : function(params, callbacks){ 743 call('user.getNewReleases', params, callbacks); 744 }, 745 746 getPastEvents : function(params, callbacks){ 747 call('user.getPastEvents', params, callbacks); 748 }, 749 750 getPersonalTags : function(params, callbacks){ 751 call('user.getPersonalTags', params, callbacks); 752 }, 753 754 getPlaylists : function(params, callbacks){ 755 call('user.getPlaylists', params, callbacks); 756 }, 757 758 getRecentStations : function(params, session, callbacks){ 759 signedCall('user.getRecentStations', params, session, callbacks); 760 }, 761 762 getRecentTracks : function(params, callbacks){ 763 call('user.getRecentTracks', params, callbacks); 764 }, 765 766 getRecommendedArtists : function(params, session, callbacks){ 767 signedCall('user.getRecommendedArtists', params, session, callbacks); 768 }, 769 770 getRecommendedEvents : function(params, session, callbacks){ 771 signedCall('user.getRecommendedEvents', params, session, callbacks); 772 }, 773 774 getShouts : function(params, callbacks){ 775 call('user.getShouts', params, callbacks); 776 }, 777 778 getTopAlbums : function(params, callbacks){ 779 call('user.getTopAlbums', params, callbacks); 780 }, 781 782 getTopArtists : function(params, callbacks){ 783 call('user.getTopArtists', params, callbacks); 784 }, 785 786 getTopTags : function(params, callbacks){ 787 call('user.getTopTags', params, callbacks); 788 }, 789 790 getTopTracks : function(params, callbacks){ 791 call('user.getTopTracks', params, callbacks); 792 }, 793 794 getWeeklyAlbumChart : function(params, callbacks){ 795 call('user.getWeeklyAlbumChart', params, callbacks); 796 }, 797 798 getWeeklyArtistChart : function(params, callbacks){ 799 call('user.getWeeklyArtistChart', params, callbacks); 800 }, 801 802 getWeeklyChartList : function(params, callbacks){ 803 call('user.getWeeklyChartList', params, callbacks); 804 }, 805 806 getWeeklyTrackChart : function(params, callbacks){ 807 call('user.getWeeklyTrackChart', params, callbacks); 808 }, 809 810 shout : function(params, session, callbacks){ 811 signedCall('user.shout', params, session, callbacks, 'POST'); 812 } 813 }; 814 815 /* Venue methods. */ 816 this.venue = { 817 getEvents : function(params, callbacks){ 818 call('venue.getEvents', params, callbacks); 819 }, 820 821 getPastEvents : function(params, callbacks){ 822 call('venue.getPastEvents', params, callbacks); 823 }, 824 825 search : function(params, callbacks){ 826 call('venue.search', params, callbacks); 827 } 828 }; 829 830 /* Private auth methods. */ 831 var auth = { 832 getApiSignature : function(params){ 833 var keys = Object.keys(params); 834 var string = ''; 835 836 keys.sort(); 837 keys.forEach(function(key) {
838 string += key + params[key]; 839 }); 840 841 string += apiSecret; 842 843 /* Needs lastfm.api.md5.js. */ 844 return md5(string); 845 } 846 }; 847}
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.