Merge remote-tracking branch 'upstream/develop' into develop
This commit is contained in:
commit
e2920920ed
@ -77,6 +77,7 @@ v2.3.0 (UNRELEASED)
|
|||||||
- Now displays album and artist info when browsing tracks. (Addresses: `#99 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/99>`_).
|
- Now displays album and artist info when browsing tracks. (Addresses: `#99 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/99>`_).
|
||||||
- Now remembers which backend was searched previously, and automatically selects that backend as the default search target.
|
- Now remembers which backend was searched previously, and automatically selects that backend as the default search target.
|
||||||
(Addresses: `#130 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/130>`_).
|
(Addresses: `#130 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/130>`_).
|
||||||
|
- Upgrade Media Progress Timer to version 3.0.0.
|
||||||
|
|
||||||
**Fixes**
|
**Fixes**
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,6 @@ module.exports = function (config) {
|
|||||||
// list of files / patterns to load in the browser
|
// list of files / patterns to load in the browser
|
||||||
files: [
|
files: [
|
||||||
'mopidy_musicbox_webclient/static/vendors/**/*.js',
|
'mopidy_musicbox_webclient/static/vendors/**/*.js',
|
||||||
// TODO: can remove the next line once JavaScript codebase has been modularized.
|
|
||||||
'mopidy_musicbox_webclient/static/js/**/*.js',
|
'mopidy_musicbox_webclient/static/js/**/*.js',
|
||||||
'tests/**/test_*.js'
|
'tests/**/test_*.js'
|
||||||
],
|
],
|
||||||
|
|||||||
@ -486,7 +486,7 @@
|
|||||||
<!-- /page one -->
|
<!-- /page one -->
|
||||||
<script type="text/javascript" src="vendors/mopidy/mopidy.min.js"></script>
|
<script type="text/javascript" src="vendors/mopidy/mopidy.min.js"></script>
|
||||||
<script type="text/javascript" src="vendors/media_progress_timer/timer.js"></script>
|
<script type="text/javascript" src="vendors/media_progress_timer/timer.js"></script>
|
||||||
<script type="text/javascript" src="js/progress_timer.js"></script>
|
<script type="text/javascript" src="js/synced_timer.js"></script>
|
||||||
<script type="text/javascript" src="js/controls.js"></script>
|
<script type="text/javascript" src="js/controls.js"></script>
|
||||||
<script type="text/javascript" src="js/library.js"></script>
|
<script type="text/javascript" src="js/library.js"></script>
|
||||||
<script type="text/javascript" src="js/functionsvars.js"></script>
|
<script type="text/javascript" src="js/functionsvars.js"></script>
|
||||||
|
|||||||
@ -288,13 +288,13 @@ function setPlayState (nwplay) {
|
|||||||
$('#btplay >i').removeClass('fa-play').addClass('fa-pause')
|
$('#btplay >i').removeClass('fa-play').addClass('fa-pause')
|
||||||
$('#btplay').attr('title', 'Pause')
|
$('#btplay').attr('title', 'Pause')
|
||||||
mopidy.playback.getTimePosition().then(processCurrentposition, console.error)
|
mopidy.playback.getTimePosition().then(processCurrentposition, console.error)
|
||||||
startProgressTimer()
|
syncedProgressTimer.start()
|
||||||
} else {
|
} else {
|
||||||
$('#btplayNowPlaying >i').removeClass('fa-pause').addClass('fa-play')
|
$('#btplayNowPlaying >i').removeClass('fa-pause').addClass('fa-play')
|
||||||
$('#btplayNowPlaying').attr('title', 'Play')
|
$('#btplayNowPlaying').attr('title', 'Play')
|
||||||
$('#btplay >i').removeClass('fa-pause').addClass('fa-play')
|
$('#btplay >i').removeClass('fa-pause').addClass('fa-play')
|
||||||
$('#btplay').attr('title', 'Play')
|
$('#btplay').attr('title', 'Play')
|
||||||
progressTimer.stop()
|
syncedProgressTimer.stop()
|
||||||
}
|
}
|
||||||
play = nwplay
|
play = nwplay
|
||||||
}
|
}
|
||||||
@ -396,7 +396,7 @@ function doSeekPos (value) {
|
|||||||
|
|
||||||
function setPosition (pos) {
|
function setPosition (pos) {
|
||||||
if (!positionChanging && $('#trackslider').val() !== pos) {
|
if (!positionChanging && $('#trackslider').val() !== pos) {
|
||||||
setProgressTimer(pos)
|
syncedProgressTimer.set(pos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
var mopidy
|
var mopidy
|
||||||
|
var syncedProgressTimer
|
||||||
|
|
||||||
// values for controls
|
// values for controls
|
||||||
var play = false
|
var play = false
|
||||||
@ -268,7 +269,7 @@ function renderSongLiDivider (track, nextTrack, currentIndex, target) {
|
|||||||
renderSongLiTrackArtists(track) + '</p></a></li>'
|
renderSongLiTrackArtists(track) + '</p></a></li>'
|
||||||
)
|
)
|
||||||
// Retrieve album covers
|
// 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) {
|
} else if (currentIndex > 0) {
|
||||||
// Small divider
|
// Small divider
|
||||||
$(target).before('<li class="smalldivider"> </li>')
|
$(target).before('<li class="smalldivider"> </li>')
|
||||||
|
|||||||
@ -128,7 +128,7 @@ function setSongInfo (data) {
|
|||||||
}
|
}
|
||||||
if (data.track.album && data.track.album.name) {
|
if (data.track.album && data.track.album.name) {
|
||||||
$('#modalalbum').html('<a href="#" onclick="return library.showAlbum(\'' + data.track.album.uri + '\');">' + data.track.album.name + '</a>')
|
$('#modalalbum').html('<a href="#" onclick="return library.showAlbum(\'' + data.track.album.uri + '\');">' + data.track.album.name + '</a>')
|
||||||
coverArt.getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge')
|
coverArt.getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge', mopidy)
|
||||||
} else {
|
} else {
|
||||||
$('#modalalbum').html('')
|
$('#modalalbum').html('')
|
||||||
$('#infocover').attr('src', 'images/default_cover.png')
|
$('#infocover').attr('src', 'images/default_cover.png')
|
||||||
@ -139,10 +139,9 @@ function setSongInfo (data) {
|
|||||||
|
|
||||||
$('#trackslider').attr('min', 0)
|
$('#trackslider').attr('min', 0)
|
||||||
$('#trackslider').attr('max', songlength)
|
$('#trackslider').attr('max', songlength)
|
||||||
resetProgressTimer()
|
syncedProgressTimer.reset().set(0, songlength)
|
||||||
progressTimer.set(0, songlength)
|
|
||||||
if (play) {
|
if (play) {
|
||||||
startProgressTimer()
|
syncedProgressTimer.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
resizeMb()
|
resizeMb()
|
||||||
@ -239,7 +238,7 @@ function initSocketevents () {
|
|||||||
showFavourites()
|
showFavourites()
|
||||||
})
|
})
|
||||||
library.getBrowseDir()
|
library.getBrowseDir()
|
||||||
library.getSearchSchemes()
|
library.getSearchSchemes(searchBlacklist, mopidy)
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
$(window).hashchange()
|
$(window).hashchange()
|
||||||
})
|
})
|
||||||
@ -302,7 +301,7 @@ function initSocketevents () {
|
|||||||
mopidy.on('event:seeked', function (data) {
|
mopidy.on('event:seeked', function (data) {
|
||||||
setPosition(parseInt(data.time_position))
|
setPosition(parseInt(data.time_position))
|
||||||
if (play) {
|
if (play) {
|
||||||
startProgressTimer()
|
syncedProgressTimer.start()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -496,9 +495,7 @@ $(document).ready(function (event) {
|
|||||||
// initialize events
|
// initialize events
|
||||||
initSocketevents()
|
initSocketevents()
|
||||||
|
|
||||||
progressTimer = new ProgressTimer({
|
syncedProgressTimer = new SyncedProgressTimer(8, mopidy)
|
||||||
callback: timerCallback
|
|
||||||
})
|
|
||||||
|
|
||||||
resetSong()
|
resetSong()
|
||||||
|
|
||||||
@ -593,13 +590,13 @@ $(document).ready(function (event) {
|
|||||||
// swipe songinfo and panel
|
// swipe songinfo and panel
|
||||||
$('#normalFooter, #nowPlayingFooter').on('swiperight', doPrevious)
|
$('#normalFooter, #nowPlayingFooter').on('swiperight', doPrevious)
|
||||||
$('#normalFooter, #nowPlayingFooter').on('swipeleft', doNext)
|
$('#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')) {
|
if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) {
|
||||||
$('#panel').panel('open')
|
$('#panel').panel('open')
|
||||||
event.stopImmediatePropagation()
|
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')) {
|
if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) {
|
||||||
$('#panel').panel('close')
|
$('#panel').panel('close')
|
||||||
event.stopImmediatePropagation()
|
event.stopImmediatePropagation()
|
||||||
@ -607,12 +604,13 @@ $(document).ready(function (event) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
$('#trackslider').on('slidestart', function () {
|
$('#trackslider').on('slidestart', function () {
|
||||||
progressTimer.stop()
|
syncedProgressTimer.stop()
|
||||||
$('#trackslider').on('change', function () { updatePosition($(this).val()) })
|
$('#trackslider').on('change', function () { syncedProgressTimer.updatePosition($(this).val()) })
|
||||||
})
|
})
|
||||||
|
|
||||||
$('#trackslider').on('slidestop', function () {
|
$('#trackslider').on('slidestop', function () {
|
||||||
$('#trackslider').off('change')
|
$('#trackslider').off('change')
|
||||||
|
syncedProgressTimer.updatePosition($(this).val())
|
||||||
doSeekPos($(this).val())
|
doSeekPos($(this).val())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,110 +1,108 @@
|
|||||||
/**
|
(function (root, factory) {
|
||||||
* @author Wouter van Wijk
|
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'
|
var API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733'
|
||||||
API_SECRET = '2c631802c2285d5d5d1502462fe42a2b'
|
var API_SECRET = '2c631802c2285d5d5d1502462fe42a2b'
|
||||||
|
|
||||||
var coverArt = {
|
var coverArt = {
|
||||||
fmcache: new LastFMCache(),
|
lastfm: new LastFM({
|
||||||
lastfm: new LastFM({
|
apiKey: API_KEY,
|
||||||
apiKey: API_KEY,
|
apiSecret: API_SECRET,
|
||||||
apiSecret: API_SECRET,
|
cache: new LastFMCache()
|
||||||
cache: this.fmcache
|
}),
|
||||||
}),
|
|
||||||
|
|
||||||
getCover: function (uri, images, size) {
|
getCover: function (uri, images, size, mopidy) {
|
||||||
var defUrl = 'images/default_cover.png'
|
var defUrl = 'images/default_cover.png'
|
||||||
$(images).attr('src', defUrl)
|
$(images).attr('src', defUrl)
|
||||||
if (!uri) {
|
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 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
mopidy.library.getImages({'uris': [uri]}).then(function (imageResults) {
|
||||||
},
|
var uri = Object.keys(imageResults)[0]
|
||||||
|
if (imageResults[uri].length > 0) {
|
||||||
getCoverFromLastFm: function (track, images, size) {
|
$(images).attr('src', imageResults[uri][0].uri)
|
||||||
var defUrl = 'images/default_cover.png'
|
} else {
|
||||||
$(images).attr('src', defUrl)
|
// Also check deprecated 'album.images' in case backend does not
|
||||||
if (!track || !(track.album || track.artists)) {
|
// implement mopidy.library.getImages yet...
|
||||||
return
|
coverArt.getCoverFromAlbum(uri, images, size, mopidy)
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}, error: function (code, message) {
|
},
|
||||||
console.error('Error retrieving album info from last.fm', code, message)
|
|
||||||
}})
|
|
||||||
},
|
|
||||||
|
|
||||||
getArtistImage: function (artist, images, size) {
|
// Note that this approach has been deprecated in Mopidy
|
||||||
var defUrl = 'images/user_24x32.png'
|
// TODO: Remove when Mopidy no longer supports getting images
|
||||||
$(images).attr('src', defUrl)
|
// with 'album.images'.
|
||||||
if (!artist || artist.length === 0) {
|
getCoverFromAlbum: function (uri, images, size, mopidy) {
|
||||||
return
|
var defUrl = 'images/default_cover.png'
|
||||||
}
|
$(images).attr('src', defUrl)
|
||||||
this.lastfm.artist.getInfo({artist: artist}, {success: function (data) {
|
if (!uri) {
|
||||||
for (var i = 0; i < data.artist.image.length; i++) {
|
return
|
||||||
if (data.artist.image[i].size === size) {
|
}
|
||||||
$(images).attr('src', data.artist.image[i]['#text'] || defUrl)
|
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
|
getArtistImage: function (artist, images, size) {
|
||||||
// in favour of bundling everything using 'browserify'.
|
var defUrl = 'images/user_24x32.png'
|
||||||
if (typeof exports !== 'undefined') {
|
$(images).attr('src', defUrl)
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
if (!artist || artist.length === 0) {
|
||||||
module.exports = coverArt
|
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
|
||||||
|
}))
|
||||||
|
|||||||
@ -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'
|
||||||
|
|
||||||
/** *******************************
|
var library = {
|
||||||
* Search
|
|
||||||
*********************************/
|
|
||||||
searchPressed: function (key) {
|
|
||||||
var value = $('#searchinput').val()
|
|
||||||
switchContent('search')
|
|
||||||
|
|
||||||
if (key === 13) {
|
/** *******************************
|
||||||
library.initSearch()
|
* Search
|
||||||
return false
|
*********************************/
|
||||||
}
|
searchPressed: function (key) {
|
||||||
return true
|
var value = $('#searchinput').val()
|
||||||
},
|
switchContent('search')
|
||||||
|
|
||||||
// init search
|
if (key === 13) {
|
||||||
initSearch: function () {
|
library.initSearch()
|
||||||
var value = $('#searchinput').val()
|
return false
|
||||||
var searchService = $('#selectSearchService').val()
|
}
|
||||||
$.cookie('searchScheme', searchService, { expires: 365 })
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
if ((value.length < 100) && (value.length > 0)) {
|
// init search
|
||||||
showLoading(true)
|
initSearch: function () {
|
||||||
// hide ios/android keyboard
|
var value = $('#searchinput').val()
|
||||||
document.activeElement.blur()
|
var searchService = $('#selectSearchService').val()
|
||||||
$('input').blur()
|
$.cookie('searchScheme', searchService, { expires: 365 })
|
||||||
|
|
||||||
delete customTracklists[URI_SCHEME + ':allresultscache']
|
if ((value.length < 100) && (value.length > 0)) {
|
||||||
delete customTracklists[URI_SCHEME + ':artistresultscache']
|
showLoading(true)
|
||||||
delete customTracklists[URI_SCHEME + ':albumresultscache']
|
// hide ios/android keyboard
|
||||||
delete customTracklists[URI_SCHEME + ':trackresultscache']
|
document.activeElement.blur()
|
||||||
$('#searchartists').hide()
|
$('input').blur()
|
||||||
$('#searchalbums').hide()
|
|
||||||
$('#searchtracks').hide()
|
|
||||||
|
|
||||||
if (searchService !== 'all') {
|
delete customTracklists[URI_SCHEME + ':allresultscache']
|
||||||
mopidy.library.search({'query': {any: [value]}, 'uris': [searchService + ':']}).then(library.processSearchResults, console.error)
|
delete customTracklists[URI_SCHEME + ':artistresultscache']
|
||||||
} else {
|
delete customTracklists[URI_SCHEME + ':albumresultscache']
|
||||||
mopidy.getUriSchemes().then(function (schemes) {
|
delete customTracklists[URI_SCHEME + ':trackresultscache']
|
||||||
var query = {}
|
$('#searchartists').hide()
|
||||||
var uris = []
|
$('#searchalbums').hide()
|
||||||
|
$('#searchtracks').hide()
|
||||||
|
|
||||||
var regexp = $.map(schemes, function (scheme) {
|
if (searchService !== 'all') {
|
||||||
return '^' + scheme + ':'
|
mopidy.library.search({'query': {any: [value]}, 'uris': [searchService + ':']}).then(library.processSearchResults, console.error)
|
||||||
}).join('|')
|
} else {
|
||||||
|
mopidy.getUriSchemes().then(function (schemes) {
|
||||||
|
var query = {}
|
||||||
|
var uris = []
|
||||||
|
|
||||||
var match = value.match(regexp)
|
var regexp = $.map(schemes, function (scheme) {
|
||||||
if (match) {
|
return '^' + scheme + ':'
|
||||||
var scheme = match[0]
|
}).join('|')
|
||||||
query = {uri: [value]}
|
|
||||||
uris = [scheme]
|
var match = value.match(regexp)
|
||||||
} else {
|
if (match) {
|
||||||
query = {any: [value]}
|
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(
|
||||||
|
'<li class="song albumli"><a href="#"><h1><i></i>No tracks found...</h1></a></li>'
|
||||||
|
)
|
||||||
|
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
|
||||||
* process results of a search
|
var showMorePattern = '<li onclick="$(this).hide().siblings().show(); return false;"><a>Show {count} more</a></li>'
|
||||||
*********************************************************/
|
|
||||||
processSearchResults: function (resultArr) {
|
|
||||||
$(SEARCH_TRACK_TABLE).empty()
|
|
||||||
$(SEARCH_ARTIST_TABLE).empty()
|
|
||||||
$(SEARCH_ALBUM_TABLE).empty()
|
|
||||||
|
|
||||||
// Merge results from different backends.
|
// Artist results
|
||||||
// TODO should of coures have multiple tables
|
var child = ''
|
||||||
var results = {'tracks': [], 'artists': [], 'albums': []}
|
var pattern = '<li><a href="#" onclick="return library.showArtist(this.id)" id={id}><i class="{class}"></i> <strong>{name}</strong></a></li>'
|
||||||
var i, j
|
var tokens
|
||||||
var emptyResult = true
|
|
||||||
|
|
||||||
for (i = 0; i < resultArr.length; i++) {
|
for (i = 0; i < results.artists.length; i++) {
|
||||||
if (resultArr[i].tracks) {
|
tokens = {
|
||||||
for (j = 0; j < resultArr[i].tracks.length; j++) {
|
'id': results.artists[i].uri,
|
||||||
results.tracks.push(resultArr[i].tracks[j])
|
'name': results.artists[i].name,
|
||||||
emptyResult = false
|
'class': getMediaClass(results.artists[i].uri)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (resultArr[i].artists) {
|
// Add 'Show all' item after a certain number of hits.
|
||||||
for (j = 0; j < resultArr[i].artists.length; j++) {
|
if (i === 4 && results.artists.length > 5) {
|
||||||
results.artists.push(resultArr[i].artists[j])
|
child += theme(showMorePattern, {'count': results.artists.length - i})
|
||||||
emptyResult = false
|
pattern = pattern.replace('<li>', '<li class="overflow">')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
child += theme(pattern, tokens)
|
||||||
}
|
}
|
||||||
if (resultArr[i].albums) {
|
|
||||||
for (j = 0; j < resultArr[i].albums.length; j++) {
|
// Inject list items, refresh listview and hide superfluous items.
|
||||||
results.albums.push(resultArr[i].albums[j])
|
$(SEARCH_ARTIST_TABLE).html(child).listview('refresh').find('.overflow').hide()
|
||||||
emptyResult = false
|
|
||||||
|
// Album results
|
||||||
|
child = ''
|
||||||
|
pattern = '<li><a href="#" onclick="return library.showAlbum(this.id)" id="{albumId}">'
|
||||||
|
pattern += '<h5 data-role="heading"><i class="{class}"></i> {albumName}</h5>'
|
||||||
|
pattern += '<p data-role="desc">{artistName}</p>'
|
||||||
|
pattern += '</a></li>'
|
||||||
|
|
||||||
|
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) {
|
||||||
customTracklists[URI_SCHEME + ':trackresultscache'] = results.tracks
|
tokens.artistName += results.albums[i].artists[j].name + ' '
|
||||||
|
}
|
||||||
if (emptyResult) {
|
|
||||||
$('#searchtracks').show()
|
|
||||||
$(SEARCH_TRACK_TABLE).append(
|
|
||||||
'<li class="song albumli"><a href="#"><h1><i></i>No tracks found...</h1></a></li>'
|
|
||||||
)
|
|
||||||
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 = '<li onclick="$(this).hide().siblings().show(); return false;"><a>Show {count} more</a></li>'
|
|
||||||
|
|
||||||
// Artist results
|
|
||||||
var child = ''
|
|
||||||
var pattern = '<li><a href="#" onclick="return library.showArtist(this.id)" id={id}><i class="{class}"></i> <strong>{name}</strong></a></li>'
|
|
||||||
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('<li>', '<li class="overflow">')
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = '<li><a href="#" onclick="return library.showAlbum(this.id)" id="{albumId}">'
|
|
||||||
pattern += '<h5 data-role="heading"><i class="{class}"></i> {albumName}</h5>'
|
|
||||||
pattern += '<p data-role="desc">{artistName}</p>'
|
|
||||||
pattern += '</a></li>'
|
|
||||||
|
|
||||||
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 (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('<li>', '<li class="overflow">')
|
||||||
|
}
|
||||||
|
|
||||||
|
child += theme(pattern, tokens)
|
||||||
}
|
}
|
||||||
if (tokens.albumYear) {
|
// Inject list items, refresh listview and hide superfluous items.
|
||||||
tokens.artistName += '(' + tokens.albumYear + ')'
|
$(SEARCH_ALBUM_TABLE).html(child).listview('refresh').find('.overflow').hide()
|
||||||
}
|
|
||||||
// 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('<li>', '<li class="overflow">')
|
|
||||||
}
|
|
||||||
|
|
||||||
child += theme(pattern, tokens)
|
// Track results
|
||||||
}
|
resultsToTables(results.tracks, SEARCH_TRACK_TABLE, URI_SCHEME + ':trackresultscache')
|
||||||
// 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')
|
|
||||||
|
|
||||||
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)
|
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')
|
* Playlists & Browse
|
||||||
$('#popupTracks').popup('close')
|
*********************************/
|
||||||
$('#controlsmodal').popup('close')
|
getPlaylists: function () {
|
||||||
$(ARTIST_TABLE).empty()
|
// get playlists without tracks
|
||||||
|
mopidy.playlists.asList().then(processGetPlaylists, console.error)
|
||||||
|
},
|
||||||
|
|
||||||
// TODO cache
|
getBrowseDir: function (rootdir) {
|
||||||
$('#h_artistname').html('')
|
// get directory to browse
|
||||||
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)
|
showLoading(true)
|
||||||
$('#h_albumname').html('')
|
if (!rootdir) {
|
||||||
$('#h_albumartist').html('')
|
browseStack.pop()
|
||||||
mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) {
|
rootdir = browseStack[browseStack.length - 1]
|
||||||
var resultArr = resultDict[uri]
|
} else {
|
||||||
resultArr.uri = uri
|
browseStack.push(rootdir)
|
||||||
processAlbumResults(resultArr)
|
}
|
||||||
|
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)
|
}, 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)
|
|
||||||
}
|
}
|
||||||
}
|
return library
|
||||||
|
}))
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -220,7 +220,7 @@ function processCurrentPlaylist (resultArr) {
|
|||||||
function processArtistResults (resultArr) {
|
function processArtistResults (resultArr) {
|
||||||
if (!resultArr || (resultArr.length === 0)) {
|
if (!resultArr || (resultArr.length === 0)) {
|
||||||
$('#h_artistname').text('Artist not found...')
|
$('#h_artistname').text('Artist not found...')
|
||||||
coverArt.getCover('', '#artistviewimage, #artistpopupimage', 'extralarge')
|
coverArt.getCover('', '#artistviewimage, #artistpopupimage', 'extralarge', mopidy)
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -239,7 +239,7 @@ function processArtistResults (resultArr) {
|
|||||||
function processAlbumResults (resultArr) {
|
function processAlbumResults (resultArr) {
|
||||||
if (!resultArr || (resultArr.length === 0)) {
|
if (!resultArr || (resultArr.length === 0)) {
|
||||||
$('#h_albumname').text('Album not found...')
|
$('#h_albumname').text('Album not found...')
|
||||||
coverArt.getCover('', '#albumviewcover, #coverpopupimage', 'extralarge')
|
coverArt.getCover('', '#albumviewcover, #coverpopupimage', 'extralarge', mopidy)
|
||||||
showLoading(false)
|
showLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
218
mopidy_musicbox_webclient/static/js/synced_timer.js
Normal file
218
mopidy_musicbox_webclient/static/js/synced_timer.js
Normal file
@ -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
|
||||||
|
}))
|
||||||
@ -1,6 +1,6 @@
|
|||||||
CACHE MANIFEST
|
CACHE MANIFEST
|
||||||
|
|
||||||
# 2016-03-28:v1
|
# 2016-04-03:v1
|
||||||
|
|
||||||
NETWORK:
|
NETWORK:
|
||||||
*
|
*
|
||||||
@ -26,7 +26,7 @@ js/gui.js
|
|||||||
js/images.js
|
js/images.js
|
||||||
js/library.js
|
js/library.js
|
||||||
js/process_ws.js
|
js/process_ws.js
|
||||||
js/progress_timer.js
|
js/synced_timer.js
|
||||||
mb.appcache
|
mb.appcache
|
||||||
system.html
|
system.html
|
||||||
vendors/font_awesome/css/font-awesome.css
|
vendors/font_awesome/css/font-awesome.css
|
||||||
|
|||||||
@ -1,128 +1,159 @@
|
|||||||
/*! timer.js v2.0.2
|
/*! timer.js v3.0.0
|
||||||
* https://github.com/adamcik/media-progress-timer
|
* 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 */
|
* Licensed under the Apache License, Version 2.0 */
|
||||||
|
|
||||||
(function() {
|
(function (root, factory) {
|
||||||
|
if (typeof define === 'function' && define.amd) {
|
||||||
'use strict';
|
define([], factory);
|
||||||
|
} else if (typeof module === 'object' && module.exports) {
|
||||||
var now = typeof window.performance !== 'undefined' &&
|
module.exports = factory();
|
||||||
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;
|
|
||||||
} else {
|
} else {
|
||||||
duration = Math.floor(
|
root.ProgressTimer = factory();
|
||||||
Math.max(duration === null ? Infinity : duration || 0, 0));
|
|
||||||
}
|
}
|
||||||
position = Math.floor(Math.min(Math.max(position || 0, 0), duration));
|
}(this, function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
this._state = {
|
// Helper function to provide a reference time in milliseconds.
|
||||||
initialTimestamp: null,
|
var now = typeof window.performance !== 'undefined' &&
|
||||||
initialPosition: position,
|
typeof window.performance.now !== 'undefined' &&
|
||||||
previousPosition: position,
|
window.performance.now.bind(window.performance) || Date.now ||
|
||||||
duration: duration
|
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);
|
// Start the timer if it is not already running.
|
||||||
return this;
|
ProgressTimer.prototype.start = function() {
|
||||||
};
|
if (this._updateId === null) {
|
||||||
|
this._updateId = this._schedule(0);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
} else {
|
return this;
|
||||||
// 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);
|
|
||||||
};
|
|
||||||
|
|
||||||
if(typeof module !== 'undefined') {
|
// Cancel the timer if it us currently tracking progress.
|
||||||
module.exports = ProgressTimer;
|
ProgressTimer.prototype.stop = function() {
|
||||||
} else {
|
if (this._updateId !== null) {
|
||||||
window.ProgressTimer = ProgressTimer;
|
this._cancel(this._updateId);
|
||||||
}
|
|
||||||
|
|
||||||
}());
|
// 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;
|
||||||
|
}));
|
||||||
@ -1,5 +1,4 @@
|
|||||||
var chai = require('chai')
|
var chai = require('chai')
|
||||||
var should = chai.should()
|
|
||||||
var expect = chai.expect
|
var expect = chai.expect
|
||||||
var assert = chai.assert
|
var assert = chai.assert
|
||||||
chai.use(require('chai-string'))
|
chai.use(require('chai-string'))
|
||||||
@ -7,24 +6,20 @@ chai.use(require('chai-jquery'))
|
|||||||
|
|
||||||
var sinon = require('sinon')
|
var sinon = require('sinon')
|
||||||
|
|
||||||
var coverArt = require('../mopidy_musicbox_webclient/static/js/images.js')
|
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 = $('<img id="img_mock">')
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('CoverArt', function () {
|
describe('CoverArt', function () {
|
||||||
|
var mopidy
|
||||||
|
var images
|
||||||
|
beforeEach(function () {
|
||||||
|
mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'}))
|
||||||
|
images = $('<img id="img_mock">')
|
||||||
|
$(images).removeAttr('src')
|
||||||
|
})
|
||||||
describe('#getCover()', function () {
|
describe('#getCover()', function () {
|
||||||
beforeEach(function () {
|
|
||||||
$(images).removeAttr('src')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should use default image if no track URI is provided', function () {
|
it('should use default image if no track URI is provided', function () {
|
||||||
coverArt.getCover('', images, '')
|
coverArt.getCover('', images, '', mopidy)
|
||||||
$(images).prop('src').should.endWith('images/default_cover.png')
|
expect($(images).prop('src')).to.endWith('images/default_cover.png')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should get image from Mopidy, if available', function () {
|
it('should get image from Mopidy, if available', function () {
|
||||||
@ -33,10 +28,10 @@ describe('CoverArt', function () {
|
|||||||
mopidy.library = library
|
mopidy.library = library
|
||||||
|
|
||||||
var getImagesSpy = sinon.spy(mopidy.library, 'getImages')
|
var getImagesSpy = sinon.spy(mopidy.library, 'getImages')
|
||||||
coverArt.getCover('mock:track:uri', images, '')
|
coverArt.getCover('mock:track:uri', images, '', mopidy)
|
||||||
|
|
||||||
assert(getImagesSpy.calledOnce)
|
assert.isTrue(getImagesSpy.calledOnce)
|
||||||
$(images).prop('src').should.endWith('mockImageUri')
|
expect($(images).prop('src')).to.endWith('mockImageUri')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should fall back to retrieving image from deprecated track.album.images', function () {
|
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 getImagesSpy = sinon.spy(mopidy.library, 'getImages')
|
||||||
var getCoverFromAlbumSpy = sinon.spy(coverArt, 'getCoverFromAlbum')
|
var getCoverFromAlbumSpy = sinon.spy(coverArt, 'getCoverFromAlbum')
|
||||||
|
|
||||||
coverArt.getCover('mock:track:uri', images, '')
|
coverArt.getCover('mock:track:uri', images, '', mopidy)
|
||||||
|
|
||||||
assert(getImagesSpy.calledOnce)
|
assert.isTrue(getImagesSpy.calledOnce)
|
||||||
assert(getCoverFromAlbumSpy.calledOnce)
|
assert.isTrue(getCoverFromAlbumSpy.calledOnce)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('#getCoverFromAlbum()', function () {
|
describe('#getCoverFromAlbum()', function () {
|
||||||
beforeEach(function () {
|
|
||||||
$(images).removeAttr('src')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should use default image if no track URI is provided', function () {
|
it('should use default image if no track URI is provided', function () {
|
||||||
coverArt.getCoverFromAlbum('', images, '')
|
coverArt.getCoverFromAlbum('', images, '', mopidy)
|
||||||
$(images).prop('src').should.endWith('images/default_cover.png')
|
expect($(images).prop('src')).to.endWith('images/default_cover.png')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should get image from Mopidy track.album.images, if available', function () {
|
it('should get image from Mopidy track.album.images, if available', function () {
|
||||||
@ -76,10 +67,10 @@ describe('CoverArt', function () {
|
|||||||
mopidy.library = library
|
mopidy.library = library
|
||||||
|
|
||||||
var lookupSpy = sinon.spy(mopidy.library, 'lookup')
|
var lookupSpy = sinon.spy(mopidy.library, 'lookup')
|
||||||
coverArt.getCoverFromAlbum('mock:track:uri', images, '')
|
coverArt.getCoverFromAlbum('mock:track:uri', images, '', mopidy)
|
||||||
|
|
||||||
assert(lookupSpy.calledOnce)
|
assert.isTrue(lookupSpy.calledOnce)
|
||||||
$(images).prop('src').should.endWith('mockAlbumImageUri')
|
expect($(images).prop('src')).to.endWith('mockAlbumImageUri')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should use default image if track.album or track.artist is not available', function () {
|
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
|
mopidy.library = library
|
||||||
|
|
||||||
var lookupSpy = sinon.spy(mopidy.library, 'lookup')
|
var lookupSpy = sinon.spy(mopidy.library, 'lookup')
|
||||||
coverArt.getCoverFromAlbum('mock:track:uri', images, '')
|
coverArt.getCoverFromAlbum('mock:track:uri', images, '', mopidy)
|
||||||
|
|
||||||
assert(lookupSpy.calledOnce)
|
assert.isTrue(lookupSpy.calledOnce)
|
||||||
$(images).prop('src').should.endWith('images/default_cover.png')
|
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 () {
|
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
|
mopidy.library = library
|
||||||
|
|
||||||
var getCoverFromLastFmSpy = sinon.spy(coverArt, 'getCoverFromLastFm')
|
var getCoverFromLastFmStub = sinon.stub(coverArt, 'getCoverFromLastFm')
|
||||||
coverArt.getCoverFromAlbum('mock:track:uri', images, '')
|
coverArt.getCoverFromAlbum('mock:track:uri', images, '', mopidy)
|
||||||
|
|
||||||
assert(getCoverFromLastFmSpy.calledOnce)
|
assert.isTrue(getCoverFromLastFmStub.calledOnce)
|
||||||
|
getCoverFromLastFmStub.restore()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('#getCoverFromLastFm()', function () {
|
describe('#getCoverFromLastFm()', function () {
|
||||||
beforeEach(function () {
|
|
||||||
$(images).removeAttr('src')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should use default image if no track is provided', function () {
|
it('should use default image if no track is provided', function () {
|
||||||
coverArt.getCoverFromLastFm(undefined, images, '')
|
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 () {
|
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, '')
|
coverArt.getCoverFromLastFm(track, images, '')
|
||||||
var args = getInfoStub.args
|
var args = getInfoStub.args
|
||||||
assert(args[0][0].artist === 'artistMock')
|
assert.equal(args[0][0].artist, 'artistMock')
|
||||||
getInfoStub.restore()
|
getInfoStub.restore()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -141,7 +129,7 @@ describe('CoverArt', function () {
|
|||||||
getInfoStub.yieldsTo('success', getInfoResultMock)
|
getInfoStub.yieldsTo('success', getInfoResultMock)
|
||||||
|
|
||||||
coverArt.getCoverFromLastFm(track, images, 'small')
|
coverArt.getCoverFromLastFm(track, images, 'small')
|
||||||
$(images).prop('src').should.endWith('mockAlbumImageUri')
|
expect($(images).prop('src')).to.endWith('mockAlbumImageUri')
|
||||||
getInfoStub.restore()
|
getInfoStub.restore()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -150,23 +138,19 @@ describe('CoverArt', function () {
|
|||||||
var getInfoStub = sinon.stub(coverArt.lastfm.album, 'getInfo')
|
var getInfoStub = sinon.stub(coverArt.lastfm.album, 'getInfo')
|
||||||
getInfoStub.yieldsTo('error', 'code', 'message')
|
getInfoStub.yieldsTo('error', 'code', 'message')
|
||||||
|
|
||||||
var consoleSpy = sinon.spy(console, 'error')
|
var consoleStub = sinon.stub(console, 'error')
|
||||||
coverArt.getCoverFromLastFm(track, images, '')
|
coverArt.getCoverFromLastFm(track, images, '')
|
||||||
|
|
||||||
assert(consoleSpy.calledOnce)
|
assert.isTrue(consoleStub.calledOnce)
|
||||||
getInfoStub.restore()
|
getInfoStub.restore()
|
||||||
consoleSpy.restore()
|
consoleStub.restore()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('#getArtistImage()', function () {
|
describe('#getArtistImage()', function () {
|
||||||
beforeEach(function () {
|
|
||||||
$(images).removeAttr('src')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should use default image if no artist is provided', function () {
|
it('should use default image if no artist is provided', function () {
|
||||||
coverArt.getArtistImage('', images, '')
|
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 () {
|
it('should get artist info from last.fm', function () {
|
||||||
@ -176,7 +160,7 @@ describe('CoverArt', function () {
|
|||||||
getInfoStub.yieldsTo('success', getInfoResultMock)
|
getInfoStub.yieldsTo('success', getInfoResultMock)
|
||||||
|
|
||||||
coverArt.getArtistImage('mockArtist', images, 'small')
|
coverArt.getArtistImage('mockArtist', images, 'small')
|
||||||
$(images).prop('src').should.endWith('mockArtistImageUri')
|
expect($(images).prop('src')).to.endWith('mockArtistImageUri')
|
||||||
getInfoStub.restore()
|
getInfoStub.restore()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -184,12 +168,12 @@ describe('CoverArt', function () {
|
|||||||
var getInfoStub = sinon.stub(coverArt.lastfm.artist, 'getInfo')
|
var getInfoStub = sinon.stub(coverArt.lastfm.artist, 'getInfo')
|
||||||
getInfoStub.yieldsTo('error', 'code', 'message')
|
getInfoStub.yieldsTo('error', 'code', 'message')
|
||||||
|
|
||||||
var consoleSpy = sinon.spy(console, 'error')
|
var consoleStub = sinon.stub(console, 'error')
|
||||||
|
|
||||||
coverArt.getArtistImage('mockArtist', images, 'small')
|
coverArt.getArtistImage('mockArtist', images, 'small')
|
||||||
assert(consoleSpy.calledOnce)
|
assert.isTrue(consoleStub.calledOnce)
|
||||||
getInfoStub.restore()
|
getInfoStub.restore()
|
||||||
consoleSpy.restore()
|
consoleStub.restore()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
56
tests/js/test_library.js
Normal file
56
tests/js/test_library.js
Normal file
@ -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('<select id="selectSearchService"></select>')
|
||||||
|
$('#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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
352
tests/js/test_synced_timer.js
Normal file
352
tests/js/test_synced_timer.js
Normal file
@ -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(
|
||||||
|
'<div id="slidercontainer"><!-- slider for track position -->' +
|
||||||
|
'<span id="songelapsed"></span>' +
|
||||||
|
'<span id="songlength"></span>' +
|
||||||
|
'<label for="trackslider" disabled="disabled">Position</label>' +
|
||||||
|
'<input id="trackslider" name="trackslider"/>' +
|
||||||
|
'</div>'
|
||||||
|
)
|
||||||
|
$('#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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -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('<select id="selectSearchService"></select>')
|
|
||||||
$('#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')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Loading…
Reference in New Issue
Block a user