Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Moved last.fm content script, added tests

  • Loading branch information...
commit 93593c74aeed11350f6da47bab51427e04f0fa52 1 parent d1475e4
@buger authored
Showing with 6,805 additions and 745 deletions.
  1. +404 −0 lib/jasmine/jasmine-jquery.js
  2. +2 −0  lib/zepto.js
  3. +17 −0 plugins/lastfm/content_script.coffee
  4. +23 −0 plugins/lastfm/content_script.js
  5. +6 −1 plugins/lastfm/package.json
  6. +389 −0 plugins/lastfm/site_integration.coffee
  7. +602 −0 plugins/lastfm/site_integration.js
  8. +150 −0 plugins/lastfm/spec/content_script_spec.coffee
  9. +119 −0 plugins/lastfm/spec/content_script_spec.js
  10. +371 −0 plugins/lastfm/spec/fixtures/artist_charts.html
  11. +470 −0 plugins/lastfm/spec/fixtures/artist_main_top_chart.html
  12. +208 −0 plugins/lastfm/spec/fixtures/artist_similar.html
  13. +401 −0 plugins/lastfm/spec/fixtures/friends_loved.html
  14. +39 −0 plugins/lastfm/spec/fixtures/track_header.html
  15. +2,418 −0 plugins/lastfm/spec/fixtures/track_similar.html
  16. +394 −0 plugins/lastfm/spec/fixtures/user_profile_artist_chart.html
  17. +28 −0 plugins/lastfm/spec/fixtures/user_profile_library.html
  18. +352 −0 plugins/lastfm/spec/fixtures/user_profile_recent.html
  19. +410 −0 plugins/lastfm/spec/fixtures/user_profile_track_chart.html
  20. 0  plugins/lastfm/{ → spec}/lastfm_spec.coffee
  21. 0  plugins/lastfm/{ → spec}/lastfm_spec.js
  22. +1 −0  src/chromus_loader.coffee
  23. +1 −1  src/chromus_loader.js
  24. +0 −743 src/manager.js
View
404 lib/jasmine/jasmine-jquery.js
@@ -0,0 +1,404 @@
+var readFixtures = function() {
+ return jasmine.getFixtures().proxyCallTo_('read', arguments)
+}
+
+var preloadFixtures = function() {
+ jasmine.getFixtures().proxyCallTo_('preload', arguments)
+}
+
+var loadFixtures = function() {
+ jasmine.getFixtures().proxyCallTo_('load', arguments)
+}
+
+var appendLoadFixtures = function() {
+ jasmine.getFixtures().proxyCallTo_('appendLoad', arguments)
+}
+
+var setFixtures = function(html) {
+ jasmine.getFixtures().proxyCallTo_('set', arguments)
+}
+
+var appendSetFixtures = function() {
+ jasmine.getFixtures().proxyCallTo_('appendSet', arguments)
+}
+
+var sandbox = function(attributes) {
+ return jasmine.getFixtures().sandbox(attributes)
+}
+
+var spyOnEvent = function(selector, eventName) {
+ return jasmine.JQuery.events.spyOn(selector, eventName)
+}
+
+jasmine.spiedEventsKey = function (selector, eventName) {
+ return [$(selector).selector, eventName].toString();
+}
+
+jasmine.getFixtures = function() {
+ return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures()
+}
+
+jasmine.Fixtures = function() {
+ this.containerId = 'jasmine-fixtures'
+ this.fixturesCache_ = {}
+ this.fixturesPath = 'spec/fixtures'
+}
+
+jasmine.Fixtures.prototype.set = function(html) {
+ this.cleanUp()
+ this.createContainer_(html)
+}
+
+jasmine.Fixtures.prototype.appendSet= function(html) {
+ this.addToContainer_(html)
+}
+
+jasmine.Fixtures.prototype.preload = function() {
+ this.read.apply(this, arguments)
+}
+
+jasmine.Fixtures.prototype.load = function() {
+ this.cleanUp()
+ this.createContainer_(this.read.apply(this, arguments))
+}
+
+jasmine.Fixtures.prototype.appendLoad = function() {
+ this.addToContainer_(this.read.apply(this, arguments))
+}
+
+jasmine.Fixtures.prototype.read = function() {
+ var htmlChunks = []
+
+ var fixtureUrls = arguments
+ for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
+ htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex]))
+ }
+
+ return htmlChunks.join('')
+}
+
+jasmine.Fixtures.prototype.clearCache = function() {
+ this.fixturesCache_ = {}
+}
+
+jasmine.Fixtures.prototype.cleanUp = function() {
+ jQuery('#' + this.containerId).remove()
+}
+
+jasmine.Fixtures.prototype.sandbox = function(attributes) {
+ var attributesToSet = attributes || {}
+ return jQuery('<div id="sandbox" />').attr(attributesToSet)
+}
+
+jasmine.Fixtures.prototype.createContainer_ = function(html) {
+ var container
+ if(html instanceof jQuery) {
+ container = jQuery('<div id="' + this.containerId + '" />')
+ container.html(html)
+ } else {
+ container = '<div id="' + this.containerId + '">' + html + '</div>'
+ }
+ jQuery('body').append(container)
+}
+
+jasmine.Fixtures.prototype.addToContainer_ = function(html){
+ var container = jQuery('body').find('#'+this.containerId).append(html)
+ if(!container.length){
+ this.createContainer_(html)
+ }
+}
+
+jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) {
+ if (typeof this.fixturesCache_[url] === 'undefined') {
+ this.loadFixtureIntoCache_(url)
+ }
+ return this.fixturesCache_[url]
+}
+
+jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) {
+ var url = this.makeFixtureUrl_(relativeUrl)
+ var request = new XMLHttpRequest()
+ request.open("GET", url + "?" + new Date().getTime(), false)
+ request.send(null)
+ this.fixturesCache_[relativeUrl] = request.responseText
+}
+
+jasmine.Fixtures.prototype.makeFixtureUrl_ = function(relativeUrl){
+ return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl
+}
+
+jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) {
+ return this[methodName].apply(this, passedArguments)
+}
+
+
+jasmine.JQuery = function() {}
+
+jasmine.JQuery.browserTagCaseIndependentHtml = function(html) {
+ return jQuery('<div/>').append(html).html()
+}
+
+jasmine.JQuery.elementToString = function(element) {
+ var domEl = $(element).get(0)
+ if (domEl == undefined || domEl.cloneNode)
+ return jQuery('<div />').append($(element).clone()).html()
+ else
+ return element.toString()
+}
+
+jasmine.JQuery.matchersClass = {};
+
+!function(namespace) {
+ var data = {
+ spiedEvents: {},
+ handlers: []
+ }
+
+ namespace.events = {
+ spyOn: function(selector, eventName) {
+ var handler = function(e) {
+ data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] = e
+ }
+ jQuery(selector).bind(eventName, handler)
+ data.handlers.push(handler)
+ return {
+ selector: selector,
+ eventName: eventName,
+ handler: handler,
+ reset: function(){
+ delete data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)];
+ }
+ }
+ },
+
+ wasTriggered: function(selector, eventName) {
+ return !!(data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)])
+ },
+
+ wasPrevented: function(selector, eventName) {
+ return data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].isDefaultPrevented()
+ },
+
+ cleanUp: function() {
+ data.spiedEvents = {}
+ data.handlers = []
+ }
+ }
+}(jasmine.JQuery)
+
+!function(){
+ var jQueryMatchers = {
+ toHaveClass: function(className) {
+ return this.actual.hasClass(className)
+ },
+
+ toHaveCss: function(css){
+ for (var prop in css){
+ if (this.actual.css(prop) !== css[prop]) return false
+ }
+ return true
+ },
+
+ toBeVisible: function() {
+ return this.actual.is(':visible')
+ },
+
+ toBeHidden: function() {
+ return this.actual.is(':hidden')
+ },
+
+ toBeSelected: function() {
+ return this.actual.is(':selected')
+ },
+
+ toBeChecked: function() {
+ return this.actual.is(':checked')
+ },
+
+ toBeEmpty: function() {
+ return this.actual.is(':empty')
+ },
+
+ toExist: function() {
+ return $(document).find(this.actual).length
+ },
+
+ toHaveAttr: function(attributeName, expectedAttributeValue) {
+ return hasProperty(this.actual.attr(attributeName), expectedAttributeValue)
+ },
+
+ toHaveProp: function(propertyName, expectedPropertyValue) {
+ return hasProperty(this.actual.prop(propertyName), expectedPropertyValue)
+ },
+
+ toHaveId: function(id) {
+ return this.actual.attr('id') == id
+ },
+
+ toHaveHtml: function(html) {
+ return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html)
+ },
+
+ toContainHtml: function(html){
+ var actualHtml = this.actual.html()
+ var expectedHtml = jasmine.JQuery.browserTagCaseIndependentHtml(html)
+ return (actualHtml.indexOf(expectedHtml) >= 0)
+ },
+
+ toHaveText: function(text) {
+ var trimmedText = $.trim(this.actual.text())
+ if (text && jQuery.isFunction(text.test)) {
+ return text.test(trimmedText)
+ } else {
+ return trimmedText == text
+ }
+ },
+
+ toHaveValue: function(value) {
+ return this.actual.val() == value
+ },
+
+ toHaveData: function(key, expectedValue) {
+ return hasProperty(this.actual.data(key), expectedValue)
+ },
+
+ toBe: function(selector) {
+ return this.actual.is(selector)
+ },
+
+ toContain: function(selector) {
+ return this.actual.find(selector).length
+ },
+
+ toBeDisabled: function(selector){
+ return this.actual.is(':disabled')
+ },
+
+ toBeFocused: function(selector) {
+ return this.actual.is(':focus')
+ },
+
+ toHandle: function(event) {
+
+ var events = this.actual.data('events')
+
+ if(!events || !event || typeof event !== "string") {
+ return false
+ }
+
+ var namespaces = event.split(".")
+ var eventType = namespaces.shift()
+ var sortedNamespaces = namespaces.slice(0).sort()
+ var namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)")
+
+ if(events[eventType] && namespaces.length) {
+ for(var i = 0; i < events[eventType].length; i++) {
+ var namespace = events[eventType][i].namespace
+ if(namespaceRegExp.test(namespace)) {
+ return true
+ }
+ }
+ } else {
+ return events[eventType] && events[eventType].length > 0
+ }
+ },
+
+ // tests the existence of a specific event binding + handler
+ toHandleWith: function(eventName, eventHandler) {
+ var stack = this.actual.data("events")[eventName]
+ for (var i = 0; i < stack.length; i++) {
+ if (stack[i].handler == eventHandler) return true
+ }
+ return false
+ }
+ }
+
+ var hasProperty = function(actualValue, expectedValue) {
+ if (expectedValue === undefined) return actualValue !== undefined
+ return actualValue == expectedValue
+ }
+
+ var bindMatcher = function(methodName) {
+ var builtInMatcher = jasmine.Matchers.prototype[methodName]
+
+ jasmine.JQuery.matchersClass[methodName] = function() {
+ if (this.actual
+ && (this.actual instanceof jQuery
+ || jasmine.isDomNode(this.actual))) {
+ this.actual = $(this.actual)
+ var result = jQueryMatchers[methodName].apply(this, arguments)
+ var element;
+ if (this.actual.get && (element = this.actual.get()[0]) && !$.isWindow(element) && element.tagName !== "HTML")
+ this.actual = jasmine.JQuery.elementToString(this.actual)
+ return result
+ }
+
+ if (builtInMatcher) {
+ return builtInMatcher.apply(this, arguments)
+ }
+
+ return false
+ }
+ }
+
+ for(var methodName in jQueryMatchers) {
+ bindMatcher(methodName)
+ }
+}()
+
+beforeEach(function() {
+ this.addMatchers(jasmine.JQuery.matchersClass)
+ this.addMatchers({
+ toHaveBeenTriggeredOn: function(selector) {
+ this.message = function() {
+ return [
+ "Expected event " + this.actual + " to have been triggered on " + selector,
+ "Expected event " + this.actual + " not to have been triggered on " + selector
+ ]
+ }
+ return jasmine.JQuery.events.wasTriggered(selector, this.actual)
+ }
+ })
+ this.addMatchers({
+ toHaveBeenTriggered: function(){
+ var eventName = this.actual.eventName,
+ selector = this.actual.selector;
+ this.message = function() {
+ return [
+ "Expected event " + eventName + " to have been triggered on " + selector,
+ "Expected event " + eventName + " not to have been triggered on " + selector
+ ]
+ }
+ return jasmine.JQuery.events.wasTriggered(selector, eventName)
+ }
+ })
+ this.addMatchers({
+ toHaveBeenPreventedOn: function(selector) {
+ this.message = function() {
+ return [
+ "Expected event " + this.actual + " to have been prevented on " + selector,
+ "Expected event " + this.actual + " not to have been prevented on " + selector
+ ]
+ }
+ return jasmine.JQuery.events.wasPrevented(selector, this.actual)
+ }
+ })
+ this.addMatchers({
+ toHaveBeenPrevented: function() {
+ var eventName = this.actual.eventName,
+ selector = this.actual.selector;
+ this.message = function() {
+ return [
+ "Expected event " + eventName + " to have been prevented on " + selector,
+ "Expected event " + eventName + " not to have been prevented on " + selector
+ ]
+ }
+ return jasmine.JQuery.events.wasPrevented(selector, eventName)
+ }
+ })
+})
+
+afterEach(function() {
+ jasmine.getFixtures().cleanUp()
+ jasmine.JQuery.events.cleanUp()
+})
View
2  lib/zepto.js
@@ -1434,3 +1434,5 @@ window.Zepto = Zepto;
$.fn[m] = function(callback){ return this.bind(m, callback) }
});
})(Zepto);
+
+jQuery = Zepto
View
17 plugins/lastfm/content_script.coffee
@@ -0,0 +1,17 @@
+_.extend @chromus.utils,
+ urlType: (url) ->
+ params = _.rest url.replace(/https?\:\/\//,'').split('/')
+
+ switch params[0]
+ when "music"
+ if params.length is 2
+ "band"
+ else if params[2] is '_'
+ "song"
+ else if params[2][0] is '+'
+ "music"
+ else if params.length is 3
+ "album"
+
+ else params[0]
+
View
23 plugins/lastfm/content_script.js
@@ -1,2 +1,25 @@
+(function() {
+ _.extend(this.chromus.utils, {
+ urlType: function(url) {
+ var params;
+ params = _.rest(url.replace(/https?\:\/\//, '').split('/'));
+ switch (params[0]) {
+ case "music":
+ if (params.length === 2) {
+ return "band";
+ } else if (params[2] === '_') {
+ return "song";
+ } else if (params[2][0] === '+') {
+ return "music";
+ } else if (params.length === 3) {
+ return "album";
+ }
+ break;
+ default:
+ return params[0];
+ }
+ }
+ });
+}).call(this);
View
7 plugins/lastfm/package.json
@@ -13,6 +13,11 @@
"popup!lastfm_ui.js",
"css!lastfm.css",
- "bg_spec!lastfm_spec.js"
+ "bg_spec!spec/lastfm_spec.js",
+
+ "test_mode!content_script.js",
+
+ "popup_spec!site_integration.js",
+ "popup_spec!spec/content_script_spec.js"
]
}
View
389 plugins/lastfm/site_integration.coffee
@@ -0,0 +1,389 @@
+###
+Class WrapperManager
+
+This Class is responsible for managing page change for Last.fm site.
+
+We have a lot of blocks with songs on site.
+For each block you need to create new wrapper and register it in InjectionManager like this:
+
+manager.registerWrapper('ul.mediumChartWithImages', SongsChart)
+
+All wrappers have same interface and inherited from MusicDomElement.
+###
+
+class WrapperManager
+
+ registred_wrappers: {}
+
+ constructor: ->
+ @init()
+
+
+ init: ->
+ @artist = undefined
+ @track_count = 0
+ @container_count = 0
+
+ try
+ title = document.querySelector("meta[property=\"og:title\"]").content
+ type = document.querySelector("meta[property=\"og:type\"]").content
+
+ if type is "song" or type is "band" or type is "album"
+ @artist = title.split("")[0]
+ catch e
+ type = null
+
+ try
+ @artist = document.querySelector("#libraryBreadcrumb h2").innerText
+ catch e
+
+
+ ###
+ WrapperManager#wrapMusicElements() -> null
+
+ Uses all registered wrappers to handle all the possible elements on the page.
+ ###
+ wrapMusicElements: ->
+
+ # if(window.location.toString().match(/\/event\//))
+ # return
+ artist = @artist
+ for css_expr of @registred_wrappers
+
+ wrapper = @registred_wrappers[css_expr]
+ elements = document.querySelectorAll(css_expr)
+ i = 0
+
+ console.warn "Using wrapper: #{css_expr}" if elements.length > 0
+
+
+ while i < elements.length
+ class_name = elements[i].className or ""
+
+ unless class_name.match("with_vk_search")
+ try
+ new wrapper(elements[i], artist).injectSearch()
+ @container_count += 1
+ catch e
+ console.warn elements[i]
+ console.warn e.message
+ elements[i].className += " with_vk_search"
+ elements[i].setAttribute "data-index-number", @container_count
+ i++
+
+ registerWrapper: (css_expr, wrapper) ->
+ @registred_wrappers[css_expr] = wrapper
+
+manager = new WrapperManager()
+
+###
+Base Class for all wrappers
+###
+class MusicDomElement
+
+ child_items_pattern: "tbody tr:not(.artist)"
+
+ constructor: (element, artist) ->
+ @element = element
+ @artist = artist
+
+ ###
+ MusicDomElement#getTrack(element) -> Array
+ - element (Element): table row, li, or other element with track information
+
+ Get track information from given element.
+ Returns Array -> [artist, track]
+ ###
+ getTrack: (el) ->
+ console.error "You should redefine this function"
+
+
+ ###
+ MusicDomElement#inserLink(element, track) -> Array
+ - element (Element): table row, li, or other element with track information
+ - track (String): Track obtained from MusicDomElement#getTrack
+
+ Insert play and search link
+ ###
+ insertLink: (el, track) ->
+ console.error "Abstract function"
+
+
+ ###
+ Finds all parent blocks matching a pattern, and injects play and search links into all matching childs.
+ ###
+ injectSearch: ->
+ track = undefined
+ return false unless @element
+
+ childs = @element.querySelectorAll(@child_items_pattern)
+ counter = 0
+ i = 0
+
+ while i < childs.length
+ try
+ track_info = @getTrack(childs[i])
+ continue unless track_info
+ if track_info[0]
+ childs[i].className += " ex_container"
+ childs[i].setAttribute "data-artist", (track_info[0]||@artist)
+ track = undefined
+ if track_info[1]
+ childs[i].setAttribute "data-song", track_info[1]
+ track = track_info.join(" - ")
+ else
+ track = track_info[0]
+ if track_info[2]
+ childs[i].className += " ex_album"
+ childs[i].setAttribute "data-album", track_info[2]
+ else
+ childs[i].className += " ex_artist"
+ unless @insertLink(childs[i], track) is false
+ if track_info[1]
+ childs[i].setAttribute "data-index-number", counter
+ counter += 1
+ catch e
+ console.error "Can't wrap row:", e.message, childs[i]
+ i++
+
+ generateAudioLink: (track) ->
+ link = "<a href=\"javascript:;\" target='_blank' class='sm2_button' title='Play song' id='ex_button_" + manager.track_count + "' >" + track + "</a>"
+ manager.track_count += 1
+ link
+
+
+###
+ Most popular block. User/Artist charts.
+###
+class TrackList extends MusicDomElement
+
+ getTrack: (row) ->
+ track_info = row.querySelectorAll(".subjectCell a")
+ track_info = row.querySelectorAll(".subject a") if track_info.length is 0
+ track_info = row.querySelectorAll(".track a") if track_info.length is 0
+
+ # If inside artist page
+ if @artist and not @element.className.match("big") and not document.getElementById("thePlaylist")
+ [@artist, track_info[0].innerText]
+ else
+ [track_info[0].innerText, (if track_info[1] then track_info[1].innerText else `undefined`)]
+
+ insertLink: (row, track) ->
+ td = row.querySelector("td.smallmultibuttonCell, td.multibuttonCell")
+ td_playbtn = row.querySelector("td.playbuttonCell")
+ if td_playbtn
+ td_playbtn.innerHTML = @generateAudioLink(track)
+ else
+ false
+
+manager.registerWrapper "table.tracklist, table.chart", TrackList
+
+
+###
+Track page, http://www.lastfm.ru/music/Ke$ha/_/TiK+ToK
+###
+class SingleTrack extends MusicDomElement
+
+ child_items_pattern: "span[itemprop=name]"
+
+ getTrack: ->
+ # var artist = document.querySelector('.breadcrumb a').innerHTML
+ song = document.querySelector(".track-overview h1 span[itemprop=name]").innerText
+ [@artist, song]
+
+ insertLink: (el, track) ->
+ el.innerHTML = @generateAudioLink(track) + " " + el.innerHTML
+ el.className = el.className + " ex_container"
+
+manager.registerWrapper ".track-overview h1", SingleTrack
+
+
+class FriendsLoved extends MusicDomElement
+
+ child_items_pattern: "li"
+
+ getTrack: (li) ->
+ track_info = li.querySelectorAll(".object strong a")
+ [track_info[track_info.length - 2].innerText, track_info[track_info.length - 1].innerText]
+
+ insertLink: (li, track) ->
+ elm = li.querySelector(".object")
+ elm.querySelector(".previewbutton").style.display = "none" if elm.querySelector(".previewbutton")
+ elm.innerHTML = generateAudioLink(track) + elm.innerHTML
+
+manager.registerWrapper "#friendsLoved", FriendsLoved
+
+
+class NowPlaying extends MusicDomElement
+
+ child_items_pattern: "li"
+
+ getTrack: (li) ->
+ track_info = li.querySelectorAll(".track a")
+ [track_info[track_info.length - 2].innerText, track_info[track_info.length - 1].innerText]
+
+ insertLink: (li, track) ->
+ elm = li.querySelector(".track")
+ elm.innerHTML = @generateAudioLink(track) + elm.innerHTML
+
+manager.registerWrapper "#nowPlaying", NowPlaying
+
+
+###
+Used in artists library
+###
+class ArtistsLargeThumbnails extends MusicDomElement
+
+ child_items_pattern: "li"
+
+ getTrack: (li) ->
+ try
+ artist = li.querySelector("strong.name").innerHTML
+ [artist]
+
+ insertLink: (li, track) ->
+ playbtn = li.querySelector(".playbutton")
+ li.removeChild playbtn if playbtn
+ li.innerHTML += @generateAudioLink(track)
+
+manager.registerWrapper "ul.artistsLarge", ArtistsLargeThumbnails
+
+###
+www.lastfm.ru/home/recs
+###
+class ArtistRecomendations extends MusicDomElement
+
+ child_items_pattern: "li"
+
+ getTrack: (li) ->
+ artist = li.querySelector("h2 a.name").innerHTML
+ [artist]
+
+ insertLink: (li, track) ->
+ elm = li.querySelector("h2")
+ playbtn = elm.querySelector("a.playbutton")
+ elm.removeChild playbtn if playbtn
+ elm.innerHTML = @generateAudioLink(track) + elm.innerHTML
+
+manager.registerWrapper "ul#artistRecs", ArtistRecomendations
+
+###
+www.lastfm.ru/home Recomendations block
+###
+class ArtistRecsPreview extends MusicDomElement
+
+ child_items_pattern: "li"
+
+ getTrack: (li) ->
+ artist = li.querySelector("strong.name").innerHTML
+ [artist]
+
+ insertLink: (li, track) ->
+ elm = li.querySelector(".container")
+ elm.innerHTML += @generateAudioLink(track)
+ playbtn = elm.querySelector(".playbutton")
+ elm.removeChild playbtn if playbtn
+
+manager.registerWrapper "ul.artistRecs", ArtistRecsPreview
+
+
+###
+www.lastfm.ru/music/Camille/+similar
+###
+class ArtistsWithInfo extends MusicDomElement
+
+ child_items_pattern: "li"
+
+ getTrack: (li) ->
+ artist = li.querySelector("a.artist strong").innerHTML
+ [artist]
+
+ insertLink: (li, track) ->
+ li.innerHTML += @generateAudioLink(track)
+ playbtn = li.querySelector(".playbutton")
+ li.removeChild playbtn if playbtn
+
+manager.registerWrapper "ul.artistsWithInfo", ArtistsWithInfo
+
+###
+www.lastfm.ru/home
+www.lastfm.ru/music/Carla+Bruni/+albums
+###
+class AlbumsMedium extends MusicDomElement
+
+ child_items_pattern: "li"
+
+ getTrack: (li) ->
+ return false if li.parentNode.className.match(/lfmDropDownBody/)
+ artist = li.querySelector("a.artist").innerHTML
+ album = li.querySelector("strong a").childNodes[1].nodeValue.replace(/^\s+/, "")
+ [artist, `undefined`, album]
+
+ insertLink: (li, track) ->
+ li.innerHTML += @generateAudioLink(track)
+ elm = li.querySelector("div.resContainer")
+ playbtn = elm.querySelector(".playbutton")
+ elm.removeChild playbtn if playbtn
+
+manager.registerWrapper "ul.albumsMedium, ul.albumsLarge", AlbumsMedium
+
+###
+www.lastfm.ru/user/buger_swamp/library/music/Carla+Bruni
+###
+class AlbumsLibrary extends MusicDomElement
+
+ child_items_pattern: "li"
+
+ getTrack: (li) ->
+ album = li.querySelector("strong.title").innerHTML
+ [@artist, `undefined`, album]
+
+ insertLink: (li, track) ->
+ elm = li.querySelector("span.albumCover")
+ elm.innerHTML = @generateAudioLink(track) + elm.innerHTML
+
+manager.registerWrapper "#albumstrip ul", AlbumsLibrary
+
+###
+www.lastfm.ru/home/newreleases
+###
+class NewReleases extends MusicDomElement
+
+ getTrack: (tr) ->
+ album = tr.querySelector(".release a.title strong")
+ return false unless album
+ album = album.innerHTML
+ artist = tr.querySelector(".library a.artist strong").innerHTML
+ [artist, `undefined`, album]
+
+ insertLink: (tr, track) ->
+ elm = tr.querySelector(".release")
+ elm.innerHTML = @generateAudioLink(track) + elm.innerHTML
+ playbtn = elm.querySelector(".playbutton")
+ elm.removeChild playbtn if playbtn
+
+manager.registerWrapper "#newReleases", NewReleases
+
+###
+www.last.fm/tag/baroque%20pop Recently added block
+###
+class RecentAlbums extends MusicDomElement
+
+ child_items_pattern: "li"
+
+ getTrack: (li) ->
+ return false if li.parentNode.className.match(/lfmDropDownBody/)
+ album = li.querySelector("a.album strong").innerHTML
+ artist = li.querySelector("a.artist").innerHTML
+ console.log "Album:", album
+ [artist, `undefined`, album]
+
+ insertLink: (li, track) ->
+ li.innerHTML += @generateAudioLink(track)
+ playbtn = li.querySelector(".playbutton")
+ li.removeChild playbtn if playbtn
+
+manager.registerWrapper "ul.recentAlbums", RecentAlbums
+
+
+chromus.plugins.lastfm.manager = manager
View
602 plugins/lastfm/site_integration.js
@@ -0,0 +1,602 @@
+
+/*
+Class WrapperManager
+
+This Class is responsible for managing page change for Last.fm site.
+
+We have a lot of blocks with songs on site.
+For each block you need to create new wrapper and register it in InjectionManager like this:
+
+manager.registerWrapper('ul.mediumChartWithImages', SongsChart)
+
+All wrappers have same interface and inherited from MusicDomElement.
+*/
+
+(function() {
+ var AlbumsLibrary, AlbumsMedium, ArtistRecomendations, ArtistRecsPreview, ArtistsLargeThumbnails, ArtistsWithInfo, FriendsLoved, MusicDomElement, NewReleases, NowPlaying, RecentAlbums, SingleTrack, TrackList, WrapperManager, manager,
+ __hasProp = Object.prototype.hasOwnProperty,
+ __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
+
+ WrapperManager = (function() {
+
+ WrapperManager.prototype.registred_wrappers = {};
+
+ function WrapperManager() {
+ this.init();
+ }
+
+ WrapperManager.prototype.init = function() {
+ var title, type;
+ this.artist = void 0;
+ this.track_count = 0;
+ this.container_count = 0;
+ try {
+ title = document.querySelector("meta[property=\"og:title\"]").content;
+ type = document.querySelector("meta[property=\"og:type\"]").content;
+ if (type === "song" || type === "band" || type === "album") {
+ return this.artist = title.split("")[0];
+ }
+ } catch (e) {
+ type = null;
+ try {
+ return this.artist = document.querySelector("#libraryBreadcrumb h2").innerText;
+ } catch (e) {
+
+ }
+ }
+ };
+
+ /*
+ WrapperManager#wrapMusicElements() -> null
+
+ Uses all registered wrappers to handle all the possible elements on the page.
+ */
+
+ WrapperManager.prototype.wrapMusicElements = function() {
+ var artist, class_name, css_expr, elements, i, wrapper, _results;
+ artist = this.artist;
+ _results = [];
+ for (css_expr in this.registred_wrappers) {
+ wrapper = this.registred_wrappers[css_expr];
+ elements = document.querySelectorAll(css_expr);
+ i = 0;
+ if (elements.length > 0) console.warn("Using wrapper: " + css_expr);
+ _results.push((function() {
+ var _results2;
+ _results2 = [];
+ while (i < elements.length) {
+ class_name = elements[i].className || "";
+ if (!class_name.match("with_vk_search")) {
+ try {
+ new wrapper(elements[i], artist).injectSearch();
+ this.container_count += 1;
+ } catch (e) {
+ console.warn(elements[i]);
+ console.warn(e.message);
+ }
+ elements[i].className += " with_vk_search";
+ elements[i].setAttribute("data-index-number", this.container_count);
+ }
+ _results2.push(i++);
+ }
+ return _results2;
+ }).call(this));
+ }
+ return _results;
+ };
+
+ WrapperManager.prototype.registerWrapper = function(css_expr, wrapper) {
+ return this.registred_wrappers[css_expr] = wrapper;
+ };
+
+ return WrapperManager;
+
+ })();
+
+ manager = new WrapperManager();
+
+ /*
+ Base Class for all wrappers
+ */
+
+ MusicDomElement = (function() {
+
+ MusicDomElement.prototype.child_items_pattern = "tbody tr:not(.artist)";
+
+ function MusicDomElement(element, artist) {
+ this.element = element;
+ this.artist = artist;
+ }
+
+ /*
+ MusicDomElement#getTrack(element) -> Array
+ - element (Element): table row, li, or other element with track information
+
+ Get track information from given element.
+ Returns Array -> [artist, track]
+ */
+
+ MusicDomElement.prototype.getTrack = function(el) {
+ return console.error("You should redefine this function");
+ };
+
+ /*
+ MusicDomElement#inserLink(element, track) -> Array
+ - element (Element): table row, li, or other element with track information
+ - track (String): Track obtained from MusicDomElement#getTrack
+
+ Insert play and search link
+ */
+
+ MusicDomElement.prototype.insertLink = function(el, track) {
+ return console.error("Abstract function");
+ };
+
+ /*
+ Finds all parent blocks matching a pattern, and injects play and search links into all matching childs.
+ */
+
+ MusicDomElement.prototype.injectSearch = function() {
+ var childs, counter, i, track, track_info, _results;
+ track = void 0;
+ if (!this.element) return false;
+ childs = this.element.querySelectorAll(this.child_items_pattern);
+ counter = 0;
+ i = 0;
+ _results = [];
+ while (i < childs.length) {
+ try {
+ track_info = this.getTrack(childs[i]);
+ if (!track_info) continue;
+ if (track_info[0]) {
+ childs[i].className += " ex_container";
+ childs[i].setAttribute("data-artist", track_info[0] || this.artist);
+ track = void 0;
+ if (track_info[1]) {
+ childs[i].setAttribute("data-song", track_info[1]);
+ track = track_info.join(" - ");
+ } else {
+ track = track_info[0];
+ if (track_info[2]) {
+ childs[i].className += " ex_album";
+ childs[i].setAttribute("data-album", track_info[2]);
+ } else {
+ childs[i].className += " ex_artist";
+ }
+ }
+ if (this.insertLink(childs[i], track) !== false) {
+ if (track_info[1]) {
+ childs[i].setAttribute("data-index-number", counter);
+ counter += 1;
+ }
+ }
+ }
+ } catch (e) {
+ console.error("Can't wrap row:", e.message, childs[i]);
+ }
+ _results.push(i++);
+ }
+ return _results;
+ };
+
+ MusicDomElement.prototype.generateAudioLink = function(track) {
+ var link;
+ link = "<a href=\"javascript:;\" target='_blank' class='sm2_button' title='Play song' id='ex_button_" + manager.track_count + "' >" + track + "</a>";
+ manager.track_count += 1;
+ return link;
+ };
+
+ return MusicDomElement;
+
+ })();
+
+ /*
+ Most popular block. User/Artist charts.
+ */
+
+ TrackList = (function(_super) {
+
+ __extends(TrackList, _super);
+
+ function TrackList() {
+ TrackList.__super__.constructor.apply(this, arguments);
+ }
+
+ TrackList.prototype.getTrack = function(row) {
+ var track_info;
+ track_info = row.querySelectorAll(".subjectCell a");
+ if (track_info.length === 0) track_info = row.querySelectorAll(".subject a");
+ if (track_info.length === 0) track_info = row.querySelectorAll(".track a");
+ if (this.artist && !this.element.className.match("big") && !document.getElementById("thePlaylist")) {
+ return [this.artist, track_info[0].innerText];
+ } else {
+ return [track_info[0].innerText, (track_info[1] ? track_info[1].innerText : undefined)];
+ }
+ };
+
+ TrackList.prototype.insertLink = function(row, track) {
+ var td, td_playbtn;
+ td = row.querySelector("td.smallmultibuttonCell, td.multibuttonCell");
+ td_playbtn = row.querySelector("td.playbuttonCell");
+ if (td_playbtn) {
+ return td_playbtn.innerHTML = this.generateAudioLink(track);
+ } else {
+ return false;
+ }
+ };
+
+ return TrackList;
+
+ })(MusicDomElement);
+
+ manager.registerWrapper("table.tracklist, table.chart", TrackList);
+
+ /*
+ Track page, http://www.lastfm.ru/music/Ke$ha/_/TiK+ToK
+ */
+
+ SingleTrack = (function(_super) {
+
+ __extends(SingleTrack, _super);
+
+ function SingleTrack() {
+ SingleTrack.__super__.constructor.apply(this, arguments);
+ }
+
+ SingleTrack.prototype.child_items_pattern = "span[itemprop=name]";
+
+ SingleTrack.prototype.getTrack = function() {
+ var song;
+ song = document.querySelector(".track-overview h1 span[itemprop=name]").innerText;
+ return [this.artist, song];
+ };
+
+ SingleTrack.prototype.insertLink = function(el, track) {
+ el.innerHTML = this.generateAudioLink(track) + " " + el.innerHTML;
+ return el.className = el.className + " ex_container";
+ };
+
+ return SingleTrack;
+
+ })(MusicDomElement);
+
+ manager.registerWrapper(".track-overview h1", SingleTrack);
+
+ FriendsLoved = (function(_super) {
+
+ __extends(FriendsLoved, _super);
+
+ function FriendsLoved() {
+ FriendsLoved.__super__.constructor.apply(this, arguments);
+ }
+
+ FriendsLoved.prototype.child_items_pattern = "li";
+
+ FriendsLoved.prototype.getTrack = function(li) {
+ var track_info;
+ track_info = li.querySelectorAll(".object strong a");
+ return [track_info[track_info.length - 2].innerText, track_info[track_info.length - 1].innerText];
+ };
+
+ FriendsLoved.prototype.insertLink = function(li, track) {
+ var elm;
+ elm = li.querySelector(".object");
+ if (elm.querySelector(".previewbutton")) {
+ elm.querySelector(".previewbutton").style.display = "none";
+ }
+ return elm.innerHTML = generateAudioLink(track) + elm.innerHTML;
+ };
+
+ return FriendsLoved;
+
+ })(MusicDomElement);
+
+ manager.registerWrapper("#friendsLoved", FriendsLoved);
+
+ NowPlaying = (function(_super) {
+
+ __extends(NowPlaying, _super);
+
+ function NowPlaying() {
+ NowPlaying.__super__.constructor.apply(this, arguments);
+ }
+
+ NowPlaying.prototype.child_items_pattern = "li";
+
+ NowPlaying.prototype.getTrack = function(li) {
+ var track_info;
+ track_info = li.querySelectorAll(".track a");
+ return [track_info[track_info.length - 2].innerText, track_info[track_info.length - 1].innerText];
+ };
+
+ NowPlaying.prototype.insertLink = function(li, track) {
+ var elm;
+ elm = li.querySelector(".track");
+ return elm.innerHTML = this.generateAudioLink(track) + elm.innerHTML;
+ };
+
+ return NowPlaying;
+
+ })(MusicDomElement);
+
+ manager.registerWrapper("#nowPlaying", NowPlaying);
+
+ /*
+ Used in artists library
+ */
+
+ ArtistsLargeThumbnails = (function(_super) {
+
+ __extends(ArtistsLargeThumbnails, _super);
+
+ function ArtistsLargeThumbnails() {
+ ArtistsLargeThumbnails.__super__.constructor.apply(this, arguments);
+ }
+
+ ArtistsLargeThumbnails.prototype.child_items_pattern = "li";
+
+ ArtistsLargeThumbnails.prototype.getTrack = function(li) {
+ var artist;
+ try {
+ artist = li.querySelector("strong.name").innerHTML;
+ } catch (_error) {}
+ return [artist];
+ };
+
+ ArtistsLargeThumbnails.prototype.insertLink = function(li, track) {
+ var playbtn;
+ playbtn = li.querySelector(".playbutton");
+ if (playbtn) li.removeChild(playbtn);
+ return li.innerHTML += this.generateAudioLink(track);
+ };
+
+ return ArtistsLargeThumbnails;
+
+ })(MusicDomElement);
+
+ manager.registerWrapper("ul.artistsLarge", ArtistsLargeThumbnails);
+
+ /*
+ www.lastfm.ru/home/recs
+ */
+
+ ArtistRecomendations = (function(_super) {
+
+ __extends(ArtistRecomendations, _super);
+
+ function ArtistRecomendations() {
+ ArtistRecomendations.__super__.constructor.apply(this, arguments);
+ }
+
+ ArtistRecomendations.prototype.child_items_pattern = "li";
+
+ ArtistRecomendations.prototype.getTrack = function(li) {
+ var artist;
+ artist = li.querySelector("h2 a.name").innerHTML;
+ return [artist];
+ };
+
+ ArtistRecomendations.prototype.insertLink = function(li, track) {
+ var elm, playbtn;
+ elm = li.querySelector("h2");
+ playbtn = elm.querySelector("a.playbutton");
+ if (playbtn) elm.removeChild(playbtn);
+ return elm.innerHTML = this.generateAudioLink(track) + elm.innerHTML;
+ };
+
+ return ArtistRecomendations;
+
+ })(MusicDomElement);
+
+ manager.registerWrapper("ul#artistRecs", ArtistRecomendations);
+
+ /*
+ www.lastfm.ru/home Recomendations block
+ */
+
+ ArtistRecsPreview = (function(_super) {
+
+ __extends(ArtistRecsPreview, _super);
+
+ function ArtistRecsPreview() {
+ ArtistRecsPreview.__super__.constructor.apply(this, arguments);
+ }
+
+ ArtistRecsPreview.prototype.child_items_pattern = "li";
+
+ ArtistRecsPreview.prototype.getTrack = function(li) {
+ var artist;
+ artist = li.querySelector("strong.name").innerHTML;
+ return [artist];
+ };
+
+ ArtistRecsPreview.prototype.insertLink = function(li, track) {
+ var elm, playbtn;
+ elm = li.querySelector(".container");
+ elm.innerHTML += this.generateAudioLink(track);
+ playbtn = elm.querySelector(".playbutton");
+ if (playbtn) return elm.removeChild(playbtn);
+ };
+
+ return ArtistRecsPreview;
+
+ })(MusicDomElement);
+
+ manager.registerWrapper("ul.artistRecs", ArtistRecsPreview);
+
+ /*
+ www.lastfm.ru/music/Camille/+similar
+ */
+
+ ArtistsWithInfo = (function(_super) {
+
+ __extends(ArtistsWithInfo, _super);
+
+ function ArtistsWithInfo() {
+ ArtistsWithInfo.__super__.constructor.apply(this, arguments);
+ }
+
+ ArtistsWithInfo.prototype.child_items_pattern = "li";
+
+ ArtistsWithInfo.prototype.getTrack = function(li) {
+ var artist;
+ artist = li.querySelector("a.artist strong").innerHTML;
+ return [artist];
+ };
+
+ ArtistsWithInfo.prototype.insertLink = function(li, track) {
+ var playbtn;
+ li.innerHTML += this.generateAudioLink(track);
+ playbtn = li.querySelector(".playbutton");
+ if (playbtn) return li.removeChild(playbtn);
+ };
+
+ return ArtistsWithInfo;
+
+ })(MusicDomElement);
+
+ manager.registerWrapper("ul.artistsWithInfo", ArtistsWithInfo);
+
+ /*
+ www.lastfm.ru/home
+ www.lastfm.ru/music/Carla+Bruni/+albums
+ */
+
+ AlbumsMedium = (function(_super) {
+
+ __extends(AlbumsMedium, _super);
+
+ function AlbumsMedium() {
+ AlbumsMedium.__super__.constructor.apply(this, arguments);
+ }
+
+ AlbumsMedium.prototype.child_items_pattern = "li";
+
+ AlbumsMedium.prototype.getTrack = function(li) {
+ var album, artist;
+ if (li.parentNode.className.match(/lfmDropDownBody/)) return false;
+ artist = li.querySelector("a.artist").innerHTML;
+ album = li.querySelector("strong a").childNodes[1].nodeValue.replace(/^\s+/, "");
+ return [artist, undefined, album];
+ };
+
+ AlbumsMedium.prototype.insertLink = function(li, track) {
+ var elm, playbtn;
+ li.innerHTML += this.generateAudioLink(track);
+ elm = li.querySelector("div.resContainer");
+ playbtn = elm.querySelector(".playbutton");
+ if (playbtn) return elm.removeChild(playbtn);
+ };
+
+ return AlbumsMedium;
+
+ })(MusicDomElement);
+
+ manager.registerWrapper("ul.albumsMedium, ul.albumsLarge", AlbumsMedium);
+
+ /*
+ www.lastfm.ru/user/buger_swamp/library/music/Carla+Bruni
+ */
+
+ AlbumsLibrary = (function(_super) {
+
+ __extends(AlbumsLibrary, _super);
+
+ function AlbumsLibrary() {
+ AlbumsLibrary.__super__.constructor.apply(this, arguments);
+ }
+
+ AlbumsLibrary.prototype.child_items_pattern = "li";
+
+ AlbumsLibrary.prototype.getTrack = function(li) {
+ var album;
+ album = li.querySelector("strong.title").innerHTML;
+ return [this.artist, undefined, album];
+ };
+
+ AlbumsLibrary.prototype.insertLink = function(li, track) {
+ var elm;
+ elm = li.querySelector("span.albumCover");
+ return elm.innerHTML = this.generateAudioLink(track) + elm.innerHTML;
+ };
+
+ return AlbumsLibrary;
+
+ })(MusicDomElement);
+
+ manager.registerWrapper("#albumstrip ul", AlbumsLibrary);
+
+ /*
+ www.lastfm.ru/home/newreleases
+ */
+
+ NewReleases = (function(_super) {
+
+ __extends(NewReleases, _super);
+
+ function NewReleases() {
+ NewReleases.__super__.constructor.apply(this, arguments);
+ }
+
+ NewReleases.prototype.getTrack = function(tr) {
+ var album, artist;
+ album = tr.querySelector(".release a.title strong");
+ if (!album) return false;
+ album = album.innerHTML;
+ artist = tr.querySelector(".library a.artist strong").innerHTML;
+ return [artist, undefined, album];
+ };
+
+ NewReleases.prototype.insertLink = function(tr, track) {
+ var elm, playbtn;
+ elm = tr.querySelector(".release");
+ elm.innerHTML = this.generateAudioLink(track) + elm.innerHTML;
+ playbtn = elm.querySelector(".playbutton");
+ if (playbtn) return elm.removeChild(playbtn);
+ };
+
+ return NewReleases;
+
+ })(MusicDomElement);
+
+ manager.registerWrapper("#newReleases", NewReleases);
+
+ /*
+ www.last.fm/tag/baroque%20pop Recently added block
+ */
+
+ RecentAlbums = (function(_super) {
+
+ __extends(RecentAlbums, _super);
+
+ function RecentAlbums() {
+ RecentAlbums.__super__.constructor.apply(this, arguments);
+ }
+
+ RecentAlbums.prototype.child_items_pattern = "li";
+
+ RecentAlbums.prototype.getTrack = function(li) {
+ var album, artist;
+ if (li.parentNode.className.match(/lfmDropDownBody/)) return false;
+ album = li.querySelector("a.album strong").innerHTML;
+ artist = li.querySelector("a.artist").innerHTML;
+ console.log("Album:", album);
+ return [artist, undefined, album];
+ };
+
+ RecentAlbums.prototype.insertLink = function(li, track) {
+ var playbtn;
+ li.innerHTML += this.generateAudioLink(track);
+ playbtn = li.querySelector(".playbutton");
+ if (playbtn) return li.removeChild(playbtn);
+ };
+
+ return RecentAlbums;
+
+ })(MusicDomElement);
+
+ manager.registerWrapper("ul.recentAlbums", RecentAlbums);
+
+ chromus.plugins.lastfm.manager = manager;
+
+}).call(this);
View
150 plugins/lastfm/spec/content_script_spec.coffee
@@ -0,0 +1,150 @@
+describe "LastFM content script", ->
+
+ lastfm = chromus.plugins.lastfm
+ manager = chromus.plugins.lastfm.manager
+
+
+ beforeEach ->
+ plugin_path = chromus.plugins_info.lastfm.path
+ plugin_path += "/spec/fixtures"
+
+ jasmine.getFixtures().fixturesPath = plugin_path
+
+ it "should get link type", ->
+ links =
+ "/user/buger_swamp": "user"
+ "/music/Blur": "band"
+ "/music/Graham+Coxon/_/What%27ll+It+Take": "song"
+ "/music/Graham+Coxon/Happiness+In+Magazines": "album"
+ "/music/Graham+Coxon/+albums": "music"
+
+ for own link of links
+ expect(chromus.utils.urlType("http://last.fm"+link)).toBe links[link]
+
+
+ # class TrackList
+ it "should identify profile recent tracks", ->
+ loadFixtures("user_profile_recent.html")
+
+ manager.init()
+ manager.wrapMusicElements()
+
+ expect($('tr[data-song]').length).toBe 15
+ expect($('tr[data-song]').data('artist')).toBe 'Blur'
+ expect($('tr[data-song]').data('song')).toBe 'Tender'
+
+
+ # class ArtistsLargeThumbnails
+ it "should identify profile library", ->
+ loadFixtures("user_profile_library.html")
+
+ manager.init()
+ manager.wrapMusicElements()
+
+ expect($('[data-song]').length).toBe 0
+ expect($('[data-artist]').length).toBe 8
+ expect($('[data-artist]').data('artist')).toBe 'ДДТ'
+
+
+ # class TrackList
+ it "should identify profile track chart", ->
+ loadFixtures("user_profile_track_chart.html")
+
+ manager.init()
+ manager.wrapMusicElements()
+
+ expect($('[data-song]').length).toBe 15
+ expect($('[data-artist]').length).toBe 15
+ expect($('[data-artist]').data('artist')).toBe 'Nick Drake'
+ expect($('[data-artist]').data('song')).toBe 'Things Behind The Sun'
+
+
+ # class TrackList
+ it "should identify profile artist chart", ->
+ loadFixtures("user_profile_artist_chart.html")
+
+ manager.init()
+ manager.wrapMusicElements()
+
+ expect($('[data-song]').length).toBe 0
+ expect($('[data-artist]').length).toBe 15
+ expect($('[data-artist]').data('artist')).toBe 'Damien Rice'
+
+
+ # class TrackList
+ it "should identify arthist top chart", ->
+ loadFixtures("artist_main_top_chart.html")
+
+ manager.init()
+ manager.wrapMusicElements()
+
+ expect($('[data-song]').length).toBe 15
+ expect($('[data-artist]').length).toBe 15
+ expect($('[data-artist]').data('artist')).toBe 'ДДТ'
+ expect($('[data-artist]').data('song')).toBe 'Дождь'
+
+
+ # class TrackList
+ it "should identify artist charts", ->
+ loadFixtures("artist_charts.html")
+
+ manager.init()
+ manager.wrapMusicElements()
+
+ expect($('[data-song]').length).toBe 14
+ expect($('[data-artist]').length).toBe 14
+ expect($('[data-artist]').data('artist')).toBe 'ДДТ'
+ expect($('[data-artist]').data('song')).toBe 'Дождь'
+
+
+ # class ArtistsWithInfo
+ it "should identify artist similar", ->
+ loadFixtures("artist_similar.html")
+
+ manager.init()
+ manager.wrapMusicElements()
+
+ expect($('[data-song]').length).toBe 0
+ expect($('[data-artist]').length).toBe 10
+ expect($('[data-artist]').data('artist')).toBe 'Graham Coxon'
+
+
+ # class SingleTrack
+ it "should identify track (header) ", ->
+ loadFixtures("track_header.html")
+
+ manager.init()
+ manager.wrapMusicElements()
+
+ expect($('[data-song]').length).toBe 1
+ expect($('[data-artist]').length).toBe 1
+ expect($('[data-artist]').data('artist')).toBe 'Ke$ha'
+ expect($('[data-artist]').data('song')).toBe 'TiK ToK'
+
+
+ # class SingleTrack
+ it "should identify friends loved ", ->
+ loadFixtures("friends_loved.html")
+
+ manager.init()
+ manager.wrapMusicElements()
+
+ expect($('[data-song]').length).toBe 20
+ expect($('[data-artist]').length).toBe 20
+ expect($('[data-artist]').data('artist')).toBe 'Blink-182'
+ expect($('[data-artist]').data('song')).toBe 'What`s My Age Again'
+
+
+ # class Track
+ it "should identify similar tracks ", ->
+ loadFixtures("track_similar.html")
+
+ manager.init()
+ manager.wrapMusicElements()
+
+ console.warn $('[data-artist]')
+
+ expect($('[data-song]').length).toBe 100
+ expect($('[data-artist]').length).toBe 100
+ expect($('[data-artist]').data('artist')).toBe 'Depeche Mode'
+ expect($('[data-artist]').data('song')).toBe 'Personal Jesus'
View
119 plugins/lastfm/spec/content_script_spec.js
@@ -0,0 +1,119 @@
+(function() {
+ var __hasProp = Object.prototype.hasOwnProperty;
+
+ describe("LastFM content script", function() {
+ var lastfm, manager;
+ lastfm = chromus.plugins.lastfm;
+ manager = chromus.plugins.lastfm.manager;
+ beforeEach(function() {
+ var plugin_path;
+ plugin_path = chromus.plugins_info.lastfm.path;
+ plugin_path += "/spec/fixtures";
+ return jasmine.getFixtures().fixturesPath = plugin_path;
+ });
+ it("should get link type", function() {
+ var link, links, _results;
+ links = {
+ "/user/buger_swamp": "user",
+ "/music/Blur": "band",
+ "/music/Graham+Coxon/_/What%27ll+It+Take": "song",
+ "/music/Graham+Coxon/Happiness+In+Magazines": "album",
+ "/music/Graham+Coxon/+albums": "music"
+ };
+ _results = [];
+ for (link in links) {
+ if (!__hasProp.call(links, link)) continue;
+ _results.push(expect(chromus.utils.urlType("http://last.fm" + link)).toBe(links[link]));
+ }
+ return _results;
+ });
+ it("should identify profile recent tracks", function() {
+ loadFixtures("user_profile_recent.html");
+ manager.init();
+ manager.wrapMusicElements();
+ expect($('tr[data-song]').length).toBe(15);
+ expect($('tr[data-song]').data('artist')).toBe('Blur');
+ return expect($('tr[data-song]').data('song')).toBe('Tender');
+ });
+ it("should identify profile library", function() {
+ loadFixtures("user_profile_library.html");
+ manager.init();
+ manager.wrapMusicElements();
+ expect($('[data-song]').length).toBe(0);
+ expect($('[data-artist]').length).toBe(8);
+ return expect($('[data-artist]').data('artist')).toBe('ДДТ');
+ });
+ it("should identify profile track chart", function() {
+ loadFixtures("user_profile_track_chart.html");
+ manager.init();
+ manager.wrapMusicElements();
+ expect($('[data-song]').length).toBe(15);
+ expect($('[data-artist]').length).toBe(15);
+ expect($('[data-artist]').data('artist')).toBe('Nick Drake');
+ return expect($('[data-artist]').data('song')).toBe('Things Behind The Sun');
+ });
+ it("should identify profile artist chart", function() {
+ loadFixtures("user_profile_artist_chart.html");
+ manager.init();
+ manager.wrapMusicElements();
+ expect($('[data-song]').length).toBe(0);
+ expect($('[data-artist]').length).toBe(15);
+ return expect($('[data-artist]').data('artist')).toBe('Damien Rice');
+ });
+ it("should identify arthist top chart", function() {
+ loadFixtures("artist_main_top_chart.html");
+ manager.init();
+ manager.wrapMusicElements();
+ expect($('[data-song]').length).toBe(15);
+ expect($('[data-artist]').length).toBe(15);
+ expect($('[data-artist]').data('artist')).toBe('ДДТ');
+ return expect($('[data-artist]').data('song')).toBe('Дождь');
+ });
+ it("should identify artist charts", function() {
+ loadFixtures("artist_charts.html");
+ manager.init();
+ manager.wrapMusicElements();
+ expect($('[data-song]').length).toBe(14);
+ expect($('[data-artist]').length).toBe(14);
+ expect($('[data-artist]').data('artist')).toBe('ДДТ');
+ return expect($('[data-artist]').data('song')).toBe('Дождь');
+ });
+ it("should identify artist similar", function() {
+ loadFixtures("artist_similar.html");
+ manager.init();
+ manager.wrapMusicElements();
+ expect($('[data-song]').length).toBe(0);
+ expect($('[data-artist]').length).toBe(10);
+ return expect($('[data-artist]').data('artist')).toBe('Graham Coxon');
+ });
+ it("should identify track (header) ", function() {
+ loadFixtures("track_header.html");
+ manager.init();
+ manager.wrapMusicElements();
+ expect($('[data-song]').length).toBe(1);
+ expect($('[data-artist]').length).toBe(1);
+ expect($('[data-artist]').data('artist')).toBe('Ke$ha');
+ return expect($('[data-artist]').data('song')).toBe('TiK ToK');
+ });
+ it("should identify friends loved ", function() {
+ loadFixtures("friends_loved.html");
+ manager.init();
+ manager.wrapMusicElements();
+ expect($('[data-song]').length).toBe(20);
+ expect($('[data-artist]').length).toBe(20);
+ expect($('[data-artist]').data('artist')).toBe('Blink-182');
+ return expect($('[data-artist]').data('song')).toBe('What`s My Age Again');
+ });
+ return it("should identify similar tracks ", function() {
+ loadFixtures("track_similar.html");
+ manager.init();
+ manager.wrapMusicElements();
+ console.warn($('[data-artist]'));
+ expect($('[data-song]').length).toBe(100);
+ expect($('[data-artist]').length).toBe(100);
+ expect($('[data-artist]').data('artist')).toBe('Depeche Mode');
+ return expect($('[data-artist]').data('song')).toBe('Personal Jesus');
+ });
+ });
+
+}).call(this);
View
371 plugins/lastfm/spec/fixtures/artist_charts.html
@@ -0,0 +1,371 @@
+<!--
+
+http://www.last.fm/music/%D0%94%D0%94%D0%A2/+charts
+
+Charts
+
+-->
+
+<!-- HEAD -->
+ <meta property="og:type" content="band" />
+ <meta property="og:title" content="ДДТ" />
+
+<!-- BODY -->
+
+<div id="charts207793" class="module modulecharts modulechartstracks">
+
+
+
+
+
+ <div class="horizontalOptions clearit">
+ <ul>
+ <li class=" current first chartweek"><a href="/music/%D0%94%D0%94%D0%A2/+charts?rangetype=week&amp;subtype=tracks"> Last Week
+ </a></li> <li class="chart6month"><a href="/music/%D0%94%D0%94%D0%A2/+charts?rangetype=6month&amp;subtype=tracks">Last 6 months</a></li>
+</ul>
+ </div><!-- .horizontalOptions -->
+
+ <div class="module-body chart chartweek current">
+
+ <table class="candyStriped chart" odd="1">
+ <tbody>
+ <tr class="first odd streamable" data-track-id="2988992">
+ <td class="positionCell">
+ 1
+ </td>
+ <td class="playbuttonCell">
+ <a class="previewbutton preview-track" href="http://play.last.fm/preview/119128056.mp3" data-analytics-redirect="false" rel="nofollow"><img class="icon preview_icon" width="16" height="16" src="http://cdn.last.fm/flatness/clear.gif" style=""></a> </td>
+ <td class="subjectCell" title="Дождь, 330 listeners">
+ <div>
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%94%D0%BE%D0%B6%D0%B4%D1%8C">Дождь</a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%94%D0%BE%D0%B6%D0%B4%D1%8C" class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:100%;" class="chartbar"><span>330</span></div> </td>
+
+ </tr><tr class="" data-track-id="5068144">
+ <td class="positionCell">
+ 388
+ </td>
+ <td class="playbuttonCell">
+ &nbsp; </td>
+ <td class="subjectCell" title="Ty Ne Odin, 1 listener">
+ <div>
+ <a href="/music/%D0%94%D0%94%D0%A2/_/Ty+Ne+Odin">Ty Ne Odin</a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/Ty+Ne+Odin" class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:20%;" class="chartbar"><span>1</span></div> </td>
+
+ </tr><tr class="odd" data-track-id="147011194">
+ <td class="positionCell">
+ 388
+ </td>
+ <td class="playbuttonCell">
+ &nbsp; </td>
+ <td class="subjectCell" title="В час, когда усну (bonus track), 1 listener">
+ <div>
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%92+%D1%87%D0%B0%D1%81,+%D0%BA%D0%BE%D0%B3%D0%B4%D0%B0+%D1%83%D1%81%D0%BD%D1%83+(bonus+track)">В час, когда усну (bonus track)</a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%92+%D1%87%D0%B0%D1%81,+%D0%BA%D0%BE%D0%B3%D0%B4%D0%B0+%D1%83%D1%81%D0%BD%D1%83+(bonus+track)" class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:20%;" class="chartbar"><span>1</span></div> </td>
+
+ </tr><tr class="" data-track-id="8616064">
+ <td class="positionCell">
+ 388
+ </td>
+ <td class="playbuttonCell">
+ &nbsp; </td>
+ <td class="subjectCell" title="Мама, я любера люблю!, 1 listener">
+ <div>
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%9C%D0%B0%D0%BC%D0%B0,+%D1%8F+%D0%BB%D1%8E%D0%B1%D0%B5%D1%80%D0%B0+%D0%BB%D1%8E%D0%B1%D0%BB%D1%8E!">Мама, я любера люблю!</a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%9C%D0%B0%D0%BC%D0%B0,+%D1%8F+%D0%BB%D1%8E%D0%B1%D0%B5%D1%80%D0%B0+%D0%BB%D1%8E%D0%B1%D0%BB%D1%8E!" class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:20%;" class="chartbar"><span>1</span></div> </td>
+
+ </tr><tr class="odd" data-track-id="504282634">
+ <td class="positionCell">
+ 388
+ </td>
+ <td class="playbuttonCell">
+ &nbsp; </td>
+ <td class="subjectCell" title="Улыбки милых дам, 1 listener">
+ <div>
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%A3%D0%BB%D1%8B%D0%B1%D0%BA%D0%B8+%D0%BC%D0%B8%D0%BB%D1%8B%D1%85+%D0%B4%D0%B0%D0%BC">Улыбки милых дам</a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%A3%D0%BB%D1%8B%D0%B1%D0%BA%D0%B8+%D0%BC%D0%B8%D0%BB%D1%8B%D1%85+%D0%B4%D0%B0%D0%BC" class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:20%;" class="chartbar"><span>1</span></div> </td>
+
+ </tr><tr class="" data-track-id="481270456">
+ <td class="positionCell">
+ 388
+ </td>
+ <td class="playbuttonCell">
+ &nbsp; </td>
+ <td class="subjectCell" title="Метель (X), 1 listener">
+ <div>
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%9C%D0%B5%D1%82%D0%B5%D0%BB%D1%8C+(X)">Метель (X)</a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%9C%D0%B5%D1%82%D0%B5%D0%BB%D1%8C+(X)" class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:20%;" class="chartbar"><span>1</span></div> </td>
+
+ </tr><tr class="odd" data-track-id="496469566">
+ <td class="positionCell">
+ 388
+ </td>
+ <td class="playbuttonCell">
+ &nbsp; </td>
+ <td class="subjectCell" title="Дядя Коля, 1 listener">
+ <div>
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%94%D1%8F%D0%B4%D1%8F+%D0%9A%D0%BE%D0%BB%D1%8F">Дядя Коля</a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%94%D1%8F%D0%B4%D1%8F+%D0%9A%D0%BE%D0%BB%D1%8F" class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:20%;" class="chartbar"><span>1</span></div> </td>
+
+ </tr><tr class="" data-track-id="581512696">
+ <td class="positionCell">
+ 388
+ </td>
+ <td class="playbuttonCell">
+ &nbsp; </td>
+ <td class="subjectCell" title="Горигоримоя звезда..., 1 listener">
+ <div>
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%93%D0%BE%D1%80%D0%B8%D0%B3%D0%BE%D1%80%D0%B8%D0%BC%D0%BE%D1%8F+%D0%B7%D0%B2%D0%B5%D0%B7%D0%B4%D0%B0...">Горигоримоя звезда...</a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%93%D0%BE%D1%80%D0%B8%D0%B3%D0%BE%D1%80%D0%B8%D0%BC%D0%BE%D1%8F+%D0%B7%D0%B2%D0%B5%D0%B7%D0%B4%D0%B0..." class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:20%;" class="chartbar"><span>1</span></div> </td>
+
+ </tr><tr class="odd" data-track-id="456362206">
+ <td class="positionCell">
+ 388
+ </td>
+ <td class="playbuttonCell">
+ &nbsp; </td>
+ <td class="subjectCell" title="Белая ночь (ледниковый период), 1 listener">
+ <div>
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%91%D0%B5%D0%BB%D0%B0%D1%8F+%D0%BD%D0%BE%D1%87%D1%8C+(%D0%BB%D0%B5%D0%B4%D0%BD%D0%B8%D0%BA%D0%BE%D0%B2%D1%8B%D0%B9+%D0%BF%D0%B5%D1%80%D0%B8%D0%BE%D0%B4)">Белая ночь (ледниковый период)</a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%91%D0%B5%D0%BB%D0%B0%D1%8F+%D0%BD%D0%BE%D1%87%D1%8C+(%D0%BB%D0%B5%D0%B4%D0%BD%D0%B8%D0%BA%D0%BE%D0%B2%D1%8B%D0%B9+%D0%BF%D0%B5%D1%80%D0%B8%D0%BE%D0%B4)" class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:20%;" class="chartbar"><span>1</span></div> </td>
+
+ </tr><tr class="" data-track-id="418138516">
+ <td class="positionCell">
+ 388
+ </td>
+ <td class="playbuttonCell">
+ &nbsp; </td>
+ <td class="subjectCell" title="Закопали штыки (bonus-track), 1 listener">
+ <div>
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%97%D0%B0%D0%BA%D0%BE%D0%BF%D0%B0%D0%BB%D0%B8+%D1%88%D1%82%D1%8B%D0%BA%D0%B8+(bonus-track)">Закопали штыки (bonus-track)</a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%97%D0%B0%D0%BA%D0%BE%D0%BF%D0%B0%D0%BB%D0%B8+%D1%88%D1%82%D1%8B%D0%BA%D0%B8+(bonus-track)" class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:20%;" class="chartbar"><span>1</span></div> </td>
+
+ </tr><tr class="odd" data-track-id="275861386">
+ <td class="positionCell">
+ 388
+ </td>
+ <td class="playbuttonCell">
+ &nbsp; </td>
+ <td class="subjectCell" title="В час, когда я усну, 1 listener">
+ <div>
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%92+%D1%87%D0%B0%D1%81,+%D0%BA%D0%BE%D0%B3%D0%B4%D0%B0+%D1%8F+%D1%83%D1%81%D0%BD%D1%83">В час, когда я усну</a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%92+%D1%87%D0%B0%D1%81,+%D0%BA%D0%BE%D0%B3%D0%B4%D0%B0+%D1%8F+%D1%83%D1%81%D0%BD%D1%83" class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:20%;" class="chartbar"><span>1</span></div> </td>
+
+ </tr><tr class="" data-track-id="189101716">
+ <td class="positionCell">
+ 388
+ </td>
+ <td class="playbuttonCell">
+ &nbsp; </td>
+ <td class="subjectCell" title="Речь, 1 listener">
+ <div>
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%A0%D0%B5%D1%87%D1%8C">Речь</a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%A0%D0%B5%D1%87%D1%8C" class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:20%;" class="chartbar"><span>1</span></div> </td>
+
+ </tr><tr class="odd" data-track-id="180238756">
+ <td class="positionCell">
+ 388
+ </td>
+ <td class="playbuttonCell">
+ &nbsp; </td>
+ <td class="subjectCell" title="Đîćäĺííűé â ŃŃŃĐ, 1 listener">
+ <div>
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%C4%90%C3%AE%C4%87%C3%A4%C4%BA%C3%AD%C3%AD%C5%B1%C3%A9+%C3%A2+%C5%83%C5%83%C5%83%C4%90">Đîćäĺííűé â ŃŃŃĐ</a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%C4%90%C3%AE%C4%87%C3%A4%C4%BA%C3%AD%C3%AD%C5%B1%C3%A9+%C3%A2+%C5%83%C5%83%C5%83%C4%90" class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:20%;" class="chartbar"><span>1</span></div> </td>
+
+ </tr><tr class="last" data-track-id="122876146">
+ <td class="positionCell">
+ 388
+ </td>
+ <td class="playbuttonCell">
+ &nbsp; </td>
+ <td class="subjectCell" title="Осень, мёртвые дожди…, 1 listener">
+ <div>
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%9E%D1%81%D0%B5%D0%BD%D1%8C,+%D0%BC%D1%91%D1%80%D1%82%D0%B2%D1%8B%D0%B5+%D0%B4%D0%BE%D0%B6%D0%B4%D0%B8%E2%80%A6">Осень, мёртвые дожди…</a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%9E%D1%81%D0%B5%D0%BD%D1%8C,+%D0%BC%D1%91%D1%80%D1%82%D0%B2%D1%8B%D0%B5+%D0%B4%D0%BE%D0%B6%D0%B4%D0%B8%E2%80%A6" class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:20%;" class="chartbar"><span>1</span></div> </td>
+
+ </tr>
+ </tbody>
+</table> </div>
+ </div>
View
470 plugins/lastfm/spec/fixtures/artist_main_top_chart.html
@@ -0,0 +1,470 @@
+<!--
+
+http://www.last.fm/music/%D0%94%D0%94%D0%A2
+
+Top Tracks
+
+-->
+
+<!-- HEAD -->
+ <meta property="og:type" content="band" />
+ <meta property="og:title" content="ДДТ" />
+
+<!-- BODY -->
+<section id="artist-top-tracks" class="artist-top-tracks">
+
+ <div id="charts871215" class="module modulecharts modulechartstracks">
+
+
+
+ <h2 class="heading"><span class="h2Wrapper"><a href="/music/%D0%94%D0%94%D0%A2/+charts" title="DDT"> Top Tracks
+ </a></span></h2>
+
+ <div class="horizontalOptions clearit">
+ <ul>
+ <li class=" current first chartweek"><a href="/music/%D0%94%D0%94%D0%A2/+charts?rangetype=week&amp;subtype=tracks"> Last Week
+ </a></li> <li class="chart6month"><a href="/music/%D0%94%D0%94%D0%A2/+charts?rangetype=6month&amp;subtype=tracks">Last 6 months</a></li>
+</ul>
+ </div><!-- .horizontalOptions -->
+
+ <div class="module-body chart chartweek current">
+
+ <table class="candyStriped chart" odd="1">
+ <tbody>
+ <tr class="first odd streamable" data-track-id="2988992" itemprop="tracks" itemscope="" itemtype="http://schema.org/MusicRecording">
+ <td class="positionCell">
+ 1
+ </td>
+ <td class="playbuttonCell">
+ <a class="previewbutton preview-track" href="http://play.last.fm/preview/119128056.mp3" data-analytics-redirect="false" rel="nofollow"><img class="icon preview_icon" width="16" height="16" src="http://cdn.last.fm/flatness/clear.gif" style=""></a> </td>
+ <td class="subjectCell" title="Дождь, 330 listeners">
+ <div>
+ <meta itemprop="duration" content="PT0M30S">
+
+ <meta itemprop="url" content="http://www.last.fm/music/%D0%94%D0%94%D0%A2/_/%D0%94%D0%BE%D0%B6%D0%B4%D1%8C">
+ <meta itemprop="inAlbum" content="Просвистела">
+ <meta itemprop="interactionCount" content="UserPlays:187,973">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%94%D0%BE%D0%B6%D0%B4%D1%8C"><span itemprop="name">Дождь</span></a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%94%D0%BE%D0%B6%D0%B4%D1%8C" class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:100%;" class="chartbar"><span>330</span></div> </td>
+
+ </tr><tr class="streamable" data-track-id="2926933" itemprop="tracks" itemscope="" itemtype="http://schema.org/MusicRecording">
+ <td class="positionCell">
+ 2
+ </td>
+ <td class="playbuttonCell">
+ <a class="previewbutton preview-track" href="http://play.last.fm/preview/119128062.mp3" data-analytics-redirect="false" rel="nofollow"><img class="icon preview_icon" width="16" height="16" src="http://cdn.last.fm/flatness/clear.gif" style=""></a> </td>
+ <td class="subjectCell" title="Что такое осень, 282 listeners">
+ <div>
+ <meta itemprop="duration" content="PT0M30S">
+
+ <meta itemprop="url" content="http://www.last.fm/music/%D0%94%D0%94%D0%A2/_/%D0%A7%D1%82%D0%BE+%D1%82%D0%B0%D0%BA%D0%BE%D0%B5+%D0%BE%D1%81%D0%B5%D0%BD%D1%8C">
+ <meta itemprop="inAlbum" content="Просвистела">
+ <meta itemprop="interactionCount" content="UserPlays:166,242">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%A7%D1%82%D0%BE+%D1%82%D0%B0%D0%BA%D0%BE%D0%B5+%D0%BE%D1%81%D0%B5%D0%BD%D1%8C"><span itemprop="name">Что такое осень</span></a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>
+ <td class="multibuttonCell">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%A7%D1%82%D0%BE+%D1%82%D0%B0%D0%BA%D0%BE%D0%B5+%D0%BE%D1%81%D0%B5%D0%BD%D1%8C" class="
+ mAddToLibrary
+ mSend mAddTags
+ mAddToPlaylist
+ lfmButton lfmMultiButton lfmButtonFortrack lfmSmallButton lfmSmallMultiButton lfmMultiButtonFull
+" forcelink="1"><span></span></a>
+ </td>
+ <td class="chartbarCell">
+ <div style="width:85%;" class="chartbar"><span>282</span></div> </td>
+
+ </tr><tr class="odd streamable" data-track-id="3078165" itemprop="tracks" itemscope="" itemtype="http://schema.org/MusicRecording">
+ <td class="positionCell">
+ 3
+ </td>
+ <td class="playbuttonCell">
+ <a class="previewbutton preview-track" href="http://play.last.fm/preview/119128057.mp3" data-analytics-redirect="false" rel="nofollow"><img class="icon preview_icon" width="16" height="16" src="http://cdn.last.fm/flatness/clear.gif" style=""></a> </td>
+ <td class="subjectCell" title="В последнюю осень, 267 listeners">
+ <div>
+ <meta itemprop="duration" content="PT0M30S">
+
+ <meta itemprop="url" content="http://www.last.fm/music/%D0%94%D0%94%D0%A2/_/%D0%92+%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BD%D1%8E%D1%8E+%D0%BE%D1%81%D0%B5%D0%BD%D1%8C">
+ <meta itemprop="inAlbum" content="Просвистела">
+ <meta itemprop="interactionCount" content="UserPlays:137,248">
+ <a href="/music/%D0%94%D0%94%D0%A2/_/%D0%92+%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%D0%B4%D0%BD%D1%8E%D1%8E+%D0%BE%D1%81%D0%B5%D0%BD%D1%8C"><span itemprop="name">В последнюю осень</span></a>
+ </div>
+ </td>
+ <td class="lovedCell">
+ </td>