From 9347c7d5575b9a32f34d9f28b4a068f9bca99a21 Mon Sep 17 00:00:00 2001
From: jcass
Date: Mon, 28 Mar 2016 13:57:25 +0200
Subject: [PATCH] Upgrade media progress timer. Modularise progress_timer.js.
---
README.rst | 1 +
karma.conf.js | 1 -
mopidy_musicbox_webclient/static/index.html | 2 +-
.../static/js/controls.js | 6 +-
.../static/js/functionsvars.js | 3 +-
mopidy_musicbox_webclient/static/js/gui.js | 24 +-
mopidy_musicbox_webclient/static/js/images.js | 194 +++---
.../static/js/library.js | 639 +++++++++---------
.../static/js/process_ws.js | 4 +-
.../static/js/progress_timer.js | 177 -----
.../static/js/synced_timer.js | 218 ++++++
mopidy_musicbox_webclient/static/mb.appcache | 4 +-
.../vendors/media_progress_timer/timer.js | 265 ++++----
tests/{ => js}/test_images.js | 94 ++-
tests/js/test_library.js | 56 ++
tests/js/test_synced_timer.js | 352 ++++++++++
tests/test_library.js | 54 --
17 files changed, 1253 insertions(+), 841 deletions(-)
delete mode 100644 mopidy_musicbox_webclient/static/js/progress_timer.js
create mode 100644 mopidy_musicbox_webclient/static/js/synced_timer.js
rename tests/{ => js}/test_images.js (71%)
create mode 100644 tests/js/test_library.js
create mode 100644 tests/js/test_synced_timer.js
delete mode 100644 tests/test_library.js
diff --git a/README.rst b/README.rst
index 58c12b0..5d3a973 100644
--- a/README.rst
+++ b/README.rst
@@ -77,6 +77,7 @@ v2.3.0 (UNRELEASED)
- Now displays album and artist info when browsing tracks. (Addresses: `#99 `_).
- Now remembers which backend was searched previously, and automatically selects that backend as the default search target.
(Addresses: `#130 `_).
+- Upgrade Media Progress Timer to version 3.0.0.
**Fixes**
diff --git a/karma.conf.js b/karma.conf.js
index 57f7fa3..7f456f9 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -13,7 +13,6 @@ module.exports = function (config) {
// list of files / patterns to load in the browser
files: [
'mopidy_musicbox_webclient/static/vendors/**/*.js',
- // TODO: can remove the next line once JavaScript codebase has been modularized.
'mopidy_musicbox_webclient/static/js/**/*.js',
'tests/**/test_*.js'
],
diff --git a/mopidy_musicbox_webclient/static/index.html b/mopidy_musicbox_webclient/static/index.html
index ee58e9e..4c4b832 100644
--- a/mopidy_musicbox_webclient/static/index.html
+++ b/mopidy_musicbox_webclient/static/index.html
@@ -486,7 +486,7 @@
-
+
diff --git a/mopidy_musicbox_webclient/static/js/controls.js b/mopidy_musicbox_webclient/static/js/controls.js
index 3db4a6d..b1bcc9b 100644
--- a/mopidy_musicbox_webclient/static/js/controls.js
+++ b/mopidy_musicbox_webclient/static/js/controls.js
@@ -288,13 +288,13 @@ function setPlayState (nwplay) {
$('#btplay >i').removeClass('fa-play').addClass('fa-pause')
$('#btplay').attr('title', 'Pause')
mopidy.playback.getTimePosition().then(processCurrentposition, console.error)
- startProgressTimer()
+ syncedProgressTimer.start()
} else {
$('#btplayNowPlaying >i').removeClass('fa-pause').addClass('fa-play')
$('#btplayNowPlaying').attr('title', 'Play')
$('#btplay >i').removeClass('fa-pause').addClass('fa-play')
$('#btplay').attr('title', 'Play')
- progressTimer.stop()
+ syncedProgressTimer.stop()
}
play = nwplay
}
@@ -396,7 +396,7 @@ function doSeekPos (value) {
function setPosition (pos) {
if (!positionChanging && $('#trackslider').val() !== pos) {
- setProgressTimer(pos)
+ syncedProgressTimer.set(pos)
}
}
diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js
index f270607..ae1634a 100644
--- a/mopidy_musicbox_webclient/static/js/functionsvars.js
+++ b/mopidy_musicbox_webclient/static/js/functionsvars.js
@@ -5,6 +5,7 @@
*/
var mopidy
+var syncedProgressTimer
// values for controls
var play = false
@@ -268,7 +269,7 @@ function renderSongLiDivider (track, nextTrack, currentIndex, target) {
renderSongLiTrackArtists(track) + '
'
)
// Retrieve album covers
- coverArt.getCover(track.uri, getjQueryID(target + '-cover', track.uri, true), 'small')
+ coverArt.getCover(track.uri, getjQueryID(target + '-cover', track.uri, true), 'small', mopidy)
} else if (currentIndex > 0) {
// Small divider
$(target).before(' ')
diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js
index a1e1be6..932ee1a 100644
--- a/mopidy_musicbox_webclient/static/js/gui.js
+++ b/mopidy_musicbox_webclient/static/js/gui.js
@@ -128,7 +128,7 @@ function setSongInfo (data) {
}
if (data.track.album && data.track.album.name) {
$('#modalalbum').html('' + data.track.album.name + ' ')
- coverArt.getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge')
+ coverArt.getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge', mopidy)
} else {
$('#modalalbum').html('')
$('#infocover').attr('src', 'images/default_cover.png')
@@ -139,10 +139,9 @@ function setSongInfo (data) {
$('#trackslider').attr('min', 0)
$('#trackslider').attr('max', songlength)
- resetProgressTimer()
- progressTimer.set(0, songlength)
+ syncedProgressTimer.reset().set(0, songlength)
if (play) {
- startProgressTimer()
+ syncedProgressTimer.start()
}
resizeMb()
@@ -239,7 +238,7 @@ function initSocketevents () {
showFavourites()
})
library.getBrowseDir()
- library.getSearchSchemes()
+ library.getSearchSchemes(searchBlacklist, mopidy)
showLoading(false)
$(window).hashchange()
})
@@ -302,7 +301,7 @@ function initSocketevents () {
mopidy.on('event:seeked', function (data) {
setPosition(parseInt(data.time_position))
if (play) {
- startProgressTimer()
+ syncedProgressTimer.start()
}
})
@@ -496,9 +495,7 @@ $(document).ready(function (event) {
// initialize events
initSocketevents()
- progressTimer = new ProgressTimer({
- callback: timerCallback
- })
+ syncedProgressTimer = new SyncedProgressTimer(8, mopidy)
resetSong()
@@ -593,13 +590,13 @@ $(document).ready(function (event) {
// swipe songinfo and panel
$('#normalFooter, #nowPlayingFooter').on('swiperight', doPrevious)
$('#normalFooter, #nowPlayingFooter').on('swipeleft', doNext)
- $('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swiperight', function () {
+ $('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swiperight', function (event) {
if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) {
$('#panel').panel('open')
event.stopImmediatePropagation()
}
})
- $('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swipeleft', function () {
+ $('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swipeleft', function (event) {
if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) {
$('#panel').panel('close')
event.stopImmediatePropagation()
@@ -607,12 +604,13 @@ $(document).ready(function (event) {
})
$('#trackslider').on('slidestart', function () {
- progressTimer.stop()
- $('#trackslider').on('change', function () { updatePosition($(this).val()) })
+ syncedProgressTimer.stop()
+ $('#trackslider').on('change', function () { syncedProgressTimer.updatePosition($(this).val()) })
})
$('#trackslider').on('slidestop', function () {
$('#trackslider').off('change')
+ syncedProgressTimer.updatePosition($(this).val())
doSeekPos($(this).val())
})
diff --git a/mopidy_musicbox_webclient/static/js/images.js b/mopidy_musicbox_webclient/static/js/images.js
index 7da092b..8bef0c3 100644
--- a/mopidy_musicbox_webclient/static/js/images.js
+++ b/mopidy_musicbox_webclient/static/js/images.js
@@ -1,110 +1,108 @@
-/**
- * @author Wouter van Wijk
- */
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define([], factory)
+ } else if (typeof module === 'object' && module.exports) {
+ module.exports = factory()
+ } else {
+ root.coverArt = factory()
+ }
+}(this, function () {
+ 'use strict'
-API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733'
-API_SECRET = '2c631802c2285d5d5d1502462fe42a2b'
+ var API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733'
+ var API_SECRET = '2c631802c2285d5d5d1502462fe42a2b'
-var coverArt = {
- fmcache: new LastFMCache(),
- lastfm: new LastFM({
- apiKey: API_KEY,
- apiSecret: API_SECRET,
- cache: this.fmcache
- }),
+ var coverArt = {
+ lastfm: new LastFM({
+ apiKey: API_KEY,
+ apiSecret: API_SECRET,
+ cache: new LastFMCache()
+ }),
- getCover: function (uri, images, size) {
- var defUrl = 'images/default_cover.png'
- $(images).attr('src', defUrl)
- if (!uri) {
- return
- }
- mopidy.library.getImages({'uris': [uri]}).then(function (imageResults) {
- var uri = Object.keys(imageResults)[0]
- if (imageResults[uri].length > 0) {
- $(images).attr('src', imageResults[uri][0].uri)
- } else {
- // Also check deprecated 'album.images' in case backend does not
- // implement mopidy.library.getImages yet...
- coverArt.getCoverFromAlbum(uri, images, size)
- }
- })
- },
-
- // Note that this approach has been deprecated in Mopidy
- // TODO: Remove when Mopidy no longer supports getting images
- // with 'album.images'.
- getCoverFromAlbum: function (uri, images, size) {
- var defUrl = 'images/default_cover.png'
- $(images).attr('src', defUrl)
- if (!uri) {
- return
- }
- mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) {
- var uri = Object.keys(resultDict)[0]
- var track = resultDict[uri][0]
- if (track && track.album && track.album.images && track.album.images.length > 0) {
- $(images).attr('src', track.album.images[0])
- } else if (track && (track.album || track.artist)) {
- // Fallback to last.fm
- coverArt.getCoverFromLastFm(track, images, size)
- } else {
+ getCover: function (uri, images, size, mopidy) {
+ var defUrl = 'images/default_cover.png'
+ $(images).attr('src', defUrl)
+ if (!uri) {
return
}
- })
- },
-
- getCoverFromLastFm: function (track, images, size) {
- var defUrl = 'images/default_cover.png'
- $(images).attr('src', defUrl)
- if (!track || !(track.album || track.artists)) {
- return
- }
- var albumname = (track.album && track.album.name) ? track.album.name : ''
- var artistname = ''
- if (track.album && track.album.artists && track.album.artists.length > 0) {
- // First look for the artist in the album
- artistname = track.album.artists[0].name
- } else if (track.artists && (track.artists.length > 0)) {
- // Fallback to using artists for specific track
- artistname = track.artists[0].name
- }
-
- this.lastfm.album.getInfo({artist: artistname, album: albumname}, {success: function (data) {
- for (var i = 0; i < data.album.image.length; i++) {
- if (data.album.image[i].size === size) {
- $(images).attr('src', data.album.image[i]['#text'] || defUrl)
+ mopidy.library.getImages({'uris': [uri]}).then(function (imageResults) {
+ var uri = Object.keys(imageResults)[0]
+ if (imageResults[uri].length > 0) {
+ $(images).attr('src', imageResults[uri][0].uri)
+ } else {
+ // Also check deprecated 'album.images' in case backend does not
+ // implement mopidy.library.getImages yet...
+ coverArt.getCoverFromAlbum(uri, images, size, mopidy)
}
- }
- }, error: function (code, message) {
- console.error('Error retrieving album info from last.fm', code, message)
- }})
- },
+ })
+ },
- getArtistImage: function (artist, images, size) {
- var defUrl = 'images/user_24x32.png'
- $(images).attr('src', defUrl)
- if (!artist || artist.length === 0) {
- return
- }
- this.lastfm.artist.getInfo({artist: artist}, {success: function (data) {
- for (var i = 0; i < data.artist.image.length; i++) {
- if (data.artist.image[i].size === size) {
- $(images).attr('src', data.artist.image[i]['#text'] || defUrl)
+ // Note that this approach has been deprecated in Mopidy
+ // TODO: Remove when Mopidy no longer supports getting images
+ // with 'album.images'.
+ getCoverFromAlbum: function (uri, images, size, mopidy) {
+ var defUrl = 'images/default_cover.png'
+ $(images).attr('src', defUrl)
+ if (!uri) {
+ return
+ }
+ mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) {
+ var uri = Object.keys(resultDict)[0]
+ var track = resultDict[uri][0]
+ if (track && track.album && track.album.images && track.album.images.length > 0) {
+ $(images).attr('src', track.album.images[0])
+ } else if (track && (track.album || track.artist)) {
+ // Fallback to last.fm
+ coverArt.getCoverFromLastFm(track, images, size)
+ } else {
+ return
}
+ })
+ },
+
+ getCoverFromLastFm: function (track, images, size) {
+ var defUrl = 'images/default_cover.png'
+ $(images).attr('src', defUrl)
+ if (!track || !(track.album || track.artists)) {
+ return
+ }
+ var albumname = (track.album && track.album.name) ? track.album.name : ''
+ var artistname = ''
+ if (track.album && track.album.artists && track.album.artists.length > 0) {
+ // First look for the artist in the album
+ artistname = track.album.artists[0].name
+ } else if (track.artists && (track.artists.length > 0)) {
+ // Fallback to using artists for specific track
+ artistname = track.artists[0].name
}
- }, error: function (code, message) {
- console.error('Error retrieving artist info from last.fm', code, message)
- }})
- }
-}
-$(document).ready(coverArt.init)
+ this.lastfm.album.getInfo({artist: artistname, album: albumname}, {success: function (data) {
+ for (var i = 0; i < data.album.image.length; i++) {
+ if (data.album.image[i].size === size) {
+ $(images).attr('src', data.album.image[i]['#text'] || defUrl)
+ }
+ }
+ }, error: function (code, message) {
+ console.error('Error retrieving album info from last.fm', code, message)
+ }})
+ },
-// TODO: Remove this once JavaScript codebase has been completely modularized
-// in favour of bundling everything using 'browserify'.
-if (typeof exports !== 'undefined') {
- if (typeof module !== 'undefined' && module.exports) {
- module.exports = coverArt
+ getArtistImage: function (artist, images, size) {
+ var defUrl = 'images/user_24x32.png'
+ $(images).attr('src', defUrl)
+ if (!artist || artist.length === 0) {
+ return
+ }
+ this.lastfm.artist.getInfo({artist: artist}, {success: function (data) {
+ for (var i = 0; i < data.artist.image.length; i++) {
+ if (data.artist.image[i].size === size) {
+ $(images).attr('src', data.artist.image[i]['#text'] || defUrl)
+ }
+ }
+ }, error: function (code, message) {
+ console.error('Error retrieving artist info from last.fm', code, message)
+ }})
+ }
}
-}
+ return coverArt
+}))
diff --git a/mopidy_musicbox_webclient/static/js/library.js b/mopidy_musicbox_webclient/static/js/library.js
index 7abd995..dad0606 100644
--- a/mopidy_musicbox_webclient/static/js/library.js
+++ b/mopidy_musicbox_webclient/static/js/library.js
@@ -1,347 +1,352 @@
-var library = {
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define([], factory)
+ } else if (typeof module === 'object' && module.exports) {
+ module.exports = factory()
+ } else {
+ root.library = factory()
+ }
+}(this, function () {
+ 'use strict'
-/** *******************************
- * Search
- *********************************/
- searchPressed: function (key) {
- var value = $('#searchinput').val()
- switchContent('search')
+ var library = {
- if (key === 13) {
- library.initSearch()
- return false
- }
- return true
- },
+ /** *******************************
+ * Search
+ *********************************/
+ searchPressed: function (key) {
+ var value = $('#searchinput').val()
+ switchContent('search')
- // init search
- initSearch: function () {
- var value = $('#searchinput').val()
- var searchService = $('#selectSearchService').val()
- $.cookie('searchScheme', searchService, { expires: 365 })
+ if (key === 13) {
+ library.initSearch()
+ return false
+ }
+ return true
+ },
- if ((value.length < 100) && (value.length > 0)) {
- showLoading(true)
- // hide ios/android keyboard
- document.activeElement.blur()
- $('input').blur()
+ // init search
+ initSearch: function () {
+ var value = $('#searchinput').val()
+ var searchService = $('#selectSearchService').val()
+ $.cookie('searchScheme', searchService, { expires: 365 })
- delete customTracklists[URI_SCHEME + ':allresultscache']
- delete customTracklists[URI_SCHEME + ':artistresultscache']
- delete customTracklists[URI_SCHEME + ':albumresultscache']
- delete customTracklists[URI_SCHEME + ':trackresultscache']
- $('#searchartists').hide()
- $('#searchalbums').hide()
- $('#searchtracks').hide()
+ if ((value.length < 100) && (value.length > 0)) {
+ showLoading(true)
+ // hide ios/android keyboard
+ document.activeElement.blur()
+ $('input').blur()
- if (searchService !== 'all') {
- mopidy.library.search({'query': {any: [value]}, 'uris': [searchService + ':']}).then(library.processSearchResults, console.error)
- } else {
- mopidy.getUriSchemes().then(function (schemes) {
- var query = {}
- var uris = []
+ delete customTracklists[URI_SCHEME + ':allresultscache']
+ delete customTracklists[URI_SCHEME + ':artistresultscache']
+ delete customTracklists[URI_SCHEME + ':albumresultscache']
+ delete customTracklists[URI_SCHEME + ':trackresultscache']
+ $('#searchartists').hide()
+ $('#searchalbums').hide()
+ $('#searchtracks').hide()
- var regexp = $.map(schemes, function (scheme) {
- return '^' + scheme + ':'
- }).join('|')
+ if (searchService !== 'all') {
+ mopidy.library.search({'query': {any: [value]}, 'uris': [searchService + ':']}).then(library.processSearchResults, console.error)
+ } else {
+ mopidy.getUriSchemes().then(function (schemes) {
+ var query = {}
+ var uris = []
- var match = value.match(regexp)
- if (match) {
- var scheme = match[0]
- query = {uri: [value]}
- uris = [scheme]
- } else {
- query = {any: [value]}
+ var regexp = $.map(schemes, function (scheme) {
+ return '^' + scheme + ':'
+ }).join('|')
+
+ var match = value.match(regexp)
+ if (match) {
+ var scheme = match[0]
+ query = {uri: [value]}
+ uris = [scheme]
+ } else {
+ query = {any: [value]}
+ }
+ mopidy.library.search({'query': query, 'uris': uris}).then(library.processSearchResults, console.error)
+ })
+ }
+ }
+ },
+
+ /** ******************************************************
+ * process results of a search
+ *********************************************************/
+ processSearchResults: function (resultArr) {
+ $(SEARCH_TRACK_TABLE).empty()
+ $(SEARCH_ARTIST_TABLE).empty()
+ $(SEARCH_ALBUM_TABLE).empty()
+
+ // Merge results from different backends.
+ // TODO should of coures have multiple tables
+ var results = {'tracks': [], 'artists': [], 'albums': []}
+ var i, j
+ var emptyResult = true
+
+ for (i = 0; i < resultArr.length; i++) {
+ if (resultArr[i].tracks) {
+ for (j = 0; j < resultArr[i].tracks.length; j++) {
+ results.tracks.push(resultArr[i].tracks[j])
+ emptyResult = false
}
- mopidy.library.search({'query': query, 'uris': uris}).then(library.processSearchResults, console.error)
+ }
+ if (resultArr[i].artists) {
+ for (j = 0; j < resultArr[i].artists.length; j++) {
+ results.artists.push(resultArr[i].artists[j])
+ emptyResult = false
+ }
+ }
+ if (resultArr[i].albums) {
+ for (j = 0; j < resultArr[i].albums.length; j++) {
+ results.albums.push(resultArr[i].albums[j])
+ emptyResult = false
+ }
+ }
+ }
+
+ customTracklists[URI_SCHEME + ':trackresultscache'] = results.tracks
+
+ if (emptyResult) {
+ $('#searchtracks').show()
+ $(SEARCH_TRACK_TABLE).append(
+ ' No tracks found... '
+ )
+ toast('No results')
+ showLoading(false)
+ return false
+ }
+
+ if (results.artists.length > 0) {
+ $('#searchartists').show()
+ }
+
+ if (results.albums.length > 0) {
+ $('#searchalbums').show()
+ }
+
+ if (results.tracks.length > 0) {
+ $('#searchtracks').show()
+ }
+
+ // Returns a string where {x} in template is replaced by tokens[x].
+ function theme (template, tokens) {
+ return template.replace(/{[^}]+}/g, function (match) {
+ return tokens[match.slice(1, -1)]
})
}
- }
- },
-/** ******************************************************
- * process results of a search
- *********************************************************/
- processSearchResults: function (resultArr) {
- $(SEARCH_TRACK_TABLE).empty()
- $(SEARCH_ARTIST_TABLE).empty()
- $(SEARCH_ALBUM_TABLE).empty()
+ // 'Show more' pattern
+ var showMorePattern = 'Show {count} more '
- // Merge results from different backends.
- // TODO should of coures have multiple tables
- var results = {'tracks': [], 'artists': [], 'albums': []}
- var i, j
- var emptyResult = true
+ // Artist results
+ var child = ''
+ var pattern = ' {name} '
+ var tokens
- for (i = 0; i < resultArr.length; i++) {
- if (resultArr[i].tracks) {
- for (j = 0; j < resultArr[i].tracks.length; j++) {
- results.tracks.push(resultArr[i].tracks[j])
- emptyResult = false
+ for (i = 0; i < results.artists.length; i++) {
+ tokens = {
+ 'id': results.artists[i].uri,
+ 'name': results.artists[i].name,
+ 'class': getMediaClass(results.artists[i].uri)
}
- }
- if (resultArr[i].artists) {
- for (j = 0; j < resultArr[i].artists.length; j++) {
- results.artists.push(resultArr[i].artists[j])
- emptyResult = false
+
+ // Add 'Show all' item after a certain number of hits.
+ if (i === 4 && results.artists.length > 5) {
+ child += theme(showMorePattern, {'count': results.artists.length - i})
+ pattern = pattern.replace('', ' ')
}
+
+ child += theme(pattern, tokens)
}
- if (resultArr[i].albums) {
- for (j = 0; j < resultArr[i].albums.length; j++) {
- results.albums.push(resultArr[i].albums[j])
- emptyResult = false
+
+ // Inject list items, refresh listview and hide superfluous items.
+ $(SEARCH_ARTIST_TABLE).html(child).listview('refresh').find('.overflow').hide()
+
+ // Album results
+ child = ''
+ pattern = ' '
+ pattern += ' {albumName} '
+ pattern += '{artistName}
'
+ pattern += ' '
+
+ for (i = 0; i < results.albums.length; i++) {
+ tokens = {
+ 'albumId': results.albums[i].uri,
+ 'albumName': results.albums[i].name,
+ 'artistName': '',
+ 'albumYear': results.albums[i].date,
+ 'class': getMediaClass(results.albums[i].uri)
}
- }
- }
-
- customTracklists[URI_SCHEME + ':trackresultscache'] = results.tracks
-
- if (emptyResult) {
- $('#searchtracks').show()
- $(SEARCH_TRACK_TABLE).append(
- ' No tracks found... '
- )
- toast('No results')
- showLoading(false)
- return false
- }
-
- if (results.artists.length > 0) {
- $('#searchartists').show()
- }
-
- if (results.albums.length > 0) {
- $('#searchalbums').show()
- }
-
- if (results.tracks.length > 0) {
- $('#searchtracks').show()
- }
-
- // Returns a string where {x} in template is replaced by tokens[x].
- function theme (template, tokens) {
- return template.replace(/{[^}]+}/g, function (match) {
- return tokens[match.slice(1, -1)]
- })
- }
-
- // 'Show more' pattern
- var showMorePattern = 'Show {count} more '
-
- // Artist results
- var child = ''
- var pattern = ' {name} '
- var tokens
-
- for (i = 0; i < results.artists.length; i++) {
- tokens = {
- 'id': results.artists[i].uri,
- 'name': results.artists[i].name,
- 'class': getMediaClass(results.artists[i].uri)
- }
-
- // Add 'Show all' item after a certain number of hits.
- if (i === 4 && results.artists.length > 5) {
- child += theme(showMorePattern, {'count': results.artists.length - i})
- pattern = pattern.replace('', ' ')
- }
-
- child += theme(pattern, tokens)
- }
-
- // Inject list items, refresh listview and hide superfluous items.
- $(SEARCH_ARTIST_TABLE).html(child).listview('refresh').find('.overflow').hide()
-
- // Album results
- child = ''
- pattern = ' '
- pattern += ' {albumName} '
- pattern += '{artistName}
'
- pattern += ' '
-
- for (i = 0; i < results.albums.length; i++) {
- tokens = {
- 'albumId': results.albums[i].uri,
- 'albumName': results.albums[i].name,
- 'artistName': '',
- 'albumYear': results.albums[i].date,
- 'class': getMediaClass(results.albums[i].uri)
- }
- if (results.albums[i].artists) {
- for (j = 0; j < results.albums[i].artists.length; j++) {
- if (results.albums[i].artists[j].name) {
- tokens.artistName += results.albums[i].artists[j].name + ' '
+ if (results.albums[i].artists) {
+ for (j = 0; j < results.albums[i].artists.length; j++) {
+ if (results.albums[i].artists[j].name) {
+ tokens.artistName += results.albums[i].artists[j].name + ' '
+ }
}
}
+ if (tokens.albumYear) {
+ tokens.artistName += '(' + tokens.albumYear + ')'
+ }
+ // Add 'Show all' item after a certain number of hits.
+ if (i === 4 && results.albums.length > 5) {
+ child += theme(showMorePattern, {'count': results.albums.length - i})
+ pattern = pattern.replace('', ' ')
+ }
+
+ child += theme(pattern, tokens)
}
- if (tokens.albumYear) {
- tokens.artistName += '(' + tokens.albumYear + ')'
- }
- // Add 'Show all' item after a certain number of hits.
- if (i === 4 && results.albums.length > 5) {
- child += theme(showMorePattern, {'count': results.albums.length - i})
- pattern = pattern.replace(' ', ' ')
- }
+ // Inject list items, refresh listview and hide superfluous items.
+ $(SEARCH_ALBUM_TABLE).html(child).listview('refresh').find('.overflow').hide()
- child += theme(pattern, tokens)
- }
- // Inject list items, refresh listview and hide superfluous items.
- $(SEARCH_ALBUM_TABLE).html(child).listview('refresh').find('.overflow').hide()
+ // Track results
+ resultsToTables(results.tracks, SEARCH_TRACK_TABLE, URI_SCHEME + ':trackresultscache')
- // Track results
- resultsToTables(results.tracks, SEARCH_TRACK_TABLE, URI_SCHEME + ':trackresultscache')
-
- showLoading(false)
- },
-
-/** *******************************
- * Playlists & Browse
- *********************************/
- getPlaylists: function () {
- // get playlists without tracks
- mopidy.playlists.asList().then(processGetPlaylists, console.error)
- },
-
- getBrowseDir: function (rootdir) {
- // get directory to browse
- showLoading(true)
- if (!rootdir) {
- browseStack.pop()
- rootdir = browseStack[browseStack.length - 1]
- } else {
- browseStack.push(rootdir)
- }
- if (!rootdir) {
- rootdir = null
- }
- mopidy.library.browse({'uri': rootdir}).then(processBrowseDir, console.error)
- },
-
- getCurrentPlaylist: function () {
- mopidy.tracklist.getTlTracks().then(processCurrentPlaylist, console.error)
- },
-
-/** ******************************************************
- * Show tracks of playlist
- ********************************************************/
- togglePlaylists: function () {
- if ($(window).width() <= 960) {
- $('#playlisttracksdiv').toggle();
- // Hide other div
- ($('#playlisttracksdiv').is(':visible')) ? $('#playlistslistdiv').hide() : $('#playlistslistdiv').show()
- } else {
- $('#playlisttracksdiv').show()
- $('#playlistslistdiv').show()
- }
- return true
- },
-
-/** **********
- * Lookups
- ************/
- showTracklist: function (uri) {
- showLoading(true)
- $(PLAYLIST_TABLE).empty()
- library.togglePlaylists()
- var tracks = getPlaylistTracks(uri).then(function (tracks) {
- resultsToTables(tracks, PLAYLIST_TABLE, uri, 'return library.togglePlaylists();', true)
showLoading(false)
- })
- updatePlayIcons(uri)
- $('#playlistslist li a').each(function () {
- $(this).removeClass('playlistactive')
- if (this.id === uri) {
- $(this).addClass('playlistactive')
- }
- })
- return false
- },
+ },
- showArtist: function (nwuri) {
- $('#popupQueue').popup('close')
- $('#popupTracks').popup('close')
- $('#controlsmodal').popup('close')
- $(ARTIST_TABLE).empty()
+ /** *******************************
+ * Playlists & Browse
+ *********************************/
+ getPlaylists: function () {
+ // get playlists without tracks
+ mopidy.playlists.asList().then(processGetPlaylists, console.error)
+ },
- // TODO cache
- $('#h_artistname').html('')
- showLoading(true)
- mopidy.library.lookup({'uris': [nwuri]}).then(function (resultDict) {
- var resultArr = resultDict[nwuri]
- resultArr.uri = nwuri
- processArtistResults(resultArr)
- }, console.error)
- switchContent('artists', nwuri)
- scrollToTop()
- return false
- },
-
- showAlbum: function (uri) {
- $('#popupQueue').popup('close')
- $('#popupTracks').popup('close')
- $('#controlsmodal').popup('close')
- $(ALBUM_TABLE).empty()
- // fill from cache
- var pl = getTracksFromUri(uri, true)
- if (pl.length > 0) {
- albumTracksToTable(pl, ALBUM_TABLE, uri)
- var albumname = getAlbum(pl)
- var artistname = getArtist(pl)
- $('#h_albumname').html(albumname)
- $('#h_albumartist').html(artistname)
- $('#coverpopupalbumname').html(albumname)
- $('#coverpopupartist').html(artistname)
- showLoading(false)
- mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) {
- var resultArr = resultDict[uri]
- resultArr.uri = uri
- processAlbumResults(resultArr)
- }, console.error)
- } else {
+ getBrowseDir: function (rootdir) {
+ // get directory to browse
showLoading(true)
- $('#h_albumname').html('')
- $('#h_albumartist').html('')
- mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) {
- var resultArr = resultDict[uri]
- resultArr.uri = uri
- processAlbumResults(resultArr)
+ if (!rootdir) {
+ browseStack.pop()
+ rootdir = browseStack[browseStack.length - 1]
+ } else {
+ browseStack.push(rootdir)
+ }
+ if (!rootdir) {
+ rootdir = null
+ }
+ mopidy.library.browse({'uri': rootdir}).then(processBrowseDir, console.error)
+ },
+
+ getCurrentPlaylist: function () {
+ mopidy.tracklist.getTlTracks().then(processCurrentPlaylist, console.error)
+ },
+
+ /** ******************************************************
+ * Show tracks of playlist
+ ********************************************************/
+ togglePlaylists: function () {
+ if ($(window).width() <= 960) {
+ $('#playlisttracksdiv').toggle();
+ // Hide other div
+ ($('#playlisttracksdiv').is(':visible')) ? $('#playlistslistdiv').hide() : $('#playlistslistdiv').show()
+ } else {
+ $('#playlisttracksdiv').show()
+ $('#playlistslistdiv').show()
+ }
+ return true
+ },
+
+ /** **********
+ * Lookups
+ ************/
+ showTracklist: function (uri) {
+ showLoading(true)
+ $(PLAYLIST_TABLE).empty()
+ library.togglePlaylists()
+ var tracks = getPlaylistTracks(uri).then(function (tracks) {
+ resultsToTables(tracks, PLAYLIST_TABLE, uri, 'return library.togglePlaylists();', true)
+ showLoading(false)
+ })
+ updatePlayIcons(uri)
+ $('#playlistslist li a').each(function () {
+ $(this).removeClass('playlistactive')
+ if (this.id === uri) {
+ $(this).addClass('playlistactive')
+ }
+ })
+ return false
+ },
+
+ showArtist: function (nwuri) {
+ $('#popupQueue').popup('close')
+ $('#popupTracks').popup('close')
+ $('#controlsmodal').popup('close')
+ $(ARTIST_TABLE).empty()
+
+ // TODO cache
+ $('#h_artistname').html('')
+ showLoading(true)
+ mopidy.library.lookup({'uris': [nwuri]}).then(function (resultDict) {
+ var resultArr = resultDict[nwuri]
+ resultArr.uri = nwuri
+ processArtistResults(resultArr)
+ }, console.error)
+ switchContent('artists', nwuri)
+ scrollToTop()
+ return false
+ },
+
+ showAlbum: function (uri) {
+ $('#popupQueue').popup('close')
+ $('#popupTracks').popup('close')
+ $('#controlsmodal').popup('close')
+ $(ALBUM_TABLE).empty()
+ // fill from cache
+ var pl = getTracksFromUri(uri, true)
+ if (pl.length > 0) {
+ albumTracksToTable(pl, ALBUM_TABLE, uri)
+ var albumname = getAlbum(pl)
+ var artistname = getArtist(pl)
+ $('#h_albumname').html(albumname)
+ $('#h_albumartist').html(artistname)
+ $('#coverpopupalbumname').html(albumname)
+ $('#coverpopupartist').html(artistname)
+ showLoading(false)
+ mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) {
+ var resultArr = resultDict[uri]
+ resultArr.uri = uri
+ processAlbumResults(resultArr)
+ }, console.error)
+ } else {
+ showLoading(true)
+ $('#h_albumname').html('')
+ $('#h_albumartist').html('')
+ mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) {
+ var resultArr = resultDict[uri]
+ resultArr.uri = uri
+ processAlbumResults(resultArr)
+ }, console.error)
+ }
+ // show page
+ switchContent('albums', uri)
+ scrollToTop()
+ return false
+ },
+
+ getSearchSchemes: function (searchBlacklist, mopidy) {
+ var backendName
+ var searchScheme = $.cookie('searchScheme')
+ if (searchScheme) {
+ searchScheme = searchScheme.replace(/"/g, '')
+ } else {
+ searchScheme = 'all'
+ }
+ $('#selectSearchService').empty()
+ $('#selectSearchService').append(new Option('All services', 'all'))
+ mopidy.getUriSchemes().then(function (schemesArray) {
+ schemesArray = schemesArray.filter(function (el) {
+ return searchBlacklist.indexOf(el) < 0
+ })
+ for (var i = 0; i < schemesArray.length; i++) {
+ backendName = getMediaHuman(schemesArray[i])
+ backendName = backendName.charAt(0).toUpperCase() + backendName.slice(1)
+ $('#selectSearchService').append(new Option(backendName, schemesArray[i]))
+ }
+ $('#selectSearchService').val(searchScheme)
+ $('#selectSearchService').selectmenu('refresh', true)
}, console.error)
}
- // show page
- switchContent('albums', uri)
- scrollToTop()
- return false
- },
-
- getSearchSchemes: function () {
- var backendName
- var searchScheme = $.cookie('searchScheme')
- if (searchScheme) {
- searchScheme = searchScheme.replace(/"/g, '')
- } else {
- searchScheme = 'all'
- }
- $('#selectSearchService').empty()
- $('#selectSearchService').append(new Option('All services', 'all'))
- mopidy.getUriSchemes().then(function (schemesArray) {
- schemesArray = schemesArray.filter(function (el) {
- return searchBlacklist.indexOf(el) < 0
- })
- for (var i = 0; i < schemesArray.length; i++) {
- backendName = getMediaHuman(schemesArray[i])
- backendName = backendName.charAt(0).toUpperCase() + backendName.slice(1)
- $('#selectSearchService').append(new Option(backendName, schemesArray[i]))
- }
- $('#selectSearchService').val(searchScheme)
- $('#selectSearchService').selectmenu('refresh', true)
- }, console.error)
}
-}
-
-// TODO: Remove this once JavaScript codebase has been completely modularized
-// in favour of bundling everything using 'browserify'.
-if (typeof exports !== 'undefined') {
- if (typeof module !== 'undefined' && module.exports) {
- module.exports = library
- }
-}
+ return library
+}))
diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js
index bbbb7d4..a3ccba3 100644
--- a/mopidy_musicbox_webclient/static/js/process_ws.js
+++ b/mopidy_musicbox_webclient/static/js/process_ws.js
@@ -220,7 +220,7 @@ function processCurrentPlaylist (resultArr) {
function processArtistResults (resultArr) {
if (!resultArr || (resultArr.length === 0)) {
$('#h_artistname').text('Artist not found...')
- coverArt.getCover('', '#artistviewimage, #artistpopupimage', 'extralarge')
+ coverArt.getCover('', '#artistviewimage, #artistpopupimage', 'extralarge', mopidy)
showLoading(false)
return
}
@@ -239,7 +239,7 @@ function processArtistResults (resultArr) {
function processAlbumResults (resultArr) {
if (!resultArr || (resultArr.length === 0)) {
$('#h_albumname').text('Album not found...')
- coverArt.getCover('', '#albumviewcover, #coverpopupimage', 'extralarge')
+ coverArt.getCover('', '#albumviewcover, #coverpopupimage', 'extralarge', mopidy)
showLoading(false)
return
}
diff --git a/mopidy_musicbox_webclient/static/js/progress_timer.js b/mopidy_musicbox_webclient/static/js/progress_timer.js
deleted file mode 100644
index 00ff6e9..0000000
--- a/mopidy_musicbox_webclient/static/js/progress_timer.js
+++ /dev/null
@@ -1,177 +0,0 @@
-var progressTimer
-var positionNode = document.createTextNode('')
-var durationNode = document.createTextNode('')
-
-var START_BEATS = 5 // 0.5 seconds, needs to be less than 1s to prevent unwanted updates.
-var RUN_BEATS = 300 // 30 seconds assuming default timer update rate of 100ms
-var callbackHeartbeats = 0 // Timer will check syncs on every n-number of calls.
-var targetPosition = null
-
-var MAX_SYNCS = 5 // Maximum number of consecutive successful syncs to perform.
-var syncsLeft = MAX_SYNCS
-var synced = false
-var consecutiveSyncs = 0
-
-$(document).ready(function () {
- $('#songelapsed').append(positionNode)
- $('#songlength').append(durationNode)
-})
-
-function timerCallback (position, duration, isRunning) {
- updateTimers(position, duration, isRunning)
-
- if (callbackHeartbeats === 0) {
- callbackHeartbeats = getHeartbeat()
- }
-
- if (mopidy && position > 0) {
- // Mopidy and timer are both initialized.
- if (callbackHeartbeats-- === 1) {
- // Get time position from Mopidy on every nth callback until
- // synced.
- mopidy.playback.getTimePosition().then(
- function (mopidy_position) {
- syncTimer(position, mopidy_position)
- }
- )
- }
- }
-}
-
-function updateTimers (position, duration, isRunning) {
- var ready = !(duration === Infinity && position === 0 && !isRunning) // Timer has been properly initialized.
- var streaming = (duration === Infinity && position > 0) // Playing a stream.
- var ok = synced && isRunning // Normal operation.
- var syncing = !synced && isRunning // Busy syncing.
-
- if (!ready) {
- // Make sure that default values are displayed while the timer is being initialized.
- positionNode.nodeValue = ''
- durationNode.nodeValue = ''
- $('#trackslider').val(0).slider('refresh')
- } else {
- durationNode.nodeValue = format(duration || Infinity)
- if (syncing) {
- if (!targetPosition) {
- // Waiting for Mopidy to provide a target position.
- positionNode.nodeValue = '(wait)'
- } else {
- // Busy seeking to new target position.
- positionNode.nodeValue = '(sync)'
- }
- } else if (synced || streaming) {
- positionNode.nodeValue = format(position)
- }
- }
-
- if (ok) {
- // Don't update the track slider unless it is synced and running.
- // (prevents awkward 'jitter' animation).
- $('#trackslider').val(position).slider('refresh')
- }
-}
-
-function getHeartbeat () {
- if (syncsLeft > 0 && callbackHeartbeats === 0) {
- // Step back exponentially while increasing heartbeat.
- return Math.round(delay_exponential(5, 2, MAX_SYNCS - syncsLeft))
- } else if (syncsLeft === 0 && callbackHeartbeats === 0) {
- // Sync completed, keep checking using maximum number of heartbeats.
- return RUN_BEATS
- } else {
- return START_BEATS
- }
-}
-
-function syncTimer (current, target) {
- if (target) {
- var drift = Math.abs(target - current)
- if (drift <= 500) {
- syncsLeft--
- // Less than 500ms == in sync.
- if (++consecutiveSyncs === 2) {
- // Need at least two consecutive syncs to know that Mopidy
- // is progressing playback and we are in sync.
- synced = true
- targetPosition = null
- consecutiveSyncs = 0
- }
- } else {
- // Drift is too large, re-sync with Mopidy.
- reset()
- targetPosition = target
- progressTimer.set(targetPosition)
- }
- }
-}
-
-function toInt (value) {
- return value.match(/^\w*\d+\w*$/) ? parseInt(value) : null
-}
-
-function format (milliseconds) {
- if (milliseconds === Infinity) {
- return '(n/a)'
- } else if (milliseconds === 0) {
- return '0:00'
- }
-
- var seconds = Math.floor(milliseconds / 1000)
- var minutes = Math.floor(seconds / 60)
- seconds = seconds % 60
-
- seconds = seconds < 10 ? '0' + seconds : seconds
- return minutes + ':' + seconds
-}
-
-function delay_exponential (base, growthFactor, attempts) {
- /* Calculate number of beats between syncs based on exponential function.
- The format is::
-
- base * growthFactor ^ (attempts - 1)
-
- If ``base`` is set to 'rand' then a random number between
- 0 and 1 will be used as the base.
- Base must be greater than 0.
- */
- if (base === 'rand') {
- base = Math.random()
- }
- beats = base * (Math.pow(growthFactor, (attempts - 1)))
- return beats
-}
-
-function reset () {
- synced = false
- consecutiveSyncs = 0
- syncsLeft = MAX_SYNCS
- callbackHeartbeats = START_BEATS
- targetPosition = null
-}
-
-function setProgressTimer (pos) {
- reset()
- targetPosition = pos
- progressTimer.set(pos)
- if (!play) {
- // Set lapsed time and slider position directly as timer callback is not currently
- // running.
- positionNode.nodeValue = format(pos)
- $('#trackslider').val(pos).slider('refresh')
- }
-}
-
-function updatePosition (pos) {
- positionNode.nodeValue = format(pos)
-}
-
-function startProgressTimer () {
- reset()
- progressTimer.start()
-}
-
-function resetProgressTimer () {
- progressTimer.reset()
- reset()
- targetPosition = 0
-}
diff --git a/mopidy_musicbox_webclient/static/js/synced_timer.js b/mopidy_musicbox_webclient/static/js/synced_timer.js
new file mode 100644
index 0000000..25b2eb9
--- /dev/null
+++ b/mopidy_musicbox_webclient/static/js/synced_timer.js
@@ -0,0 +1,218 @@
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define([], factory)
+ } else if (typeof module === 'object' && module.exports) {
+ module.exports = factory()
+ } else {
+ root.SyncedProgressTimer = factory()
+ }
+}(this, function () {
+ 'use strict'
+
+ function delay_exponential (base, growthFactor, attempts) {
+ /* Calculate number of beats between syncs based on exponential function.
+ The format is::
+
+ base * growthFactor ^ (attempts - 1)
+
+ If ``base`` is set to 'rand' then a random number between
+ 0 and 1 will be used as the base.
+ Base must be greater than 0.
+ */
+ if (base === 'rand') {
+ base = Math.random()
+ }
+ // console.log(base + ' * (Math.pow(' + growthFactor + ', (' + attempts + ' - 1)) = ' + base * (Math.pow(growthFactor, (attempts - 1))))
+ return base * (Math.pow(growthFactor, (attempts - 1)))
+ }
+
+ function SyncedProgressTimer (maxAttempts, mopidy) {
+ if (!(this instanceof SyncedProgressTimer)) {
+ return new SyncedProgressTimer(maxAttempts, mopidy)
+ }
+
+ this.positionNode = document.createTextNode('')
+ this.durationNode = document.createTextNode('')
+
+ $('#songelapsed').append(this.positionNode)
+ $('#songlength').append(this.durationNode)
+
+ this._progressTimer = new ProgressTimer({
+ // Make sure that the timer object's context is available.
+ callback: $.proxy(this.timerCallback, this)
+ })
+
+ this._maxAttempts = maxAttempts
+ this._mopidy = mopidy
+ this._isConnected = false
+ this._mopidy.on('state:online', $.proxy(function () { this._isConnected = true }), this)
+ this._mopidy.on('state:offline', $.proxy(function () { this._isConnected = false }), this)
+ this.init()
+ }
+
+ SyncedProgressTimer.SYNC_STATE = {
+ NOT_SYNCED: 0,
+ SYNCING: 1,
+ SYNCED: 2
+ }
+
+ SyncedProgressTimer.format = function (milliseconds) {
+ if (milliseconds === Infinity) {
+ return '(n/a)'
+ } else if (milliseconds === 0) {
+ return '0:00'
+ }
+
+ var seconds = Math.floor(milliseconds / 1000)
+ var minutes = Math.floor(seconds / 60)
+ seconds = seconds % 60
+
+ seconds = seconds < 10 ? '0' + seconds : seconds
+ return minutes + ':' + seconds
+ }
+
+ SyncedProgressTimer.prototype.init = function () {
+ this.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED
+ this._syncAttemptsRemaining = this._maxAttempts
+
+ this.positionNode.nodeValue = ''
+ this.durationNode.nodeValue = ''
+
+ this._scheduledSyncTime = null
+ this._previousSyncPosition = null
+ this._duration = null
+
+ return this
+ }
+
+ SyncedProgressTimer.prototype.timerCallback = function (position, duration) {
+ this._update(position, duration)
+
+ if (this._isConnected && this._isSyncScheduled()) {
+ this._doSync(position, duration)
+ }
+ }
+
+ SyncedProgressTimer.prototype._update = function (position, duration) {
+ if (!(duration === Infinity && position === 0)) {
+ // Timer has been properly initialized.
+ this.durationNode.nodeValue = SyncedProgressTimer.format(duration || Infinity)
+ switch (this.syncState) {
+ case SyncedProgressTimer.SYNC_STATE.NOT_SYNCED:
+ // Waiting for Mopidy to provide a target position.
+ this.positionNode.nodeValue = '(wait)'
+ break
+ case SyncedProgressTimer.SYNC_STATE.SYNCING:
+ // Busy seeking to new target position.
+ this.positionNode.nodeValue = '(sync)'
+ break
+ case SyncedProgressTimer.SYNC_STATE.SYNCED:
+ this._previousSyncPosition = position
+ this.positionNode.nodeValue = SyncedProgressTimer.format(position)
+ $('#trackslider').val(position).slider('refresh')
+ break
+ }
+ } else {
+ // Make sure that default values are displayed while the timer is being initialized.
+ this.positionNode.nodeValue = ''
+ this.durationNode.nodeValue = ''
+ $('#trackslider').val(0).slider('refresh')
+ }
+ }
+
+ SyncedProgressTimer.prototype._isSyncScheduled = function () {
+ return this._scheduledSyncTime !== null && this._scheduledSyncTime <= new Date().getTime()
+ }
+
+ SyncedProgressTimer.prototype._doSync = function (position, duration) {
+ var ready = !(duration === Infinity && position === 0) // Timer has been properly initialized.
+ if (!ready) {
+ // Don't try to sync if progress timer has not been initialized yet.
+ return
+ }
+ var _this = this
+ _this._mopidy.playback.getTimePosition().then(function (targetPosition) {
+ if (Math.abs(targetPosition - position) <= 500) {
+ // Less than 500ms == in sync.
+ _this.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING
+ _this._syncAttemptsRemaining = Math.max(_this._syncAttemptsRemaining - 1, 0)
+ if (_this._syncAttemptsRemaining < _this._maxAttempts - 1 && _this._previousSyncPosition !== targetPosition) {
+ // Need at least two consecutive syncs to know that Mopidy
+ // is progressing playback and we are in sync.
+ _this.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED
+ }
+ _this._previousSyncPosition = targetPosition
+ // Step back exponentially while increasing number of callbacks.
+ _this._scheduledSyncTime = new Date().getTime() +
+ delay_exponential(0.25, 2, _this._maxAttempts - _this._syncAttemptsRemaining) * 1000
+ } else {
+ // Drift is too large, re-sync with Mopidy.
+ _this._syncAttemptsRemaining = _this._maxAttempts
+ _this._scheduledSyncTime = new Date().getTime() + 100
+ _this._previousSyncPosition = null
+ _this.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING
+ _this._progressTimer.set(targetPosition)
+ }
+ })
+ }
+
+ SyncedProgressTimer.prototype.set = function (position, duration) {
+ if (arguments.length === 0) {
+ throw new Error('"SyncedProgressTimer.set" requires the "position" argument.')
+ }
+ // Workaround for https://github.com/adamcik/media-progress-timer/issues/3
+ // This causes the timer to die unexpectedly if the position exceeds
+ // the duration slightly.
+ if (this._duration && this._duration < position) {
+ position = this._duration - 1
+ }
+ this.init()
+ if (arguments.length === 1) {
+ this._progressTimer.set(position)
+ } else {
+ this._duration = duration
+ this._progressTimer.set(position, duration)
+ }
+
+ if (!this._isSyncScheduled()) {
+ // Set lapsed time and slider position directly as timer callback is not currently
+ // running.
+ this.positionNode.nodeValue = SyncedProgressTimer.format(position)
+ if (arguments.length === 2) {
+ this.durationNode.nodeValue = SyncedProgressTimer.format(duration)
+ }
+ $('#trackslider').val(position).slider('refresh')
+ }
+
+ return this
+ }
+
+ SyncedProgressTimer.prototype.start = function () {
+ this.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED
+ this._scheduledSyncTime = new Date().getTime()
+ this._progressTimer.start()
+ return this
+ }
+
+ SyncedProgressTimer.prototype.stop = function () {
+ this._progressTimer.stop()
+ this._scheduledSyncTime = null
+ this.updatePosition(this._previousSyncPosition)
+ return this
+ }
+
+ SyncedProgressTimer.prototype.reset = function () {
+ this._progressTimer.reset()
+ this.stop()
+ this.init()
+ this.set(0, Infinity)
+
+ return this
+ }
+
+ SyncedProgressTimer.prototype.updatePosition = function (position) {
+ this.positionNode.nodeValue = SyncedProgressTimer.format(position)
+ }
+
+ return SyncedProgressTimer
+}))
diff --git a/mopidy_musicbox_webclient/static/mb.appcache b/mopidy_musicbox_webclient/static/mb.appcache
index c825720..246f924 100644
--- a/mopidy_musicbox_webclient/static/mb.appcache
+++ b/mopidy_musicbox_webclient/static/mb.appcache
@@ -1,6 +1,6 @@
CACHE MANIFEST
-# 2016-03-28:v1
+# 2016-04-03:v1
NETWORK:
*
@@ -26,7 +26,7 @@ js/gui.js
js/images.js
js/library.js
js/process_ws.js
-js/progress_timer.js
+js/synced_timer.js
mb.appcache
system.html
vendors/font_awesome/css/font-awesome.css
diff --git a/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js b/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js
index 138605f..4592fc0 100644
--- a/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js
+++ b/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js
@@ -1,128 +1,159 @@
-/*! timer.js v2.0.2
+/*! timer.js v3.0.0
* https://github.com/adamcik/media-progress-timer
- * Copyright (c) 2015 Thomas Adamcik
+ * Copyright (c) 2015-2016 Thomas Adamcik
* Licensed under the Apache License, Version 2.0 */
-(function() {
-
-'use strict';
-
-var now = typeof window.performance !== 'undefined' &&
- typeof window.performance.now !== 'undefined' &&
- window.performance.now.bind(window.performance) || Date.now ||
- function() { return new Date().getTime(); };
-
-function ProgressTimer(options) {
- if (!(this instanceof ProgressTimer)) {
- return new ProgressTimer(options);
- } else if (typeof options === 'function') {
- options = {'callback': options};
- } else if (typeof options !== 'object') {
- throw 'ProgressTimer must be called with a callback or options.';
- } else if (typeof options.callback !== 'function') {
- throw 'ProgressTimer needs a callback to operate.';
- }
-
- this._running = false;
- this._updateRate = Math.max(options.updateRate || 100, 10);
- this._callback = options.callback;
- this._fallback = typeof window.requestAnimationFrame === 'undefined' ||
- options.disableRequestAnimationFrame|| false;
-
- if (!this._fallback) {
- this._callUpdate = this._scheduleAnimationFrame;
- this._scheduleUpdate = this._scheduleAnimationFrame;
- }
-
- this._boundCallUpdate = this._callUpdate.bind(this);
- this._boundUpdate = this._update.bind(this);
-
- this.reset();
-}
-
-ProgressTimer.prototype.set = function(position, duration) {
- if (arguments.length === 0) {
- throw 'set requires at least a position argument.';
- } else if (arguments.length === 1) {
- duration = this._state.duration;
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define([], factory);
+ } else if (typeof module === 'object' && module.exports) {
+ module.exports = factory();
} else {
- duration = Math.floor(
- Math.max(duration === null ? Infinity : duration || 0, 0));
+ root.ProgressTimer = factory();
}
- position = Math.floor(Math.min(Math.max(position || 0, 0), duration));
+}(this, function () {
+ 'use strict';
- this._state = {
- initialTimestamp: null,
- initialPosition: position,
- previousPosition: position,
- duration: duration
+ // Helper function to provide a reference time in milliseconds.
+ var now = typeof window.performance !== 'undefined' &&
+ typeof window.performance.now !== 'undefined' &&
+ window.performance.now.bind(window.performance) || Date.now ||
+ function() { return new Date().getTime(); };
+
+ // Helper to warn library users about deprecated features etc.
+ function warn(msg) {
+ window.setTimeout(function() { throw msg; }, 0);
+ }
+
+ // Creates a new timer object, works with both 'new ProgressTimer(options)'
+ // and just 'ProgressTimer(options). Optionally the timer can also be
+ // called with only the callback instead of options.
+ function ProgressTimer(options) {
+ if (!(this instanceof ProgressTimer)) {
+ return new ProgressTimer(options);
+ } else if (typeof options === 'function') {
+ options = {'callback': options};
+ } else if (typeof options !== 'object') {
+ throw '"ProgressTimer" must be called with a callback or options.';
+ } else if (typeof options['callback'] !== 'function') {
+ throw '"ProgressTimer" needs a callback to operate.';
+ }
+
+ this._userCallback = options['callback'];
+ this._updateId = null;
+ this._state = null; // Gets initialized by the set() call.
+
+ var frameDuration = 1000 / (options['fallbackTargetFrameRate'] || 30);
+ // TODO: Remove this legacy code path at some point.
+ if (options['updateRate'] && !options['fallbackTargetFrameRate']) {
+ warn('"ProgressTimer" no longer supports the updateRate option.');
+ frameDuration = Math.max(options['updateRate'], 1000 / 60);
+ }
+
+ var useFallback = (
+ typeof window.requestAnimationFrame === 'undefined' ||
+ typeof window.cancelAnimationFrame === 'undefined' ||
+ options['disableRequestAnimationFrame'] || false);
+
+ // Make sure this works in _update.
+ var update = this._update.bind(this);
+
+ if (useFallback) {
+ this._schedule = function(timestamp) {
+ var timeout = Math.max(timestamp + frameDuration - now(), 0);
+ return window.setTimeout(update, Math.floor(timeout));
+ };
+ this._cancel = window.clearTimeout.bind(window);
+ } else {
+ this._schedule = window.requestAnimationFrame.bind(window, update);
+ this._cancel = window.cancelAnimationFrame.bind(window);
+ }
+
+ this.reset(); // Reuse reset code to ensure we start in the same state.
+ }
+
+ // If called with one argument the previous duration is preserved. Note
+ // that the position can be changed while the timer is running.
+ ProgressTimer.prototype.set = function(position, duration) {
+ if (arguments.length === 0) {
+ throw '"ProgressTimer.set" requires the "position" arugment.';
+ } else if (arguments.length === 1) {
+ // Fallback to previous duration, whatever that was.
+ duration = this._state.duration;
+ } else {
+ // Round down and make sure zero and null are treated as inf.
+ duration = Math.floor(Math.max(
+ duration === null ? Infinity : duration || Infinity, 0));
+ }
+ // Make sure '0 <= position <= duration' always holds.
+ position = Math.floor(Math.min(Math.max(position || 0, 0), duration));
+
+ this._state = {
+ initialTimestamp: null,
+ initialPosition: position,
+ position: position,
+ duration: duration
+ };
+
+ // Update right away if we don't have anything running.
+ if (this._updateId === null) {
+ // TODO: Consider wrapping this in a try/catch?
+ this._userCallback(position, duration);
+ }
+ return this;
};
- this._callback(position, duration, this._running);
- return this;
-};
-
-ProgressTimer.prototype.start = function() {
- this._running = true;
- this._callUpdate();
- return this;
-};
-
-ProgressTimer.prototype.stop = function() {
- this._running = false;
- var state = this._state;
- return this.set(state.previousPosition, state.duration);
-};
-
-ProgressTimer.prototype.reset = function() {
- this._running = false;
- return this.set(0, Infinity);
-};
-
-ProgressTimer.prototype._callUpdate = function() {
- this._update(now());
-};
-
-ProgressTimer.prototype._scheduleUpdate = function(timestamp) {
- var adjustedTimeout = timestamp + this._updateRate - now();
- setTimeout(this._boundCallUpdate, adjustedTimeout);
-};
-
-ProgressTimer.prototype._scheduleAnimationFrame = function() {
- window.requestAnimationFrame(this._boundUpdate);
-};
-
-ProgressTimer.prototype._update = function(timestamp) {
- if (!this._running) {
- return;
- }
-
- var state = this._state;
- state.initialTimestamp = state.initialTimestamp || timestamp;
-
- var position = state.initialPosition + timestamp - state.initialTimestamp;
- var duration = state.duration;
-
- if (position < duration || duration === null) {
- var delta = position - state.previousPosition;
- if (delta >= this._updateRate || this._fallback) {
- this._callback(Math.floor(position), duration, this._running);
- state.previousPosition = position;
+ // Start the timer if it is not already running.
+ ProgressTimer.prototype.start = function() {
+ if (this._updateId === null) {
+ this._updateId = this._schedule(0);
}
- } else {
- // Workaround for https://github.com/adamcik/media-progress-timer/issues/3
- // This causes the timer to die unexpectedly if the position goes
- // over the duration slightly.
- // this._running = false;
- this._callback(duration, duration, this._running);
- }
- this._scheduleUpdate(timestamp);
-};
+ return this;
+ };
-if(typeof module !== 'undefined') {
- module.exports = ProgressTimer;
-} else {
- window.ProgressTimer = ProgressTimer;
-}
+ // Cancel the timer if it us currently tracking progress.
+ ProgressTimer.prototype.stop = function() {
+ if (this._updateId !== null) {
+ this._cancel(this._updateId);
-}());
\ No newline at end of file
+ // Ensure we correctly reset the initial position and timestamp.
+ this.set(this._state.position, this._state.duration);
+ this._updateId = null; // Last step to avoid callback in set()
+ }
+ return this;
+ };
+
+ // Marks the timer as stopped, sets position to zero and duration to inf.
+ ProgressTimer.prototype.reset = function() {
+ return this.stop().set(0, Infinity);
+ };
+
+ // Calls the user callback with the current position/duration and then
+ // schedules the next update run via _schedule if we haven't finished.
+ ProgressTimer.prototype._update = function(timestamp) {
+ var state = this._state; // We refer a lot to state, this is shorter.
+
+ // Make sure setTimeout has a timestamp and store first reference time.
+ timestamp = timestamp || now();
+ state.initialTimestamp = state.initialTimestamp || timestamp;
+
+ // Recalculate position according to start location and reference.
+ state.position = (
+ state.initialPosition + timestamp - state.initialTimestamp);
+
+ // Ensure callback gets an integer and that 'position <= duration'.
+ var userPosisition = Math.min(
+ Math.floor(state.position), state.duration);
+
+ // TODO: Consider wrapping this in a try/catch?
+ this._userCallback(userPosisition, state.duration);
+
+ if (state.position < state.duration) {
+ this._updateId = this._schedule(timestamp); // Schedule update.
+ } else {
+ this._updateId = null; // Unset since we didn't reschedule.
+ }
+ };
+
+ return ProgressTimer;
+}));
\ No newline at end of file
diff --git a/tests/test_images.js b/tests/js/test_images.js
similarity index 71%
rename from tests/test_images.js
rename to tests/js/test_images.js
index 0d8769f..4cd6ab8 100644
--- a/tests/test_images.js
+++ b/tests/js/test_images.js
@@ -1,5 +1,4 @@
var chai = require('chai')
-var should = chai.should()
var expect = chai.expect
var assert = chai.assert
chai.use(require('chai-string'))
@@ -7,24 +6,20 @@ chai.use(require('chai-jquery'))
var sinon = require('sinon')
-var coverArt = require('../mopidy_musicbox_webclient/static/js/images.js')
-
-var images
-
-before(function () {
- mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'}))
- images = $(' ')
-})
+var coverArt = require('../../mopidy_musicbox_webclient/static/js/images.js')
describe('CoverArt', function () {
+ var mopidy
+ var images
+ beforeEach(function () {
+ mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'}))
+ images = $(' ')
+ $(images).removeAttr('src')
+ })
describe('#getCover()', function () {
- beforeEach(function () {
- $(images).removeAttr('src')
- })
-
it('should use default image if no track URI is provided', function () {
- coverArt.getCover('', images, '')
- $(images).prop('src').should.endWith('images/default_cover.png')
+ coverArt.getCover('', images, '', mopidy)
+ expect($(images).prop('src')).to.endWith('images/default_cover.png')
})
it('should get image from Mopidy, if available', function () {
@@ -33,10 +28,10 @@ describe('CoverArt', function () {
mopidy.library = library
var getImagesSpy = sinon.spy(mopidy.library, 'getImages')
- coverArt.getCover('mock:track:uri', images, '')
+ coverArt.getCover('mock:track:uri', images, '', mopidy)
- assert(getImagesSpy.calledOnce)
- $(images).prop('src').should.endWith('mockImageUri')
+ assert.isTrue(getImagesSpy.calledOnce)
+ expect($(images).prop('src')).to.endWith('mockImageUri')
})
it('should fall back to retrieving image from deprecated track.album.images', function () {
@@ -51,21 +46,17 @@ describe('CoverArt', function () {
var getImagesSpy = sinon.spy(mopidy.library, 'getImages')
var getCoverFromAlbumSpy = sinon.spy(coverArt, 'getCoverFromAlbum')
- coverArt.getCover('mock:track:uri', images, '')
+ coverArt.getCover('mock:track:uri', images, '', mopidy)
- assert(getImagesSpy.calledOnce)
- assert(getCoverFromAlbumSpy.calledOnce)
+ assert.isTrue(getImagesSpy.calledOnce)
+ assert.isTrue(getCoverFromAlbumSpy.calledOnce)
})
})
describe('#getCoverFromAlbum()', function () {
- beforeEach(function () {
- $(images).removeAttr('src')
- })
-
it('should use default image if no track URI is provided', function () {
- coverArt.getCoverFromAlbum('', images, '')
- $(images).prop('src').should.endWith('images/default_cover.png')
+ coverArt.getCoverFromAlbum('', images, '', mopidy)
+ expect($(images).prop('src')).to.endWith('images/default_cover.png')
})
it('should get image from Mopidy track.album.images, if available', function () {
@@ -76,10 +67,10 @@ describe('CoverArt', function () {
mopidy.library = library
var lookupSpy = sinon.spy(mopidy.library, 'lookup')
- coverArt.getCoverFromAlbum('mock:track:uri', images, '')
+ coverArt.getCoverFromAlbum('mock:track:uri', images, '', mopidy)
- assert(lookupSpy.calledOnce)
- $(images).prop('src').should.endWith('mockAlbumImageUri')
+ assert.isTrue(lookupSpy.calledOnce)
+ expect($(images).prop('src')).to.endWith('mockAlbumImageUri')
})
it('should use default image if track.album or track.artist is not available', function () {
@@ -90,10 +81,10 @@ describe('CoverArt', function () {
mopidy.library = library
var lookupSpy = sinon.spy(mopidy.library, 'lookup')
- coverArt.getCoverFromAlbum('mock:track:uri', images, '')
+ coverArt.getCoverFromAlbum('mock:track:uri', images, '', mopidy)
- assert(lookupSpy.calledOnce)
- $(images).prop('src').should.endWith('images/default_cover.png')
+ assert.isTrue(lookupSpy.calledOnce)
+ expect($(images).prop('src')).to.endWith('images/default_cover.png')
})
it('should fall back to retrieving image from last.fm if none provided by Mopidy', function () {
@@ -103,21 +94,18 @@ describe('CoverArt', function () {
}
mopidy.library = library
- var getCoverFromLastFmSpy = sinon.spy(coverArt, 'getCoverFromLastFm')
- coverArt.getCoverFromAlbum('mock:track:uri', images, '')
+ var getCoverFromLastFmStub = sinon.stub(coverArt, 'getCoverFromLastFm')
+ coverArt.getCoverFromAlbum('mock:track:uri', images, '', mopidy)
- assert(getCoverFromLastFmSpy.calledOnce)
+ assert.isTrue(getCoverFromLastFmStub.calledOnce)
+ getCoverFromLastFmStub.restore()
})
})
describe('#getCoverFromLastFm()', function () {
- beforeEach(function () {
- $(images).removeAttr('src')
- })
-
it('should use default image if no track is provided', function () {
coverArt.getCoverFromLastFm(undefined, images, '')
- $(images).prop('src').should.endWith('images/default_cover.png')
+ expect($(images).prop('src')).to.endWith('images/default_cover.png')
})
it('should fall back to using track artist if album artist is not available', function () {
@@ -129,7 +117,7 @@ describe('CoverArt', function () {
coverArt.getCoverFromLastFm(track, images, '')
var args = getInfoStub.args
- assert(args[0][0].artist === 'artistMock')
+ assert.equal(args[0][0].artist, 'artistMock')
getInfoStub.restore()
})
@@ -141,7 +129,7 @@ describe('CoverArt', function () {
getInfoStub.yieldsTo('success', getInfoResultMock)
coverArt.getCoverFromLastFm(track, images, 'small')
- $(images).prop('src').should.endWith('mockAlbumImageUri')
+ expect($(images).prop('src')).to.endWith('mockAlbumImageUri')
getInfoStub.restore()
})
@@ -150,23 +138,19 @@ describe('CoverArt', function () {
var getInfoStub = sinon.stub(coverArt.lastfm.album, 'getInfo')
getInfoStub.yieldsTo('error', 'code', 'message')
- var consoleSpy = sinon.spy(console, 'error')
+ var consoleStub = sinon.stub(console, 'error')
coverArt.getCoverFromLastFm(track, images, '')
- assert(consoleSpy.calledOnce)
+ assert.isTrue(consoleStub.calledOnce)
getInfoStub.restore()
- consoleSpy.restore()
+ consoleStub.restore()
})
})
describe('#getArtistImage()', function () {
- beforeEach(function () {
- $(images).removeAttr('src')
- })
-
it('should use default image if no artist is provided', function () {
coverArt.getArtistImage('', images, '')
- $(images).prop('src').should.endWith('images/user_24x32.png')
+ expect($(images).prop('src')).to.endWith('images/user_24x32.png')
})
it('should get artist info from last.fm', function () {
@@ -176,7 +160,7 @@ describe('CoverArt', function () {
getInfoStub.yieldsTo('success', getInfoResultMock)
coverArt.getArtistImage('mockArtist', images, 'small')
- $(images).prop('src').should.endWith('mockArtistImageUri')
+ expect($(images).prop('src')).to.endWith('mockArtistImageUri')
getInfoStub.restore()
})
@@ -184,12 +168,12 @@ describe('CoverArt', function () {
var getInfoStub = sinon.stub(coverArt.lastfm.artist, 'getInfo')
getInfoStub.yieldsTo('error', 'code', 'message')
- var consoleSpy = sinon.spy(console, 'error')
+ var consoleStub = sinon.stub(console, 'error')
coverArt.getArtistImage('mockArtist', images, 'small')
- assert(consoleSpy.calledOnce)
+ assert.isTrue(consoleStub.calledOnce)
getInfoStub.restore()
- consoleSpy.restore()
+ consoleStub.restore()
})
})
})
diff --git a/tests/js/test_library.js b/tests/js/test_library.js
new file mode 100644
index 0000000..47c2637
--- /dev/null
+++ b/tests/js/test_library.js
@@ -0,0 +1,56 @@
+var chai = require('chai')
+var expect = chai.expect
+var assert = chai.assert
+chai.use(require('chai-string'))
+chai.use(require('chai-jquery'))
+
+var sinon = require('sinon')
+
+var library = require('../../mopidy_musicbox_webclient/static/js/library.js')
+
+describe('Library', function () {
+ var selectID = '#selectSearchService'
+ var schemesArray = ['mockScheme1', 'mockScheme2', 'mockScheme3']
+ var mopidy = { getUriSchemes: function () { return $.when(schemesArray) } }
+
+ before(function () {
+ $(document.body).append(' ')
+ $('#selectSearchService').selectmenu()
+ })
+ describe('#getSearchSchemes()', function () {
+ beforeEach(function () {
+ $(selectID).empty()
+ })
+
+ it('should add human-readable options for backend schemes', function () {
+ uriHumanList = [['mockScheme2', 'mockUriHuman2']]
+
+ library.getSearchSchemes([], mopidy)
+ assert.equal($(selectID).children().length, schemesArray.length + 1)
+ expect($(selectID).children(':eq(2)')).to.have.text('MockUriHuman2')
+ })
+
+ it('should get default value from cookie', function () {
+ $.cookie('searchScheme', 'mockScheme3')
+ library.getSearchSchemes([], mopidy)
+ expect($(selectID + ' option:selected')).to.have.value('mockScheme3')
+ })
+
+ it('should default to "all" backends if no cookie is available', function () {
+ $.removeCookie('searchScheme')
+ library.getSearchSchemes([], mopidy)
+ expect($(selectID + ' option:selected')).to.have.value('all')
+ })
+
+ it('should capitalize first character of backend schema', function () {
+ library.getSearchSchemes([], mopidy)
+ expect($(selectID).children(':eq(1)')).to.have.text('MockScheme1')
+ })
+
+ it('should blacklist services that should not be searched', function () {
+ library.getSearchSchemes(['mockScheme2'], mopidy)
+ assert.equal($(selectID).children().length, schemesArray.length)
+ expect($(selectID).children()).not.to.contain('mockScheme2')
+ })
+ })
+})
diff --git a/tests/js/test_synced_timer.js b/tests/js/test_synced_timer.js
new file mode 100644
index 0000000..458c294
--- /dev/null
+++ b/tests/js/test_synced_timer.js
@@ -0,0 +1,352 @@
+var chai = require('chai')
+var expect = chai.expect
+var assert = chai.assert
+chai.use(require('chai-string'))
+chai.use(require('chai-jquery'))
+
+var sinon = require('sinon')
+
+var SyncedProgressTimer = require('../../mopidy_musicbox_webclient/static/js/synced_timer.js')
+
+describe('SyncedTimer', function () {
+ var MAX_ATTEMPTS = 8
+ var syncedProgressTimer
+ var mopidy
+ var playback
+ var getTimePositionStub
+ before(function () {
+ $(document.body).append(
+ '' +
+ ' ' +
+ ' ' +
+ 'Position ' +
+ ' ' +
+ '
'
+ )
+ $('#trackslider').slider() // Initialize slider
+ })
+ beforeEach(function () {
+ playback = {
+ getTimePosition: function () { return $.when(1000) },
+ getState: function () { return $.when('stopped') }
+ }
+ mopidy = new Mopidy({callingConvention: 'by-position-or-by-name'})
+ mopidy.playback = playback
+ getTimePositionStub = sinon.stub(playback, 'getTimePosition')
+ // Simulate Mopidy's track position advancing 10ms between each call.
+ for (var i = 0; i < MAX_ATTEMPTS; i++) {
+ getTimePositionStub.onCall(i).returns($.when(1000 + i * 10))
+ }
+ mopidy = sinon.stub(mopidy)
+ syncedProgressTimer = new SyncedProgressTimer(MAX_ATTEMPTS, mopidy)
+ syncedProgressTimer._isConnected = true
+ })
+ afterEach(function () {
+ getTimePositionStub.restore()
+ })
+ describe('#SyncedTimer()', function () {
+ it('should add text nodes to DOM for position and duration indicators', function () {
+ expect($('#songelapsed')).to.have.text('')
+ expect($('#songlength')).to.have.text('')
+ })
+
+ it('should start out in unsynced state', function () {
+ assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED)
+ })
+ })
+
+ describe('#format()', function () {
+ it('should set value of text node', function () {
+ assert.equal(SyncedProgressTimer.format(1000), '0:01')
+ })
+
+ it('should handle Infinity', function () {
+ assert.equal(SyncedProgressTimer.format(Infinity), '(n/a)')
+ })
+
+ it('should handle zero', function () {
+ assert.equal(SyncedProgressTimer.format(0), '0:00')
+ })
+ })
+
+ describe('#timerCallback()', function () {
+ var clock
+ beforeEach(function () {
+ clock = sinon.useFakeTimers()
+ syncedProgressTimer._progressTimer = new ProgressTimer({
+ callback: $.proxy(syncedProgressTimer.timerCallback, syncedProgressTimer),
+ disableRequestAnimationFrame: true // No window available during testing - use fallback mechanism to schedule updates
+ })
+ })
+ afterEach(function () {
+ clock.restore()
+ })
+
+ it('should not try to sync unless connected to mopidy', function () {
+ syncedProgressTimer._isConnected = false
+ var _syncScheduledStub = sinon.stub(syncedProgressTimer, '_isSyncScheduled')
+ assert.isFalse(_syncScheduledStub.called, '_syncScheduledStub called')
+ _syncScheduledStub.restore()
+ })
+
+ it('should update text nodes', function () {
+ var updateStub = sinon.stub(syncedProgressTimer, '_update')
+ syncedProgressTimer.set(0, 1000).start()
+ assert.isTrue(updateStub.called, '_update not called')
+ updateStub.restore()
+ })
+
+ it('should check if a sync is scheduled', function () {
+ var scheduleStub = sinon.stub(syncedProgressTimer, '_isSyncScheduled').returns(true)
+ syncedProgressTimer.set(0, 1000).start()
+ assert.isTrue(scheduleStub.called, '_isSyncScheduled not called')
+ scheduleStub.restore()
+ })
+
+ it('should attempt to perform a sync when scheduled', function () {
+ var syncStub = sinon.stub(syncedProgressTimer, '_doSync')
+ syncedProgressTimer.set(0, 1000).start()
+ clock.tick(250)
+ assert.isTrue(syncStub.called, '_doSync not called')
+ syncStub.restore()
+ })
+
+ it('should perform sync', function () {
+ // Simulate runtime on a 5-second track
+ assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED, 'Timer was initialized in incorrect state')
+ syncedProgressTimer.set(0, 5000).start()
+ var wasSyncing = false
+ for (var i = 0; i < MAX_ATTEMPTS; i++) {
+ clock.tick(250) // 250ms * MAX_ATTEMPTS is only 2 seconds, but we'll be synced after only two attempts
+ wasSyncing = wasSyncing || syncedProgressTimer.syncState === SyncedProgressTimer.SYNC_STATE.SYNCING
+ }
+ assert.isTrue(wasSyncing, 'Timer never entered the "syncing" state')
+ assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCED, 'Timer failed to sync')
+ })
+ })
+
+ describe('#_update()', function () {
+ it('should clear timers and reset slider to zero while not ready', function () {
+ syncedProgressTimer.positionNode.nodeValue = '1:00'
+ syncedProgressTimer.durationNode.nodeValue = '2:00'
+ $('#trackslider').val(100).slider('refresh')
+ syncedProgressTimer._update(0, Infinity)
+
+ assert.equal(syncedProgressTimer.positionNode.nodeValue, '')
+ assert.equal(syncedProgressTimer.durationNode.nodeValue, '')
+ assert.equal($('#trackslider').val(), 0)
+ })
+
+ it('should set duration to "(n/a)" for tracks with infinite duration (e.g. streams)', function () {
+ syncedProgressTimer._update(1000, Infinity)
+ assert.equal(syncedProgressTimer.durationNode.nodeValue, '(n/a)')
+ })
+
+ it('should show "(wait)" while waiting for Mopidy to supply a position', function () {
+ syncedProgressTimer._update(1000, 2000)
+ assert.equal(syncedProgressTimer.positionNode.nodeValue, '(wait)')
+ })
+
+ it('should show "(sync)" while trying to sync up with Mopidy', function () {
+ syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING
+ syncedProgressTimer._update(1000, 2000)
+ assert.equal(syncedProgressTimer.positionNode.nodeValue, '(sync)')
+ })
+
+ it('should update position text and position track slider when synced', function () {
+ syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED
+ syncedProgressTimer._update(1000, 2000)
+ assert.equal(syncedProgressTimer.positionNode.nodeValue, '0:01')
+ assert.equal($('#trackslider').val(), 1000)
+ })
+ })
+
+ describe('#_isSyncScheduled()', function () {
+ var scheduleSpy
+ var clock
+ before(function () {
+ scheduleSpy = sinon.spy(syncedProgressTimer, '_isSyncScheduled')
+ clock = sinon.useFakeTimers()
+ })
+ after(function () {
+ scheduleSpy.restore()
+ clock.restore()
+ })
+ it('should schedule sync when scheduled time arrives', function () {
+ syncedProgressTimer._scheduledSyncTime = new Date().getTime() + 1000
+ assert.isFalse(syncedProgressTimer._isSyncScheduled())
+ clock.tick(1000)
+ assert.isTrue(syncedProgressTimer._isSyncScheduled())
+ })
+ })
+
+ describe('#_doSync', function () {
+ var clock
+ beforeEach(function () {
+ clock = sinon.useFakeTimers()
+ })
+ afterEach(function () {
+ clock.restore()
+ })
+ it('should not try to sync until timer has been set', function () {
+ syncedProgressTimer._doSync(0, Infinity)
+ assert.isFalse(getTimePositionStub.called, 'getTimePosition called even though timer has not been set')
+ })
+ it('should request position from Mopidy', function () {
+ syncedProgressTimer._doSync(1000, 2000)
+ assert.isTrue(getTimePositionStub.called, 'getTimePosition not called')
+ })
+
+ it('should set state to synced after two consecutive successful syncs (i.e. time drift < 500ms)', function () {
+ assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED)
+ clock.tick(10)
+ syncedProgressTimer._doSync(1010, 2000)
+ assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING)
+ clock.tick(10)
+ syncedProgressTimer._doSync(1020, 2000)
+ assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCED)
+ })
+
+ it('should re-initialize and set state to syncing if time drift is more than 500ms', function () {
+ syncedProgressTimer._doSync(1, 2000)
+ assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING)
+ assert.equal(syncedProgressTimer._syncAttemptsRemaining, syncedProgressTimer._maxAttempts)
+ })
+
+ it('should step back exponentially while syncing', function () {
+ for (var i = 0; i < syncedProgressTimer._maxAttempts; i++) {
+ syncedProgressTimer._doSync(1000 + (i + 1) * 10, 2000)
+ // If we don't advance the clock then '_syncAttemptsRemaining' should just contain the step-back in seconds
+ assert.equal(syncedProgressTimer._syncAttemptsRemaining, syncedProgressTimer._maxAttempts - i - 1, 'Incorrect sync attempts remaining')
+ assert.equal(syncedProgressTimer._scheduledSyncTime, (0.25 * (Math.pow(2, i)) * 1000), 'Incorrect sync time scheduled')
+ }
+ })
+
+ it('should check sync every 32s once synced', function () {
+ syncedProgressTimer._syncAttemptsRemaining = 0
+ syncedProgressTimer._doSync(1000, 2000)
+ assert.equal(syncedProgressTimer._scheduledSyncTime, 32000)
+ })
+
+ it('should not sync unless track playback is progressing', function () {
+ getTimePositionStub.restore()
+ getTimePositionStub = sinon.stub(playback, 'getTimePosition')
+ getTimePositionStub.returns($.when(1000)) // Simulate playback 'stuck' at 1000ms.
+ assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED)
+ clock.tick(10)
+ syncedProgressTimer._doSync(1010, 2000)
+ assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING)
+ clock.tick(10)
+ syncedProgressTimer._doSync(1010, 2000)
+ assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING)
+ })
+ })
+
+ describe('#set()', function () {
+ it('should throw exception if no arguments are provided', function () {
+ assert.throw(function () { syncedProgressTimer.set() }, Error)
+ })
+
+ it('should set position if only one argument is provided', function () {
+ syncedProgressTimer.set(1000)
+ assert.equal(syncedProgressTimer._progressTimer._state.position, 1000)
+ })
+
+ it('should update track slider if no sync is scheduled', function () {
+ syncedProgressTimer.stop()
+ syncedProgressTimer.set(1000, 2000)
+ expect($('#songelapsed').text()).to.endWith('0:01')
+ assert.equal($('#trackslider').val(), 1000)
+ })
+
+ it('should implement workaround for https://github.com/adamcik/media-progress-timer/issues/3', function () {
+ syncedProgressTimer.set(1000, 2000).start()
+ assert.equal(syncedProgressTimer._duration, 2000)
+ syncedProgressTimer.set(3000)
+ assert.equal(syncedProgressTimer._progressTimer._state.position, 1999)
+ })
+ })
+
+ describe('#start()', function () {
+ it('should start timer', function () {
+ var startStub = sinon.stub(syncedProgressTimer._progressTimer, 'start')
+ syncedProgressTimer.start()
+ assert(startStub.called)
+ startStub.restore()
+ })
+
+ it('should always start in unsynced state', function () {
+ syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED
+ syncedProgressTimer.start()
+ assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED)
+ })
+
+ it('should schedule a sync immediately', function () {
+ var clock = sinon.useFakeTimers()
+ syncedProgressTimer._scheduledSyncTime = new Date().getTime() + 5000
+ expect(syncedProgressTimer._scheduledSyncTime).to.be.above(new Date().getTime())
+ syncedProgressTimer.start()
+ clock.tick(1000)
+ expect(syncedProgressTimer._scheduledSyncTime).to.be.below(new Date().getTime())
+ clock.restore()
+ })
+ })
+
+ describe('#stop()', function () {
+ it('should stop timer', function () {
+ var stopStub = sinon.stub(syncedProgressTimer._progressTimer, 'stop')
+ syncedProgressTimer.stop()
+ assert(stopStub.called)
+ stopStub.restore()
+ })
+
+ it('should show position when stopped', function () {
+ syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED
+ syncedProgressTimer._update(1000, 5000)
+ syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING
+ syncedProgressTimer._update(2000, 5000)
+ assert.equal(syncedProgressTimer.positionNode.nodeValue, '(sync)')
+ syncedProgressTimer.stop()
+ assert.equal(syncedProgressTimer.positionNode.nodeValue, '0:01')
+ })
+
+ it('should cancel any scheduled syncs', function () {
+ syncedProgressTimer._scheduledSyncTime = 5000
+ syncedProgressTimer.stop()
+ expect(syncedProgressTimer._scheduledSyncTime).to.be.null
+ })
+ })
+
+ describe('#reset()', function () {
+ it('should reset timer to 0:00 - (n/a) ', function () {
+ var resetStub = sinon.stub(syncedProgressTimer._progressTimer, 'reset')
+ var initStub = sinon.stub(syncedProgressTimer, 'init')
+ var stopStub = sinon.stub(syncedProgressTimer, 'stop')
+
+ syncedProgressTimer.reset()
+
+ assert(resetStub.called)
+ assert(initStub.called)
+ assert(stopStub.called)
+
+ assert.equal(syncedProgressTimer.positionNode.nodeValue, '0:00')
+ assert.equal(syncedProgressTimer.durationNode.nodeValue, '(n/a)')
+
+ resetStub.restore()
+ initStub.restore()
+ stopStub.restore()
+ })
+ })
+
+ describe('#updatePosition()', function () {
+ it('should format and set position node', function () {
+ var formatSpy = sinon.spy(SyncedProgressTimer, 'format')
+ assert.equal(syncedProgressTimer.positionNode.nodeValue, '')
+ syncedProgressTimer.updatePosition(1000)
+
+ assert.isTrue(formatSpy.called)
+ expect(syncedProgressTimer.positionNode.nodeValue).to.endWith('0:01')
+ formatSpy.restore()
+ })
+ })
+})
diff --git a/tests/test_library.js b/tests/test_library.js
deleted file mode 100644
index 1d3b19f..0000000
--- a/tests/test_library.js
+++ /dev/null
@@ -1,54 +0,0 @@
-var chai = require('chai')
-var should = chai.should()
-var expect = chai.expect
-var assert = chai.assert
-chai.use(require('chai-string'))
-chai.use(require('chai-jquery'))
-
-var sinon = require('sinon')
-
-var library = require('../mopidy_musicbox_webclient/static/js/library.js')
-
-var selectID = '#selectSearchService'
-var schemesArray
-
-before(function () {
- $(document.body).append(' ')
- $('#selectSearchService').selectmenu()
-})
-describe('Library', function () {
- describe('#getSearchSchemes()', function () {
- beforeEach(function () {
- schemesArray = ['mockScheme1', 'mockScheme2', 'mockScheme3']
- mopidy = {
- getUriSchemes: function () { return $.when(schemesArray) }
- }
- $(selectID).empty()
- })
-
- it('should add human-readable options for backend schemes', function () {
- uriHumanList = [['mockScheme2', 'mockUriHuman2']]
-
- library.getSearchSchemes()
- assert($(selectID).children().length === schemesArray.length + 1)
- $(selectID).children(':eq(2)').should.have.text('MockUriHuman2')
- })
-
- it('should get default value from cookie', function () {
- $.cookie('searchScheme', 'mockScheme3')
- library.getSearchSchemes()
- $(selectID + ' option:selected').should.have.value('mockScheme3')
- })
-
- it('should default to "all" backends if no cookie is available', function () {
- $.removeCookie('searchScheme')
- library.getSearchSchemes()
- $(selectID + ' option:selected').should.have.value('all')
- })
-
- it('should capitalize first character of backend schema', function () {
- library.getSearchSchemes()
- $(selectID).children(':eq(1)').should.have.text('MockScheme1')
- })
- })
-})