From 8d9074a73368d9f3873271cf8ef4024f6b9521e9 Mon Sep 17 00:00:00 2001 From: jcass Date: Tue, 1 Mar 2016 07:40:15 +0200 Subject: [PATCH 01/38] Add tox environment configuration for linters. --- .csslintrc | 1 + .eslintrc | 11 +++ .gitignore | 1 + .travis.yml | 3 + README.rst | 29 ++++-- .../static/js/controls.js | 16 ++-- mopidy_musicbox_webclient/static/js/gui.js | 4 +- .../static/js/process_ws.js | 2 +- package.json | 33 +++++++ tidy.js | 88 +++++++++++++++++++ tox.ini | 36 +++++++- 11 files changed, 204 insertions(+), 20 deletions(-) create mode 100644 .csslintrc create mode 100644 .eslintrc create mode 100644 package.json create mode 100644 tidy.js diff --git a/.csslintrc b/.csslintrc new file mode 100644 index 0000000..a271b01 --- /dev/null +++ b/.csslintrc @@ -0,0 +1 @@ +--format=compact diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..dc14e5b --- /dev/null +++ b/.eslintrc @@ -0,0 +1,11 @@ +{ + "extends": "standard", + "env": { + "jquery": true + }, + "rules": { + "indent": [2, 4, {"SwitchCase": 1}], + "no-undef": 0, // TODO: Set this to '2' once Javascript has been modularised. + "no-unused-vars": 0, // TODO: Set this to '2' once Javascript has been modularised. + } +} diff --git a/.gitignore b/.gitignore index 1b83512..b754965 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ docs/_build/ mopidy.log* node_modules/ nosetests.xml +npm-debug.log .project *.pbxproj *.egg-info diff --git a/.travis.yml b/.travis.yml index 637d177..5ce0d3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,9 @@ python: env: - TOX_ENV=py27 - TOX_ENV=flake8 + - TOX_ENV=eslint + - TOX_ENV=csslint + - TOX_ENV=tidy install: - "pip install tox" diff --git a/README.rst b/README.rst index 1dc0007..1a9ca35 100644 --- a/README.rst +++ b/README.rst @@ -10,14 +10,26 @@ Mopidy-MusicBox-Webclient :target: https://pypi.python.org/pypi/Mopidy-MusicBox-Webclient/ :alt: Number of PyPI downloads -With Mopidy MusicBox Webclient, you can play your music on your computer (`Rapsberry Pi `_) +.. image:: https://img.shields.io/travis/pimusicbox/mopidy-musicbox-webclient/develop.svg?style=flat + :target: https://travis-ci.org/rectalogic/mopidy-pandora + :alt: Travis CI build status + +.. image:: https://img.shields.io/coveralls/pimusicbox/mopidy-musicbox-webclient/develop.svg?style=flat + :target: https://coveralls.io/r/rectalogic/mopidy-pandora?branch=develop + :alt: Test coverage + +.. image:: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat + :target: http://standardjs.com/ + :alt: JavaScript Standard Style + +With Mopidy MusicBox Webclient (MMW), you can play your music on your computer (`Rapsberry Pi `_) and remotely control it using your computer, tablet or phone. -This is a responsive webclient especially written for Mopidy, a music server. Responsive, so it works on desktop and -mobile browsers. You can browse, search and play albums, artists, playlists, and it has cover art from Last.fm. +This is a responsive webclient especially written for `Mopidy `_: a music server that can play +music from many different sources including Spotify, Google Music, SoundCloud, etc. or from your hard drive. The +webclient is responsive, so it works on desktop and mobile browsers. You can browse, search and play albums, artists, +playlists, and it has cover art from Last.fm. -`Mopidy `_ is a music server which can play music from Spotify, Google Music, SoundCloud, etc. -or from your hard drive. If you want to run Mopidy with this webclient on a Raspberry Pi, do yourself a favor and use my custom built SD-image: `Pi MusicBox `_. @@ -30,7 +42,7 @@ Installation Install by running:: - pip install Mopidy-MusicBox-Webclient + pip install mopidy-musicbox-webclient Alternatively, clone the repository and run ``sudo python setup.py install`` from within the project directory. e.g. :: @@ -58,6 +70,11 @@ Project resources Changelog ========= +v2.3.0 (UNRELEASED) +------------------- + +- Enhance build workflow to include style checks and syntax validation for HTML, CSS, and Javascript. + v2.2.0 (2016-03-01) ------------------- diff --git a/mopidy_musicbox_webclient/static/js/controls.js b/mopidy_musicbox_webclient/static/js/controls.js index 2638886..a7821bb 100644 --- a/mopidy_musicbox_webclient/static/js/controls.js +++ b/mopidy_musicbox_webclient/static/js/controls.js @@ -35,7 +35,7 @@ function playBrowsedTracks(action, trackIndex) { mopidy.playback.play({'tl_track': tlTracks[playIndex]}); } }; - + switch (action) { case PLAY_NOW: case PLAY_NEXT: @@ -65,7 +65,7 @@ function playTrack(action) { if (action == PLAY_NOW && divid == 'search') { action = PLAY_NOW_SEARCH; } - + $('#popupTracks').popup('close'); $('#controlspopup').popup('close'); toast('Loading...'); @@ -142,7 +142,7 @@ function playTrackByUri(track_uri, playlist_uri) { toast('Loading...'); mopidy.tracklist.add({'uris': [playlist_uri]}).then(function(tlTracks) { - // Can fail for all sorts of reasons. If so, just add individually. + // Can fail for all sorts of reasons. If so, just add individually. if (tlTracks.length === 0) { var trackUris = getTracksFromUri(playlist_uri, false); mopidy.tracklist.add({'uris': trackUris}).then(findAndPlayTrack); @@ -474,7 +474,7 @@ function playStreamUri(uri) { } function getCurrentlyPlaying() { - $('#streamuriinput').val(songdata.track.uri); + $('#streamuriinput').val(songdata.track.uri); var name = songdata.track.name; if (songdata.track.artists) { var artistStr = artistsToString(songdata.track.artists); @@ -482,7 +482,7 @@ function getCurrentlyPlaying() { name = artistStr + ' - ' + name; } } - $('#streamnameinput').val(name); + $('#streamnameinput').val(name); return true; } @@ -526,7 +526,7 @@ function getPlaylistFull(uri) { } function getFavourites() { - return getPlaylistByName(STREAMS_PLAYLIST_NAME, + return getPlaylistByName(STREAMS_PLAYLIST_NAME, STREAMS_PLAYLIST_SCHEME, true).then(function(playlist) { if (playlist) { @@ -593,7 +593,7 @@ function showFavourites() { return; } var tmp = ''; - + $.cookie.json = true; if ($.cookie('streamUris')) { tmp = ''; @@ -609,7 +609,7 @@ function showFavourites() { } } $('#streamuristable').html(tmp); - }); + }); } // TODO: Remove this upgrade path in next major release. diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index 827dc13..d1be849 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -235,7 +235,7 @@ function initSocketevents() { getPlaylists(); getUriSchemes().then(function() { showFavourites(); - }); + }); getBrowseDir(); getSearchSchemes(); showLoading(false); @@ -580,7 +580,7 @@ $(document).ready(function(event) { return true; } }); - + if ($(window).width() < 980) { $("#panel").panel("close"); diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js index d8cfeeb..605a398 100644 --- a/mopidy_musicbox_webclient/static/js/process_ws.js +++ b/mopidy_musicbox_webclient/static/js/process_ws.js @@ -240,7 +240,7 @@ function processAlbumResults(resultArr) { return; } customTracklists[resultArr.uri] = resultArr; - + albumTracksToTable(resultArr, ALBUM_TABLE, resultArr.uri); var albumname = getAlbum(resultArr); var artistname = getArtist(resultArr); diff --git a/package.json b/package.json new file mode 100644 index 0000000..ee6c8e3 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "Mopidy-MusicBox-Webclient", + "version": "2.1.1", + "description": "Mopidy MusicBox web extension", + "main": "gui.js", + "directories": { + "test": "tests" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "eslint": "eslint mopidy_musicbox_webclient/static/js/**.js", + "csslint": "csslint mopidy_musicbox_webclient/static/css/**.css", + "tidy": "node tidy.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/pimusicbox/mopidy-musicbox-webclient.git" + }, + "author": "Wouter van Wijk", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/pimusicbox/mopidy-musicbox-webclient/issues" + }, + "devDependencies": { + "eslint": "latest", + "eslint-config-standard": "latest", + "eslint-plugin-standard": "latest", + "eslint-plugin-promise": "latest", + "csslint": "latest", + "tidy-html5": "latest" + }, + "homepage": "https://github.com/pimusicbox/mopidy-musicbox-webclient#readme" +} diff --git a/tidy.js b/tidy.js new file mode 100644 index 0000000..bf857e0 --- /dev/null +++ b/tidy.js @@ -0,0 +1,88 @@ +var tidy = require('tidy-html5').tidy_html5 + +var fs = require('fs') + +// Traverse directory structure looking for 'html' or 'htm' files. +var getAllHtmlFilesFromFolder = function (dir) { + var filesystem = require('fs') + var results = [] + filesystem.readdirSync(dir).forEach(function (file) { + file = dir + '/' + file + var stat = filesystem.statSync(file) + + if (stat && stat.isDirectory()) { + results = results.concat(getAllHtmlFilesFromFolder(file)) + } else { + var extension = file.substr(file.lastIndexOf('.') + 1).toUpperCase() + if (extension === 'HTM' || extension === 'HTML') { + results.push(file) + } + } + }) + return results +} + +// Read file contents. +function readFiles (dirname, onFileContent) { + var filenames = getAllHtmlFilesFromFolder(dirname) + filenames.forEach(function (filename) { + fs.readFile(filename, 'utf-8', function (err, content) { + if (err) { + throw (err) + } + onFileContent(filename, content) + }) + }) +} + +var util = require('util') + +// Trap stderr output so that we can detect parsing errors. +function hook_stderr (callback) { + var old_write = process.stderr.write + + process.stderr.write = (function (write) { + return function (string, encoding, fd) { + write.apply(process.stdout, arguments) + callback(string, encoding, fd) + } + })(process.stderr.write) + + return function () { + process.stderr.write = old_write + } +} + +var unhook = hook_stderr(function (string, encoding, fd) { + if (string.indexOf('Error:') > 0) { + errors.push(string) + } +}) + +var errorsOccurred = false +var errors = [] + +// Exit with status 1 so that tox can detect errors. +process.on('exit', function () { + if (errorsOccurred === true) { + process.exit(1) + } +}) + +// Start linter +function processFiles (callback) { + console.log('Starting HTML linter...') + readFiles('mopidy_musicbox_webclient/static', function (filename, content) { + console.log('\n' + filename) + var result = tidy(content, {'quiet': true}) + if (errors.length > 0) { + console.error('\nHTML errors detected:\n' + errors.join('')) + errors = [] + errorsOccurred = true + } + }) +} + +processFiles(function () { + unhook() +}) diff --git a/tox.ini b/tox.ini index ead2ff1..96a5099 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, flake8 +envlist = py27, flake8, eslint, csslint, tidy [testenv] deps = @@ -13,11 +13,41 @@ commands = --basetemp={envtmpdir} \ --junit-xml=xunit-{envname}.xml \ --cov=mopidy_musicbox_webclient --cov-report=term-missing \ - {posargs} + {posargs:tests/} [testenv:flake8] deps = flake8 flake8-import-order skip_install = true -commands = flake8 +commands = flake8 {posargs:mopidy_musicbox_webclient} + +[testenv:eslint] +whitelist_externals = + /bin/bash +deps = + nodeenv +skip_install = true +commands = + - nodeenv --prebuilt {toxworkdir}/node_env + bash -c '. {toxworkdir}/node_env/bin/activate; npm install; npm run eslint' + +[testenv:csslint] +whitelist_externals = + /bin/bash +deps = + nodeenv +skip_install = true +commands = + - nodeenv --prebuilt {toxworkdir}/node_env + bash -c '. {toxworkdir}/node_env/bin/activate; npm install; npm run csslint' + +[testenv:tidy] +whitelist_externals = + /bin/bash +deps = + nodeenv +skip_install = true +commands = + - nodeenv --prebuilt {toxworkdir}/node_env + bash -c '. {toxworkdir}/node_env/bin/activate; npm install; npm run tidy' From 785d923fcf1350c375949939f02e8a8051409b54 Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 5 Mar 2016 15:06:13 +0200 Subject: [PATCH 02/38] docs:Fix travis and coveralls link targets to point to correct repository. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 1a9ca35..2635fb2 100644 --- a/README.rst +++ b/README.rst @@ -11,11 +11,11 @@ Mopidy-MusicBox-Webclient :alt: Number of PyPI downloads .. image:: https://img.shields.io/travis/pimusicbox/mopidy-musicbox-webclient/develop.svg?style=flat - :target: https://travis-ci.org/rectalogic/mopidy-pandora + :target: https://travis-ci.org/pimusicbox/mopidy-musicbox-webclient :alt: Travis CI build status .. image:: https://img.shields.io/coveralls/pimusicbox/mopidy-musicbox-webclient/develop.svg?style=flat - :target: https://coveralls.io/r/rectalogic/mopidy-pandora?branch=develop + :target: https://coveralls.io/r/pimusicbox/mopidy-musicbox-webclient?branch=develop :alt: Test coverage .. image:: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat From 168f2c549b30b56edd3d599f7c295abaa0f03bd0 Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 5 Mar 2016 15:11:48 +0200 Subject: [PATCH 03/38] fox:Automatic style changes applied by running eslint --fix --- .../static/js/controls.js | 692 ++++++++-------- .../static/js/functionsvars.js | 534 ++++++------ mopidy_musicbox_webclient/static/js/gui.js | 774 +++++++++--------- mopidy_musicbox_webclient/static/js/images.js | 90 +- .../static/js/library.js | 360 ++++---- .../static/js/process_ws.js | 266 +++--- .../static/js/progress_timer.js | 166 ++-- 7 files changed, 1441 insertions(+), 1441 deletions(-) diff --git a/mopidy_musicbox_webclient/static/js/controls.js b/mopidy_musicbox_webclient/static/js/controls.js index a7821bb..daec3cc 100644 --- a/mopidy_musicbox_webclient/static/js/controls.js +++ b/mopidy_musicbox_webclient/static/js/controls.js @@ -1,665 +1,665 @@ -/*********************************** +/** ********************************* * play tracks from a browse list * ***********************************/ -function playBrowsedTracks(action, trackIndex) { - $('#popupBrowse').popup('close'); - toast('Loading...'); +function playBrowsedTracks (action, trackIndex) { + $('#popupBrowse').popup('close') + toast('Loading...') if (typeof trackIndex === 'undefined') { - trackIndex = $('#popupBrowse').data("tlid"); + trackIndex = $('#popupBrowse').data('tlid') } if (action == PLAY_ALL) { - mopidy.tracklist.clear(); + mopidy.tracklist.clear() // Default for radio streams is to just add the selected URI. if (isStreamUri(browseTracks[trackIndex].uri)) { - action = PLAY_NOW; + action = PLAY_NOW } } - var trackUris = []; + var trackUris = [] switch (action) { case PLAY_NOW: case PLAY_NEXT: case ADD_THIS_BOTTOM: - trackUris.push(browseTracks[trackIndex].uri); - break; + trackUris.push(browseTracks[trackIndex].uri) + break case PLAY_ALL: case ADD_ALL_BOTTOM: - trackUris = getUris(browseTracks); - break; + trackUris = getUris(browseTracks) + break default: - break; + break } - var maybePlay = function(tlTracks) { + var maybePlay = function (tlTracks) { if (action === PLAY_NOW || action === PLAY_ALL) { - var playIndex = (action === PLAY_ALL) ? trackIndex : 0; - mopidy.playback.play({'tl_track': tlTracks[playIndex]}); + var playIndex = (action === PLAY_ALL) ? trackIndex : 0 + mopidy.playback.play({'tl_track': tlTracks[playIndex]}) } - }; + } switch (action) { case PLAY_NOW: case PLAY_NEXT: mopidy.tracklist.index().then(function (currentIndex) { - mopidy.tracklist.add({'at_position': currentIndex + 1, 'uris': trackUris}).then(maybePlay); - }); - break; + mopidy.tracklist.add({'at_position': currentIndex + 1, 'uris': trackUris}).then(maybePlay) + }) + break case ADD_THIS_BOTTOM: case ADD_ALL_BOTTOM: case PLAY_ALL: - mopidy.tracklist.add({'uris': trackUris}).then(maybePlay); - break; + mopidy.tracklist.add({'uris': trackUris}).then(maybePlay) + break default: - break; + break } - return false; + return false } -/********************************* +/** ******************************* * play an uri from a tracklist * *********************************/ -function playTrack(action) { - var hash = document.location.hash.split('?'); - var divid = hash[0].substr(1); +function playTrack (action) { + var hash = document.location.hash.split('?') + var divid = hash[0].substr(1) // Search page default click behaviour adds and plays selected track only. if (action == PLAY_NOW && divid == 'search') { - action = PLAY_NOW_SEARCH; + action = PLAY_NOW_SEARCH } - $('#popupTracks').popup('close'); - $('#controlspopup').popup('close'); - toast('Loading...'); + $('#popupTracks').popup('close') + $('#controlspopup').popup('close') + toast('Loading...') - playlisturi = $('#popupTracks').data("list"); - uri = $('#popupTracks').data("track"); + playlisturi = $('#popupTracks').data('list') + uri = $('#popupTracks').data('track') - var trackUris = getTracksFromUri(playlisturi); - //find track that was selected + var trackUris = getTracksFromUri(playlisturi) + // find track that was selected for (var selected = 0; selected < trackUris.length; selected++) { if (trackUris[selected] == uri) { - break; + break } } switch (action) { case ADD_THIS_BOTTOM: case PLAY_NEXT: case PLAY_NOW_SEARCH: - trackUris = [trackUris[selected]]; - selected = 0; + trackUris = [trackUris[selected]] + selected = 0 } switch (action) { case PLAY_NOW: case PLAY_NOW_SEARCH: mopidy.tracklist.clear().then( mopidy.tracklist.add({'uris': trackUris}).then( - function(tlTracks) { - mopidy.playback.play({'tl_track': tlTracks[selected]}); + function (tlTracks) { + mopidy.playback.play({'tl_track': tlTracks[selected]}) } ) - ); - break; + ) + break case PLAY_NEXT: - mopidy.tracklist.index().then(function(currentIndex) { - mopidy.tracklist.add({'at_position': currentIndex + 1, 'uris': trackUris}); - }); - break; + mopidy.tracklist.index().then(function (currentIndex) { + mopidy.tracklist.add({'at_position': currentIndex + 1, 'uris': trackUris}) + }) + break case ADD_THIS_BOTTOM: case ADD_ALL_BOTTOM: - mopidy.tracklist.add({'uris': trackUris}); - break; + mopidy.tracklist.add({'uris': trackUris}) + break } - return false; + return false } -/*** +/** * * Plays a Track given by an URI from the given playlist URI. * @param track_uri, playlist_uri * @returns {boolean} */ -function playTrackByUri(track_uri, playlist_uri) { - function findAndPlayTrack(tlTracks) { +function playTrackByUri (track_uri, playlist_uri) { + function findAndPlayTrack (tlTracks) { if (tlTracks.length > 0) { // Find track that was selected for (var selected = 0; selected < tlTracks.length; selected++) { if (tlTracks[selected].track.uri == track_uri) { - mopidy.playback.play({'tl_track': tlTracks[selected]}); - return; + mopidy.playback.play({'tl_track': tlTracks[selected]}) + return } } } - console.error('Failed to find and play selected track ', track_uri); - return; + console.error('Failed to find and play selected track ', track_uri) + return } // Stop directly, for user feedback - mopidy.tracklist.clear(); + mopidy.tracklist.clear() - //this is deprecated, remove when popuptracks is removed completly - $('#popupTracks').popup('close'); - $('#controlspopup').popup('close'); - //end of deprecated + // this is deprecated, remove when popuptracks is removed completly + $('#popupTracks').popup('close') + $('#controlspopup').popup('close') + // end of deprecated - toast('Loading...'); + toast('Loading...') - mopidy.tracklist.add({'uris': [playlist_uri]}).then(function(tlTracks) { + mopidy.tracklist.add({'uris': [playlist_uri]}).then(function (tlTracks) { // Can fail for all sorts of reasons. If so, just add individually. if (tlTracks.length === 0) { - var trackUris = getTracksFromUri(playlist_uri, false); - mopidy.tracklist.add({'uris': trackUris}).then(findAndPlayTrack); + var trackUris = getTracksFromUri(playlist_uri, false) + mopidy.tracklist.add({'uris': trackUris}).then(findAndPlayTrack) } else { - findAndPlayTrack(tlTracks); + findAndPlayTrack(tlTracks) } - }); - return false; + }) + return false } -/******************************************************** +/** ****************************************************** * play an uri from the queue *********************************************************/ -/*** +/** * * Plays a Track from a Playlist. * @param uri * @param tlid * @returns {boolean} */ -function playTrackQueueByTlid(uri, tlid) { - //stop directly, for user feedback - mopidy.playback.stop(); - $('#popupQueue').popup('close'); - toast('Loading...'); +function playTrackQueueByTlid (uri, tlid) { + // stop directly, for user feedback + mopidy.playback.stop() + $('#popupQueue').popup('close') + toast('Loading...') - tlid = parseInt(tlid); + tlid = parseInt(tlid) mopidy.tracklist.filter({ 'tlid': [tlid] }).then( - function(tlTracks) { + function (tlTracks) { if (tlTracks.length > 0) { - mopidy.playback.play({'tl_track': tlTracks[0]}); - return; + mopidy.playback.play({'tl_track': tlTracks[0]}) + return } - console.log('Failed to play selected track ', tlid); + console.log('Failed to play selected track ', tlid) } - ); - return false; + ) + return false } -/*** +/** * * @deprecated * @returns {boolean} */ -function playTrackQueue() { - uri = $('#popupQueue').data("track"); - tlid = $('#popupQueue').data("tlid"); - return playTrackQueueByTlid(uri, tlid); +function playTrackQueue () { + uri = $('#popupQueue').data('track') + tlid = $('#popupQueue').data('tlid') + return playTrackQueueByTlid(uri, tlid) } -/*********************************** +/** ********************************* * remove a track from the queue * ***********************************/ -function removeTrack() { - $('#popupQueue').popup('close'); - toast('Deleting...'); +function removeTrack () { + $('#popupQueue').popup('close') + toast('Deleting...') - tlid = parseInt($('#popupQueue').data("tlid")); - console.log(tlid); - mopidy.tracklist.remove({'tlid': [tlid]}); + tlid = parseInt($('#popupQueue').data('tlid')) + console.log(tlid) + mopidy.tracklist.remove({'tlid': [tlid]}) } -function clearQueue() { +function clearQueue () { mopidy.tracklist.clear().then( resetSong() - ); - return false; + ) + return false } -function savePressed(key) { +function savePressed (key) { if (key == 13) { - saveQueue(); - return false; + saveQueue() + return false } - return true; + return true } -function showSavePopup(){ - mopidy.tracklist.getTracks().then(function(tracks) { +function showSavePopup () { + mopidy.tracklist.getTracks().then(function (tracks) { if (tracks.length > 0) { - $('#saveinput').val(''); - $('#popupSave').popup('open'); + $('#saveinput').val('') + $('#popupSave').popup('open') } - }); + }) } -function saveQueue() { - mopidy.tracklist.getTracks().then(function(tracks) { - var playlistName = $('#saveinput').val().trim(); - if (playlistName !== null && playlistName !== "") { - getPlaylistByName(playlistName, 'm3u', false).then(function(exists) { +function saveQueue () { + mopidy.tracklist.getTracks().then(function (tracks) { + var playlistName = $('#saveinput').val().trim() + if (playlistName !== null && playlistName !== '') { + getPlaylistByName(playlistName, 'm3u', false).then(function (exists) { if (exists) { - $('#popupSave').popup('close'); - $('#popupOverwrite').popup('open'); - $('#overwriteConfirmBtn').click(function() { - initSave(playlistName, tracks); - }); + $('#popupSave').popup('close') + $('#popupOverwrite').popup('open') + $('#overwriteConfirmBtn').click(function () { + initSave(playlistName, tracks) + }) } else { - initSave(playlistName, tracks); + initSave(playlistName, tracks) } - }); + }) } - }); - return false; + }) + return false } -function initSave(playlistName, tracks) { - $('#popupOverwrite').popup('close'); - $('#popupSave').popup('close'); - $('#saveinput').val(''); - toast('Saving...'); - mopidy.playlists.create({'name': playlistName, 'uri_scheme': "m3u"}).then(function(playlist) { - playlist.tracks = tracks; - mopidy.playlists.save({'playlist': playlist}).then(); - }); +function initSave (playlistName, tracks) { + $('#popupOverwrite').popup('close') + $('#popupSave').popup('close') + $('#saveinput').val('') + toast('Saving...') + mopidy.playlists.create({'name': playlistName, 'uri_scheme': 'm3u'}).then(function (playlist) { + playlist.tracks = tracks + mopidy.playlists.save({'playlist': playlist}).then() + }) } -function refreshPlaylists() { - mopidy.playlists.refresh().then(function() { - playlists = {}; - $('#playlisttracksdiv').hide(); - $('#playlistslistdiv').show(); - }); - return false; +function refreshPlaylists () { + mopidy.playlists.refresh().then(function () { + playlists = {} + $('#playlisttracksdiv').hide() + $('#playlistslistdiv').show() + }) + return false } -/************* +/** *********** * Buttons * *************/ -function doShuffle() { - mopidy.playback.stop(); - mopidy.tracklist.shuffle(); - mopidy.playback.play(); +function doShuffle () { + mopidy.playback.stop() + mopidy.tracklist.shuffle() + mopidy.playback.play() } /* Toggle state of play button */ -function setPlayState(nwplay) { +function setPlayState (nwplay) { if (nwplay) { - $("#btplayNowPlaying >i").removeClass('fa-play').addClass('fa-pause'); - $("#btplayNowPlaying").attr('title', 'Pause'); - $("#btplay >i").removeClass('fa-play').addClass('fa-pause'); - $("#btplay").attr('title', 'Pause'); - mopidy.playback.getTimePosition().then(processCurrentposition, console.error); - startProgressTimer(); + $('#btplayNowPlaying >i').removeClass('fa-play').addClass('fa-pause') + $('#btplayNowPlaying').attr('title', 'Pause') + $('#btplay >i').removeClass('fa-play').addClass('fa-pause') + $('#btplay').attr('title', 'Pause') + mopidy.playback.getTimePosition().then(processCurrentposition, console.error) + startProgressTimer() } else { - $("#btplayNowPlaying >i").removeClass('fa-pause').addClass('fa-play'); - $("#btplayNowPlaying").attr('title', 'Play'); - $("#btplay >i").removeClass('fa-pause').addClass('fa-play'); - $("#btplay").attr('title', 'Play'); - progressTimer.stop(); + $('#btplayNowPlaying >i').removeClass('fa-pause').addClass('fa-play') + $('#btplayNowPlaying').attr('title', 'Play') + $('#btplay >i').removeClass('fa-pause').addClass('fa-play') + $('#btplay').attr('title', 'Play') + progressTimer.stop() } - play = nwplay; + play = nwplay } -//play or pause -function doPlay() { - toast('Please wait...', 250); +// play or pause +function doPlay () { + toast('Please wait...', 250) if (!play) { - mopidy.playback.play(); + mopidy.playback.play() } else { - if(isStreamUri(songdata.track.uri)) { - mopidy.playback.stop(); + if (isStreamUri(songdata.track.uri)) { + mopidy.playback.stop() } else { - mopidy.playback.pause(); + mopidy.playback.pause() } } - setPlayState(!play); + setPlayState(!play) } -function doPrevious() { - toast('Playing previous track...'); - mopidy.playback.previous(); +function doPrevious () { + toast('Playing previous track...') + mopidy.playback.previous() } -function doNext() { - toast('Playing next track...'); - mopidy.playback.next(); +function doNext () { + toast('Playing next track...') + mopidy.playback.next() } -function backbt() { - history.back(); - return false; +function backbt () { + history.back() + return false } -/************* +/** *********** * Options * *************/ -function setTracklistOption(name, new_value) { +function setTracklistOption (name, new_value) { if (!new_value) { - $("#"+name+"bt").attr('style', 'color:#2489ce'); + $("#" + name+'bt').attr('style', 'color:#2489ce') } else { - $("#"+name+"bt").attr('style', 'color:#66DD33'); + $("#" + name+'bt').attr('style', 'color:#66DD33') } - return new_value; + return new_value } -function setRepeat(nwrepeat) { +function setRepeat (nwrepeat) { if (repeat != nwrepeat) { - repeat = setTracklistOption("repeat", nwrepeat); + repeat = setTracklistOption('repeat', nwrepeat) } } -function setRandom(nwrandom) { +function setRandom (nwrandom) { if (random != nwrandom) { - random = setTracklistOption("random", nwrandom); + random = setTracklistOption('random', nwrandom) } } -function setConsume(nwconsume) { +function setConsume (nwconsume) { if (consume != nwconsume) { - consume = setTracklistOption("consume", nwconsume); + consume = setTracklistOption('consume', nwconsume) } } -function setSingle(nwsingle) { +function setSingle (nwsingle) { if (single != nwsingle) { - single = setTracklistOption("single", nwsingle); + single = setTracklistOption('single', nwsingle) } } -function doRandom() { - mopidy.tracklist.setRandom({'value': !random}).then(); +function doRandom () { + mopidy.tracklist.setRandom({'value': !random}).then() } -function doRepeat() { - mopidy.tracklist.setRepeat({'value': !repeat}).then(); +function doRepeat () { + mopidy.tracklist.setRepeat({'value': !repeat}).then() } -function doConsume() { - mopidy.tracklist.setConsume({'value': !consume}).then(); +function doConsume () { + mopidy.tracklist.setConsume({'value': !consume}).then() } -function doSingle() { - mopidy.tracklist.setSingle({'value': !single}).then(); +function doSingle () { + mopidy.tracklist.setSingle({'value': !single}).then() } -/*********************************************** +/** ********************************************* * Track Slider * * Use a timer to prevent looping of commands * ***********************************************/ -function doSeekPos(value) { +function doSeekPos (value) { if (!positionChanging) { - positionChanging = value; - mopidy.playback.seek({'time_position': Math.round(value)}).then( function() { - positionChanging = null; - }); + positionChanging = value + mopidy.playback.seek({'time_position': Math.round(value)}).then(function () { + positionChanging = null + }) } } -function setPosition(pos) { - if (!positionChanging && $("#trackslider").val() != pos) { - setProgressTimer(pos); +function setPosition (pos) { + if (!positionChanging && $('#trackslider').val() != pos) { + setProgressTimer(pos) } } -/*********************************************** +/** ********************************************* * Volume slider * * Use a timer to prevent looping of commands * ***********************************************/ -function setVolume(value) { - if (!volumeChanging && !volumeSliding && $("#volumeslider").val() != value) { - $( "#volumeslider" ).off( "change"); - $( "#volumeslider" ).val(value).slider('refresh'); - $( "#volumeslider" ).on( "change", function() { doVolume( $(this).val() ); } ); +function setVolume (value) { + if (!volumeChanging && !volumeSliding && $('#volumeslider').val() != value) { + $("#volumeslider").off( 'change') + $("#volumeslider").val(value).slider('refresh') + $("#volumeslider").on( 'change', function () { doVolume($(this).val()) }) } } -function doVolume(value) { +function doVolume (value) { if (!volumeChanging) { - volumeChanging = value; - mopidy.playback.setVolume({'volume': parseInt(volumeChanging)}).then( function() { - volumeChanging = null; - }); + volumeChanging = value + mopidy.playback.setVolume({'volume': parseInt(volumeChanging)}).then(function () { + volumeChanging = null + }) } } -function setMute(nwmute) { +function setMute (nwmute) { if (mute != nwmute) { - mute = nwmute; + mute = nwmute if (mute) { - $("#mutebt").attr('class', 'fa fa-volume-off'); + $('#mutebt').attr('class', 'fa fa-volume-off') } else { - $("#mutebt").attr('class', 'fa fa-volume-up'); + $('#mutebt').attr('class', 'fa fa-volume-up') } } } -function doMute() { - mopidy.mixer.setMute({'mute': !mute}); +function doMute () { + mopidy.mixer.setMute({'mute': !mute}) } -/************ +/** ********** * Stream * ************/ -function streamPressed(key) { +function streamPressed (key) { if (key == 13) { - playStreamUri(); - return false; + playStreamUri() + return false } - return true; + return true } -function playStreamUri(uri) { - //value of name is based on the passing of an uri as a parameter or not - var nwuri = uri || $('#streamuriinput').val().trim(); - var service = $('#selectstreamservice').val(); +function playStreamUri (uri) { + // value of name is based on the passing of an uri as a parameter or not + var nwuri = uri || $('#streamuriinput').val().trim() + var service = $('#selectstreamservice').val() if (!uri && service) { - nwuri = service + ':' + nwuri; + nwuri = service + ':' + nwuri } if (isServiceUri(nwuri) || isStreamUri(nwuri) || validUri(nwuri)) { - toast('Playing...'); - //stop directly, for user feedback - mopidy.playback.stop(); - //hide ios/android keyboard - document.activeElement.blur(); - clearQueue(); - $("input").blur(); - mopidy.tracklist.add({'uris': [nwuri]}); - mopidy.playback.play(); + toast('Playing...') + // stop directly, for user feedback + mopidy.playback.stop() + // hide ios/android keyboard + document.activeElement.blur() + clearQueue() + $('input').blur() + mopidy.tracklist.add({'uris': [nwuri]}) + mopidy.playback.play() } else { - toast('No valid url!'); + toast('No valid url!') } - return false; + return false } -function getCurrentlyPlaying() { - $('#streamuriinput').val(songdata.track.uri); - var name = songdata.track.name; +function getCurrentlyPlaying () { + $('#streamuriinput').val(songdata.track.uri) + var name = songdata.track.name if (songdata.track.artists) { - var artistStr = artistsToString(songdata.track.artists); + var artistStr = artistsToString(songdata.track.artists) if (artistStr) { - name = artistStr + ' - ' + name; + name = artistStr + ' - ' + name } } - $('#streamnameinput').val(name); - return true; + $('#streamnameinput').val(name) + return true } -function getUriSchemes() { - uriSchemes = {}; - return mopidy.getUriSchemes().then(function(schemes) { +function getUriSchemes () { + uriSchemes = {} + return mopidy.getUriSchemes().then(function (schemes) { for (var i = 0; i < schemes.length; i++) { - uriSchemes[schemes[i].toLowerCase()] = true; + uriSchemes[schemes[i].toLowerCase()] = true } - }); + }) } -function getPlaylistByName(name, scheme, create) { - var uri_scheme = scheme || ''; - var uri = ''; +function getPlaylistByName (name, scheme, create) { + var uri_scheme = scheme || '' + var uri = '' if (uri_scheme && !uriSchemes[uri_scheme]) { - return Mopidy.when(false); + return Mopidy.when(false) } - return mopidy.playlists.asList().catch(console.error.bind(console)).then(function(plists) { + return mopidy.playlists.asList().catch(console.error.bind(console)).then(function (plists) { for (var i = 0; i < plists.length; i++) { if ((plists[i].name === name) && (uri_scheme === '' || getScheme(plists[i].uri) === uri_scheme)) { - return plists[i]; + return plists[i] } } if (create) { - return mopidy.playlists.create({'name': name, 'uri_scheme': uri_scheme}).done(function(plist) { - console.log("Created playlist '%s'", plist.name); - return plist; - }); + return mopidy.playlists.create({'name': name, 'uri_scheme': uri_scheme}).done(function (plist) { + console.log("Created playlist '%s'", plist.name) + return plist + }) } - console.log("Can't find playist '%s", name); - return Mopidy.when(false); - }); + console.log("Can't find playist '%s", name) + return Mopidy.when(false) + }) } -function getPlaylistFull(uri) { - return mopidy.playlists.lookup({'uri': uri}).then(function(pl) { - playlists[uri] = pl; - return pl; - }); +function getPlaylistFull (uri) { + return mopidy.playlists.lookup({'uri': uri}).then(function (pl) { + playlists[uri] = pl + return pl + }) } -function getFavourites() { +function getFavourites () { return getPlaylistByName(STREAMS_PLAYLIST_NAME, STREAMS_PLAYLIST_SCHEME, - true).then(function(playlist) { - if (playlist) { - return getPlaylistFull(playlist.uri); + true).then(function (playlist) { + if (playlist) { + return getPlaylistFull(playlist.uri) } - return Mopidy.when(false); - }); + return Mopidy.when(false) + }) } -function addToFavourites(newTracks) { - getFavourites().catch(console.error.bind(console)).then(function(favourites) { +function addToFavourites (newTracks) { + getFavourites().catch(console.error.bind(console)).then(function (favourites) { if (favourites) { if (favourites.tracks) { - Array.prototype.push.apply(favourites.tracks, newTracks); + Array.prototype.push.apply(favourites.tracks, newTracks) } else { - favourites.tracks = newTracks; + favourites.tracks = newTracks } - mopidy.playlists.save({'playlist': favourites}).then(function(s) { - showFavourites(); - }); + mopidy.playlists.save({'playlist': favourites}).then(function (s) { + showFavourites() + }) } - }); + }) } -function addFavourite(uri, name) { - var uri = uri || $('#streamuriinput').val().trim(); - var name = name || $('#streamnameinput').val().trim(); - mopidy.library.lookup({'uris': [uri]}).then(function(results) { - var newTracks = results[uri]; +function addFavourite (uri, name) { + var uri = uri || $('#streamuriinput').val().trim() + var name = name || $('#streamnameinput').val().trim() + mopidy.library.lookup({'uris': [uri]}).then(function (results) { + var newTracks = results[uri] if (newTracks.length == 1) { // TODO: Supporting adding an entire playlist? if (name) { - newTracks[0].name = name; // User overrides name. + newTracks[0].name = name // User overrides name. } - addToFavourites(newTracks); + addToFavourites(newTracks) } else { if (newTracks.length === 0) { - console.log('No tracks to add'); + console.log('No tracks to add') } else { - console.log('Too many tracks (%d) to add', tracks.length); + console.log('Too many tracks (%d) to add', tracks.length) } } - }); + }) } -function deleteFavourite(index) { - getFavourites().then(function(favourites) { +function deleteFavourite (index) { + getFavourites().then(function (favourites) { if (favourites && favourites.tracks && index < favourites.tracks.length) { - var name = favourites.tracks[index].name; + var name = favourites.tracks[index].name if (confirm("Are you sure you want to remove '" + name + "'?")) { - favourites.tracks.splice(index, 1); - mopidy.playlists.save({'playlist': favourites}).then(function(s) { - showFavourites(); - }); + favourites.tracks.splice(index, 1) + mopidy.playlists.save({'playlist': favourites}).then(function (s) { + showFavourites() + }) } } - }); + }) } -function showFavourites() { - $('#streamuristable').empty(); - getFavourites().then(function(favourites) { +function showFavourites () { + $('#streamuristable').empty() + getFavourites().then(function (favourites) { if (!favourites) { - return; + return } - var tmp = ''; + var tmp = '' - $.cookie.json = true; + $.cookie.json = true if ($.cookie('streamUris')) { - tmp = ''; + tmp = '' } if (favourites.tracks) { - var child = ''; + var child = '' for (var i = 0; i < favourites.tracks.length; i++) { child = '
  •  ' + '' + - ' '; - child += '

    ' + favourites.tracks[i].name + '

  • '; - tmp += child; + ' ' + child += '

    ' + favourites.tracks[i].name + '

    ' + tmp += child } } - $('#streamuristable').html(tmp); - }); + $('#streamuristable').html(tmp) + }) } // TODO: Remove this upgrade path in next major release. -function upgradeStreamUrisToFavourites() { - toast('Converting streamUris...'); - $.cookie.json = true; - var streamUris = $.cookie('streamUris'); // Read the cookie. +function upgradeStreamUrisToFavourites () { + toast('Converting streamUris...') + $.cookie.json = true + var streamUris = $.cookie('streamUris') // Read the cookie. if (streamUris) { - var uris = []; // Prepare a list of uris to lookup. + var uris = [] // Prepare a list of uris to lookup. for (var key in streamUris) { - var rs = streamUris[key]; + var rs = streamUris[key] if (rs) { - uris.push(rs[1]); + uris.push(rs[1]) } } - mopidy.library.lookup({'uris': uris}).then(function(results) { - var tracks = []; // Prepare a list of tracks to add. + mopidy.library.lookup({'uris': uris}).then(function (results) { + var tracks = [] // Prepare a list of tracks to add. for (var key in streamUris) { - var rs = streamUris[key]; + var rs = streamUris[key] if (rs) { - var track = results[rs[1]][0]; + var track = results[rs[1]][0] if (track) { - track.name = rs[0] || track.name; // Use custom name if provided. - tracks.push(track); + track.name = rs[0] || track.name // Use custom name if provided. + tracks.push(track) } else { - console.log("Skipping unplayable streamUri " + rs[1]); + console.log('Skipping unplayable streamUri ' + rs[1]) } } } - addToFavourites(tracks); - $.cookie('streamUris', null); // Delete the cookie now we're done. - console.log(tracks.length + " streamUris added to favourites"); - }); + addToFavourites(tracks) + $.cookie('streamUris', null) // Delete the cookie now we're done. + console.log(tracks.length + ' streamUris added to favourites') + }) } else { - console.log("No streamUris cookie found"); + console.log('No streamUris cookie found') } } -function haltSystem() { - $.post("/settings/shutdown"); - toast('Stopping system...', 10000); - setTimeout(function() { - window.history.back(); - }, 10000); +function haltSystem () { + $.post('/settings/shutdown') + toast('Stopping system...', 10000) + setTimeout(function () { + window.history.back() + }, 10000) } -function rebootSystem() { - $.post("/settings/reboot"); - toast('Rebooting...', 10000); - setTimeout(function() { - window.history.back(); - }, 10000); +function rebootSystem () { + $.post('/settings/reboot') + toast('Rebooting...', 10000) + setTimeout(function () { + window.history.back() + }, 10000) } diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index d0b99fc..1b8f060 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -4,74 +4,74 @@ * all kinds functions and vars */ -var mopidy; +var mopidy -//values for controls -var play = false; -var random; -var repeat; -var consume; -var single; -var mute; -var volumeChanging; -var volumeSliding = false; +// values for controls +var play = false +var random +var repeat +var consume +var single +var mute +var volumeChanging +var volumeSliding = false -var positionChanging; +var positionChanging -var initgui = true; -var popupData = {}; -var songlength = 0; +var initgui = true +var popupData = {} +var songlength = 0 -var artistshtml = ''; -var artiststext = ''; -var songname = ''; -var songdata = {'track': {}, 'tlid': -1}; +var artistshtml = '' +var artiststext = '' +var songname = '' +var songdata = {'track': {}, 'tlid': -1} -var playlisttracksScroll; -var playlistslistScroll; +var playlisttracksScroll +var playlistslistScroll -var STREAMS_PLAYLIST_NAME = '[Radio Streams]'; -var STREAMS_PLAYLIST_SCHEME = 'm3u'; -var uriSchemes = {}; +var STREAMS_PLAYLIST_NAME = '[Radio Streams]' +var STREAMS_PLAYLIST_SCHEME = 'm3u' +var uriSchemes = {} -//array of cached playlists (not only user-playlists, also search, artist, album-playlists) -var playlists = {}; -var currentplaylist; -var customTracklists = []; +// array of cached playlists (not only user-playlists, also search, artist, album-playlists) +var playlists = {} +var currentplaylist +var customTracklists = [] -var browseStack = []; -var browseTracks = []; +var browseStack = [] +var browseTracks = [] var ua = navigator.userAgent, isMobileSafari = /Mac/.test(ua) && /Mobile/.test(ua), isMobileWebkit = /WebKit/.test(ua) && /Mobile/.test(ua), isMobile = /Mobile/.test(ua), - isWebkit = /WebKit/.test(ua); + isWebkit = /WebKit/.test(ua) -//constants -PROGRAM_NAME = 'MusicBox'; -ARTIST_TABLE = '#artiststable'; -ALBUM_TABLE = '#albumstable'; -PLAYLIST_TABLE = '#playlisttracks'; -CURRENT_PLAYLIST_TABLE = '#currenttable'; -SEARCH_ALL_TABLE = '#allresulttable'; -SEARCH_ALBUM_TABLE = '#albumresulttable'; -SEARCH_ARTIST_TABLE = '#artistresulttable'; -SEARCH_TRACK_TABLE = '#trackresulttable'; +// constants +PROGRAM_NAME = 'MusicBox' +ARTIST_TABLE = '#artiststable' +ALBUM_TABLE = '#albumstable' +PLAYLIST_TABLE = '#playlisttracks' +CURRENT_PLAYLIST_TABLE = '#currenttable' +SEARCH_ALL_TABLE = '#allresulttable' +SEARCH_ALBUM_TABLE = '#albumresulttable' +SEARCH_ARTIST_TABLE = '#artistresulttable' +SEARCH_TRACK_TABLE = '#trackresulttable' -URI_SCHEME = 'mbw'; +URI_SCHEME = 'mbw' -PLAY_NOW = 0; -PLAY_NEXT = 1; -ADD_THIS_BOTTOM = 2; -ADD_ALL_BOTTOM = 3; -PLAY_ALL = 4; -PLAY_NOW_SEARCH = 5; +PLAY_NOW = 0 +PLAY_NEXT = 1 +ADD_THIS_BOTTOM = 2 +ADD_ALL_BOTTOM = 3 +PLAY_ALL = 4 +PLAY_NOW_SEARCH = 5 -MAX_TABLEROWS = 50; +MAX_TABLEROWS = 50 // the first part of Mopidy extensions which serve radio streams -var radioExtensionsList = ['somafm', 'tunein', 'dirble', 'audioaddict']; +var radioExtensionsList = ['somafm', 'tunein', 'dirble', 'audioaddict'] var uriClassList = [ ['spotify', 'fa-spotify'], @@ -90,7 +90,7 @@ var uriClassList = [ ['yt', 'fa-youtube'], ['audioaddict', 'fa-bullhorn'], ['subsonic', 'fa-folder-open'] -]; +] var uriHumanList = [ ['spotify', 'Spotify'], @@ -107,441 +107,441 @@ var uriHumanList = [ ['youtube', 'YouTube'], ['audioaddict', 'AudioAddict'], ['subsonic', 'Subsonic'] -]; +] -function scrollToTop() { - var divtop = 0; +function scrollToTop () { + var divtop = 0 $('body,html').animate({ scrollTop: divtop - }, 250); + }, 250) } -function scrollToTracklist() { - var divtop = $("#playlisttracksdiv").offset().top - 120; +function scrollToTracklist () { + var divtop = $('#playlisttracksdiv').offset().top - 120 $('body,html').animate({ scrollTop: divtop - }, 250); + }, 250) } -//A hack to find the name of the first artist of a playlist. this is not yet returned by mopidy -//does not work wel with multiple artists of course -function getArtist(pl) { +// A hack to find the name of the first artist of a playlist. this is not yet returned by mopidy +// does not work wel with multiple artists of course +function getArtist (pl) { for (var i = 0; i < pl.length; i++) { for (var j = 0; j < pl[i].artists.length; j++) { if (pl[i].artists[j].name !== '') { - return pl[i].artists[j].name; + return pl[i].artists[j].name } } } } -//A hack to find the first album of a playlist. this is not yet returned by mopidy -function getAlbum(pl) { +// A hack to find the first album of a playlist. this is not yet returned by mopidy +function getAlbum (pl) { for (var i = 0; i < pl.length; i++) { if (pl[i].album.name !== '') { - return pl[i].album.name; + return pl[i].album.name } } } -function artistsToString(artists, max) { - var result = ''; - max = max || 3; +function artistsToString (artists, max) { + var result = '' + max = max || 3 for (var i = 0; i < artists.length && i < max; i++) { if (artists[i].name) { if (i > 0) { - result += ', '; + result += ', ' } - result += artists[i].name; + result += artists[i].name } } - return result; + return result } -/******************************************************** +/** ****************************************************** * break up results and put them in album tables *********************************************************/ -function albumTracksToTable(pl, target, uri) { - var tmp = '
      '; - var liId = ''; - var targetmin = target.substr(1); - $(target).empty(); +function albumTracksToTable (pl, target, uri) { + var tmp = '
        ' + var liId = '' + var targetmin = target.substr(1) + $(target).empty() for (var i = 0; i < pl.length; i++) { - popupData[pl[i].uri] = pl[i]; - liID = targetmin + '-' + pl[i].uri; - tmp += renderSongLi(pl[i], liID, uri); + popupData[pl[i].uri] = pl[i] + liID = targetmin + '-' + pl[i].uri + tmp += renderSongLi(pl[i], liID, uri) } - tmp += '
      '; - $(target).html(tmp); - $(target).attr('data', uri); + tmp += '
    ' + $(target).html(tmp) + $(target).attr('data', uri) } -function renderSongLi(song, liID, uri) { - var name; +function renderSongLi (song, liID, uri) { + var name if (!song.name || song.name === '') { - name = uri.split('/'); - name = decodeURI(name[name.length - 1]); + name = uri.split('/') + name = decodeURI(name[name.length - 1]) } else { - name = song.name; + name = song.name } songLi = '
  • ' + '' + '' + '' + '

    ' + name + '

    ' + - '
  • '; - return songLi; + '' + return songLi } -function renderQueueSongLi(song, liID, uri, tlid) { - var name; +function renderQueueSongLi (song, liID, uri, tlid) { + var name if (!song.name || song.name === '') { - name = uri.split('/'); - name = decodeURI(name[name.length - 1]); + name = uri.split('/') + name = decodeURI(name[name.length - 1]) } else { - name = song.name; + name = song.name } songLi = '
  • ' + '' + '' + '' + '

    ' + name + '

    ' + - '
  • '; - return songLi; + '' + return songLi } -function resultsToTables(results, target, uri) { +function resultsToTables (results, target, uri) { if (!results) { - return; + return } - var tlids = []; + var tlids = [] if (target == CURRENT_PLAYLIST_TABLE) { for (i = 0; i < results.length; i++) { - tlids[i] = results[i].tlid; - results[i] = results[i].track; + tlids[i] = results[i].tlid + results[i] = results[i].track } } - var newalbum = []; - var newtlids = []; - //keep a list of track URIs for retrieving of covers - var coversList = []; - var nextname = ''; - var count = 0; - $(target).html(''); + var newalbum = [] + var newtlids = [] + // keep a list of track URIs for retrieving of covers + var coversList = [] + var nextname = '' + var count = 0 + $(target).html('') - //break into albums and put in tables - var html = ''; - var tableid, j, artistname, alburi, name, iconClass; - var targetmin = target.substr(1); - $(target).attr('data', uri); - var length = 0 || results.length; + // break into albums and put in tables + var html = '' + var tableid, j, artistname, alburi, name, iconClass + var targetmin = target.substr(1) + $(target).attr('data', uri) + var length = 0 || results.length for (i = 0; i < length; i++) { - //create album if none extists + // create album if none extists if (!results[i].album) { - results[i].album = {"__model__": "Album"}; + results[i].album = {'__model__': 'Album'} } - //create album uri if there is none + // create album uri if there is none if (!results[i].album.uri) { - results[i].album.uri = 'x'; + results[i].album.uri = 'x' } if (!results[i].album.name) { - results[i].album.name = ''; + results[i].album.name = '' } - //create name if there is no one + // create name if there is no one if (!results[i].name || results[i].name === '') { - name = results[i].uri.split('/'); - results[i].name = decodeURI(name[name.length - 1]) || 'Track ' + String(i); + name = results[i].uri.split('/') + results[i].name = decodeURI(name[name.length - 1]) || 'Track ' + String(i) } - //leave out unplayable items - if (results[i].name.substring(0, 12) == '[unplayable]') continue; + // leave out unplayable items + if (results[i].name.substring(0, 12) == '[unplayable]') continue - newalbum.push(results[i]); - newtlids.push(tlids[i]); - nextname = ''; + newalbum.push(results[i]) + newtlids.push(tlids[i]) + nextname = '' if ((i < length - 1) && results[i + 1].album && results[i + 1].album.name) { - nextname = results[i + 1].album.name; + nextname = results[i + 1].album.name } if (results[i].length == -1) { - html += '
  • ' + results[i].name + ' [Stream]

  • '; - newalbum = []; - newtlids = []; - nextname = ''; + html += '
  • ' + results[i].name + ' [Stream]

  • ' + newalbum = [] + newtlids = [] + nextname = '' } else { if ((results[i].album.name != nextname) || (nextname === '')) { - tableid = 'art' + i; - //render differently if only one track in the album + tableid = 'art' + i + // render differently if only one track in the album if (newalbum.length == 1) { if (i !== 0) { - html += '
  •  
  • '; + html += '
  •  
  • ' } - iconClass = getMediaClass(newalbum[0].uri); - var liID = targetmin + '-' + newalbum[0].uri; + iconClass = getMediaClass(newalbum[0].uri) + var liID = targetmin + '-' + newalbum[0].uri if (target == CURRENT_PLAYLIST_TABLE) { html += '
  • ' + '' + '' + '' + - '

    ' + newalbum[0].name + "

    "; + '

    ' + newalbum[0].name + "

    " } else { html += '

  • ' + '' + '' + '' + - '

    ' + newalbum[0].name + "

    "; + '

    ' + newalbum[0].name + "

    " } if (newalbum[0].artists) { for (j = 0; j < newalbum[0].artists.length; j++) { - html += newalbum[0].artists[j].name; - html += (j == newalbum[0].artists.length - 1) ? '' : ' / '; - //stop after 3 + html += newalbum[0].artists[j].name + html += (j == newalbum[0].artists.length - 1) ? '' : ' / ' + // stop after 3 if (j > 2) { - html += '...'; - break; + html += '...' + break } } } if (newalbum[0].album.name !== '') { - html += ' / '; + html += ' / ' } - html += '' + newalbum[0].album.name + '

    '; - html += '
  • '; + html += '' + newalbum[0].album.name + '

    ' + html += '' - popupData[newalbum[0].uri] = newalbum[0]; - newalbum = []; - newtlids = []; - } else { //newalbum length + popupData[newalbum[0].uri] = newalbum[0] + newalbum = [] + newtlids = [] + } else { // newalbum length if (results[i].album.uri && results[i].album.name) { - iconClass = getMediaClass(newalbum[0].uri); - html += '
  • '; + iconClass = getMediaClass(newalbum[0].uri) + html += '
  • ' html += '

    ' + results[i].album.name + '

    '; + targetmin + '-cover-' + i + '" class="artistcover" width="30" height="30" />

    ' + results[i].album.name + '

    ' } if (results[i].album.artists) { for (j = 0; j < results[i].album.artists.length; j++) { - html += results[i].album.artists[j].name; - html += (j == results[i].album.artists.length - 1) ? '' : ' / '; - //stop after 3 + html += results[i].album.artists[j].name + html += (j == results[i].album.artists.length - 1) ? '' : ' / ' + // stop after 3 if (j > 2) { - child += '...'; - break; + child += '...' + break } } } - html += '

  • '; + html += '

    ' for (j = 0; j < newalbum.length; j++) { - popupData[newalbum[j].uri] = newalbum[j]; - //hERE! - var liID = targetmin + '-' + newalbum[j].uri; + popupData[newalbum[j].uri] = newalbum[j] + // hERE! + var liID = targetmin + '-' + newalbum[j].uri if (target == CURRENT_PLAYLIST_TABLE) { - html += renderQueueSongLi(newalbum[j], liID, uri, newtlids[j]); + html += renderQueueSongLi(newalbum[j], liID, uri, newtlids[j]) } else { - html += renderSongLi(newalbum[j], liID, uri); + html += renderSongLi(newalbum[j], liID, uri) } } - newalbum = []; - newtlids = []; + newalbum = [] + newtlids = [] if (results[i].album) { - coversList.push([results[i].uri, i]); + coversList.push([results[i].uri, i]) } - } //newalbum length - } //albums name + } // newalbum length + } // albums name } } - tableid = "#" + tableid; - $(target).html(html); - $(target).attr('data', uri); - //retrieve albumcovers + tableid = '#' + tableid + $(target).html(html) + $(target).attr('data', uri) + // retrieve albumcovers for (i = 0; i < coversList.length; i++) { - getCover(coversList[i][0], target + '-cover-' + coversList[i][1], 'small'); + getCover(coversList[i][0], target + '-cover-' + coversList[i][1], 'small') } } -//process updated playlist to gui -function playlisttotable(pl, target, uri) { - var tmp = ''; - $(target).html(''); - var targetmin = target.substr(1); - var child = ''; +// process updated playlist to gui +function playlisttotable (pl, target, uri) { + var tmp = '' + $(target).html('') + var targetmin = target.substr(1) + var child = '' for (var i = 0; i < pl.length; i++) { if (pl[i]) { - popupData[pl[i].uri] = pl[i]; - child = '
  • '; - child += '

    ' + pl[i].name + "

    "; - child += '

    '; - child += '' + timeFromSeconds(pl[i].length / 1000) + ''; + popupData[pl[i].uri] = pl[i] + child = '

  • ' + child += '

    ' + pl[i].name + "

    " + child += '

    ' + child += '' + timeFromSeconds(pl[i].length / 1000) + '' for (var j = 0; j < pl[i].artists.length; j++) { if (pl[i].artists[j]) { - child += pl[i].artists[j].name; - child += (j == pl[i].artists.length - 1) ? '' : ' / '; - //stop after 3 + child += pl[i].artists[j].name + child += (j == pl[i].artists.length - 1) ? '' : ' / ' + // stop after 3 if (j > 2) { - child += '...'; - break; + child += '...' + break } } } - child += ' / ' + pl[i].album.name + '

    '; - child += '
  • '; - tmp += child; + child += ' / ' + pl[i].album.name + '

    ' + child += '' + tmp += child } } - $(target).html(tmp); - $(target).attr('data', uri); + $(target).html(tmp) + $(target).attr('data', uri) } -function getPlaylistTracks(uri) { +function getPlaylistTracks (uri) { if (playlists[uri] && playlists[uri].tracks) { - return Mopidy.when(playlists[uri].tracks); + return Mopidy.when(playlists[uri].tracks) } else { - showLoading(true); - return mopidy.playlists.getItems({'uri': uri}).then(function(refs) { - return processPlaylistItems({'uri': uri, 'items': refs}); - }, console.error); + showLoading(true) + return mopidy.playlists.getItems({'uri': uri}).then(function (refs) { + return processPlaylistItems({'uri': uri, 'items': refs}) + }, console.error) } } -function getUris(tracks) { - var results = []; +function getUris (tracks) { + var results = [] for (var i = 0; i < tracks.length; i++) { - results.push(tracks[i].uri); + results.push(tracks[i].uri) } - return results; + return results } -function getTracksFromUri(uri, full_track_data) { - var returnTracksOrUris = function(tracks) { - return (full_track_data || false) ? tracks : getUris(tracks); - }; +function getTracksFromUri (uri, full_track_data) { + var returnTracksOrUris = function (tracks) { + return (full_track_data || false) ? tracks : getUris(tracks) + } if (customTracklists[uri]) { - return returnTracksOrUris(customTracklists[uri]); + return returnTracksOrUris(customTracklists[uri]) } else if (playlists[uri] && playlists[uri].tracks) { - return returnTracksOrUris(playlists[uri].tracks); + return returnTracksOrUris(playlists[uri].tracks) } - return []; + return [] } -//convert time to human readable format -function timeFromSeconds(length) { - var d = Number(length); - var h = Math.floor(d / 3600); - var m = Math.floor(d % 3600 / 60); - var s = Math.floor(d % 3600 % 60); - return ((h > 0 ? h + ":" : "") + (m > 0 ? (h > 0 && m < 10 ? "0" : "") + m + ":" : "0:") + (s < 10 ? "0" : "") + s); +// convert time to human readable format +function timeFromSeconds (length) { + var d = Number(length) + var h = Math.floor(d / 3600) + var m = Math.floor(d % 3600 / 60) + var s = Math.floor(d % 3600 % 60) + return ((h > 0 ? h + ':' : '') + (m > 0 ? (h > 0 && m < 10 ? '0' : '') + m + ':' : '0:') + (s < 10 ? '0' : '') + s) } -/******* Toast ***/ -function toast(message, delay, textOnly) { - textOnl = textOnly || false; - message = message || "Loading..."; - delay = delay || 1000; +/** ***** Toast ***/ +function toast (message, delay, textOnly) { + textOnl = textOnly || false + message = message || "Loading..." + delay = delay || 1000 $.mobile.loading('show', { text: message, textVisible: true, theme: 'a', textonly: textOnl - }); + }) if (delay > 0) { - setTimeout(function() { - $.mobile.loading('hide'); - }, delay); + setTimeout(function () { + $.mobile.loading('hide') + }, delay) } } -/****************** +/** **************** * Modal dialogs * ******************/ -function showLoading(on) { +function showLoading (on) { if (on) { - $("body").css("cursor", "progress"); + $('body').css('cursor', 'progress') $.mobile.loading('show', { text: 'Loading data from ' + PROGRAM_NAME + '. Please wait...', textVisible: true, theme: 'a' - }); + }) } else { - $("body").css("cursor", "default"); - $.mobile.loading('hide'); + $('body').css('cursor', 'default') + $.mobile.loading('hide') } } -function showOffline(on) { +function showOffline (on) { if (on) { $.mobile.loading('show', { text: 'Trying to reach ' + PROGRAM_NAME + '. Please wait...', textVisible: true, theme: 'a' - }); + }) } else { - $.mobile.loading('hide'); + $.mobile.loading('hide') } } // from http://dzone.com/snippets/validate-url-regexp -function validUri(str) { - var regexp = /^(mms|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/; - return regexp.test(str); +function validUri (str) { + var regexp = /^(mms|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/ + return regexp.test(str) } -function validServiceUri(str) { - return validUri(str) || isServiceUri(str); +function validServiceUri (str) { + return validUri(str) || isServiceUri(str) } -function getScheme(uri) { - return uri.split(':')[0].toLowerCase(); +function getScheme (uri) { + return uri.split(':')[0].toLowerCase() } -function isStreamUri(uri) { - var a = validUri(uri); - var b = radioExtensionsList.indexOf(getScheme(uri)) >= 0; - return a || b; +function isStreamUri (uri) { + var a = validUri(uri) + var b = radioExtensionsList.indexOf(getScheme(uri)) >= 0 + return a || b } -function getMediaClass(uri) { - var scheme = getScheme(uri); +function getMediaClass (uri) { + var scheme = getScheme(uri) for (var i = 0; i < uriClassList.length; i++) { if (scheme == uriClassList[i][0]) { - return "fa " + uriClassList[i][1]; + return 'fa ' + uriClassList[i][1] } } - return ''; + return '' } -function getMediaHuman(uri) { - var scheme = getScheme(uri); +function getMediaHuman (uri) { + var scheme = getScheme(uri) for (var i = 0; i < uriHumanList.length; i++) { if (scheme == uriHumanList[i][0]) { - return uriHumanList[i][1]; + return uriHumanList[i][1] } } - return ''; + return '' } -function isServiceUri(uri) { - var scheme = getScheme(uri); +function isServiceUri (uri) { + var scheme = getScheme(uri) for (var i = 0; i < uriClassList.length; i++) { if (scheme == uriClassList[i][0]) { - return true; + return true } } for (var i = 0; i < radioExtensionsList.length; i++) { if (scheme == radioExtensionsList[i]) { - return true; + return true } } - return false; + return false } -function isFavouritesPlaylist(playlist) { +function isFavouritesPlaylist (playlist) { return (playlist.name == STREAMS_PLAYLIST_NAME && - getScheme(playlist.uri) == STREAMS_PLAYLIST_SCHEME); + getScheme(playlist.uri) == STREAMS_PLAYLIST_SCHEME) } -function isSpotifyStarredPlaylist(playlist) { - var starredRegex = /spotify:user:.*:starred/g; - return (starredRegex.test(playlist.uri) && playlist.name == 'Starred'); +function isSpotifyStarredPlaylist (playlist) { + var starredRegex = /spotify:user:.*:starred/g + return (starredRegex.test(playlist.uri) && playlist.name == 'Starred') } diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index d1be849..4827d1c 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -3,32 +3,32 @@ * do- functions interact with the server * show- functions do both */ -/******************** +/** ****************** * Song Info Sreen * ********************/ -function resetSong() { - setPlayState(false); - setPosition(0); - var data = new Object({}); - data.tlid = -1; - data.track = new Object({}); - data.track.name = ''; - data.track.artists = ''; - data.track.length = 0; - data.track.uri = ' '; - setSongInfo(data); +function resetSong () { + setPlayState(false) + setPosition(0) + var data = new Object({}) + data.tlid = -1 + data.track = new Object({}) + data.track.name = '' + data.track.artists = '' + data.track.length = 0 + data.track.uri = ' ' + setSongInfo(data) } -function resizeMb() { - $("#infoname").html(songdata.track.name); - $("#infoartist").html(artiststext); +function resizeMb () { + $('#infoname').html(songdata.track.name) + $('#infoartist').html(artiststext) if ($(window).width() <= 960) { // $('#playlisttracksdiv').hide(); // $('#playlistslistdiv').show(); } else { - $('#playlisttracksdiv').show(); - $('#playlistslistdiv').show(); + $('#playlisttracksdiv').show() + $('#playlistslistdiv').show() } // //set height of playlist scrollers /* if ($(window).width() > 960) { @@ -68,412 +68,412 @@ function resizeMb() { */ } -function setSongTitle(title, refresh_ui) { - songdata.track.name = title; - $("#modalname").html(title); +function setSongTitle (title, refresh_ui) { + songdata.track.name = title + $('#modalname').html(title) if (refresh_ui) { - resizeMb(); + resizeMb() } } -function setSongInfo(data) { +function setSongInfo (data) { // console.log(data, songdata); - if (!data ) { return; } - if (data.tlid == songdata.tlid) { return; } + if (!data) { return } + if (data.tlid == songdata.tlid) { return } if (!data.track.name || data.track.name === '') { - var name = data.track.uri.split('/'); - data.track.name = decodeURI(name[name.length - 1]); + var name = data.track.uri.split('/') + data.track.name = decodeURI(name[name.length - 1]) } - updatePlayIcons(data.track.uri, data.tlid); - artistshtml = ''; - artiststext = ''; + updatePlayIcons(data.track.uri, data.tlid) + artistshtml = '' + artiststext = '' if (validUri(data.track.name)) { for (var key in streamUris) { - rs = streamUris[key]; + rs = streamUris[key] if (rs && rs[1] == data.track.name) { - data.track.name = (rs[0] || rs[1]); + data.track.name = (rs[0] || rs[1]) } } } - songdata = data; + songdata = data - setSongTitle(data.track.name, false); - songlength = Infinity; + setSongTitle(data.track.name, false) + songlength = Infinity if (!data.track.length || data.track.length === 0) { - $('#trackslider').next().find('.ui-slider-handle').hide(); - $('#trackslider').slider('disable'); + $('#trackslider').next().find('.ui-slider-handle').hide() + $('#trackslider').slider('disable') // $('#streamnameinput').val(data.track.name); // $('#streamuriinput').val(data.track.uri); } else { - songlength = data.track.length; - $('#trackslider').slider('enable'); - $('#trackslider').next().find('.ui-slider-handle').show(); + songlength = data.track.length + $('#trackslider').slider('enable') + $('#trackslider').next().find('.ui-slider-handle').show() } - var arttmp = ''; + var arttmp = '' - if(data.track.artists) { - for (var j = 0; j < data.track.artists.length; j++) { - artistshtml += '' + data.track.artists[j].name + ''; - artiststext += data.track.artists[j].name; - if (j != data.track.artists.length - 1) { - artistshtml += ', '; - artiststext += ', '; - } + if (data.track.artists) { + for (var j = 0; j < data.track.artists.length; j++) { + artistshtml += '' + data.track.artists[j].name + '' + artiststext += data.track.artists[j].name + if (j != data.track.artists.length - 1) { + artistshtml += ', ' + artiststext += ', ' + } } - arttmp = artistshtml; + arttmp = artistshtml } if (data.track.album && data.track.album.name) { - $("#modalalbum").html('' + data.track.album.name + ''); - getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge'); + $('#modalalbum').html('' + data.track.album.name + '') + getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge') } else { - $("#modalalbum").html(''); - $("#infocover").attr('src', 'images/default_cover.png'); - $("#controlspopupimage").attr('src', 'images/default_cover.png'); + $('#modalalbum').html('') + $('#infocover').attr('src', 'images/default_cover.png') + $('#controlspopupimage').attr('src', 'images/default_cover.png') } - $("#modalartist").html(arttmp); + $('#modalartist').html(arttmp) - $("#trackslider").attr("min", 0); - $("#trackslider").attr("max", songlength); - resetProgressTimer(); - progressTimer.set(0, songlength); + $('#trackslider').attr('min', 0) + $('#trackslider').attr('max', songlength) + resetProgressTimer() + progressTimer.set(0, songlength) if (play) { - startProgressTimer(); + startProgressTimer() } - resizeMb(); + resizeMb() } -/****************** +/** **************** * display popups * ******************/ -function closePopups() { - $('#popupTracks').popup('close'); - $('#artistpopup').popup('close'); - $('#coverpopup').popup('close'); - $('#popupQueue').popup('close'); - $('#controlspopup').popup('close'); +function closePopups () { + $('#popupTracks').popup('close') + $('#artistpopup').popup('close') + $('#coverpopup').popup('close') + $('#popupQueue').popup('close') + $('#controlspopup').popup('close') } -function popupTracks(e, listuri, trackuri, tlid) { +function popupTracks (e, listuri, trackuri, tlid) { if (!e) - e = window.event; - $('.popupTrackName').html(popupData[trackuri].name); - $('.popupAlbumName').html(popupData[trackuri].album.name); - var child = ""; + e = window.event + $('.popupTrackName').html(popupData[trackuri].name) + $('.popupAlbumName').html(popupData[trackuri].album.name) + var child = "" if (popupData[trackuri].artists) { if (popupData[trackuri].artists.length == 1) { - child = 'Show Artist'; - $('.popupArtistName').html(popupData[trackuri].artists[0].name); - $('.popupArtistHref').attr('onclick', 'showArtist("' + popupData[trackuri].artists[0].uri + '");' ); - $('.popupArtistsDiv').hide(); - $('.popupArtistsLi').show(); + child = 'Show Artist' + $('.popupArtistName').html(popupData[trackuri].artists[0].name) + $('.popupArtistHref').attr('onclick', 'showArtist("' + popupData[trackuri].artists[0].uri + '");') + $('.popupArtistsDiv').hide() + $('.popupArtistsLi').show() } else { for (var j = 0; j < popupData[trackuri].artists.length; j++) { - child += '
  • ' + popupData[trackuri].artists[j].name + '
  • '; + child += '
  • ' + popupData[trackuri].artists[j].name + '
  • ' } - $('.popupArtistsLi').hide(); - $('.popupArtistsLv').html(child).show(); - $('.popupArtistsDiv').show(); + $('.popupArtistsLi').hide() + $('.popupArtistsLv').html(child).show() + $('.popupArtistsDiv').show() // this makes the viewport of the window resize somehow - $('.popupArtistsLv').listview("refresh"); + $('.popupArtistsLv').listview('refresh') } } else { - $('.popupArtistsDiv').hide(); - $('.popupArtistsLi').hide(); + $('.popupArtistsDiv').hide() + $('.popupArtistsLi').hide() } - var hash = document.location.hash.split('?'); - var divid = hash[0].substr(1); - var popupName = ''; + var hash = document.location.hash.split('?') + var divid = hash[0].substr(1) + var popupName = '' if (divid == 'current') { - $(".addqueue").hide(); - popupName = '#popupQueue'; + $('.addqueue').hide() + popupName = '#popupQueue' } else if (divid == 'browse') { - $(".addqueue").show(); - popupName = '#popupBrowse'; + $('.addqueue').show() + popupName = '#popupBrowse' } else { - $(".addqueue").show(); - popupName = '#popupTracks'; + $('.addqueue').show() + popupName = '#popupTracks' } if (typeof tlid != 'undefined' && tlid !== '') { - $(popupName).data("list", listuri).data("track", trackuri).data("tlid", tlid).popup("open", { + $(popupName).data('list', listuri).data('track', trackuri).data('tlid', tlid).popup('open', { x : e.pageX, y : e.pageY - }); + }) } else { - $(popupName).data("list", listuri).data("track", trackuri).popup("open", { + $(popupName).data('list', listuri).data('track', trackuri).popup('open', { x : e.pageX, y : e.pageY - }); + }) } - return false; + return false } -function showAlbumPopup(popupId) { - uri = $(popupId).data("track"); - showAlbum(popupData[uri].album.uri); +function showAlbumPopup (popupId) { + uri = $(popupId).data('track') + showAlbum(popupData[uri].album.uri) } -/********************** +/** ******************** * initialize sockets * **********************/ -function initSocketevents() { - mopidy.on("state:online", function() { - showOffline(false); - getCurrentPlaylist(); - updateStatusOfAll(); - getPlaylists(); - getUriSchemes().then(function() { - showFavourites(); - }); - getBrowseDir(); - getSearchSchemes(); - showLoading(false); - $(window).hashchange(); - }); +function initSocketevents () { + mopidy.on('state:online', function () { + showOffline(false) + getCurrentPlaylist() + updateStatusOfAll() + getPlaylists() + getUriSchemes().then(function () { + showFavourites() + }) + getBrowseDir() + getSearchSchemes() + showLoading(false) + $(window).hashchange() + }) - mopidy.on("state:offline", function() { - resetSong(); - showOffline(true); - }); + mopidy.on('state:offline', function () { + resetSong() + showOffline(true) + }) - mopidy.on("event:optionsChanged", updateOptions); + mopidy.on('event:optionsChanged', updateOptions) - mopidy.on("event:trackPlaybackStarted", function(data) { - setSongInfo(data.tl_track); - setPlayState(true); - }); + mopidy.on('event:trackPlaybackStarted', function (data) { + setSongInfo(data.tl_track) + setPlayState(true) + }) - mopidy.on("event:playlistsLoaded", function(data) { - showLoading(true); - getPlaylists(); - }); + mopidy.on('event:playlistsLoaded', function (data) { + showLoading(true) + getPlaylists() + }) - mopidy.on("event:playlistChanged", function(data) { - $('#playlisttracksdiv').hide(); - $('#playlistslistdiv').show(); - delete playlists[data.playlist.uri]; - getPlaylists(); - }); + mopidy.on('event:playlistChanged', function (data) { + $('#playlisttracksdiv').hide() + $('#playlistslistdiv').show() + delete playlists[data.playlist.uri] + getPlaylists() + }) - mopidy.on("event:playlistDeleted", function(data) { - $('#playlisttracksdiv').hide(); - $('#playlistslistdiv').show(); - delete playlists[data.uri]; - getPlaylists(); - }); + mopidy.on('event:playlistDeleted', function (data) { + $('#playlisttracksdiv').hide() + $('#playlistslistdiv').show() + delete playlists[data.uri] + getPlaylists() + }) - mopidy.on("event:volumeChanged", function(data) { - setVolume(data.volume); - }); + mopidy.on('event:volumeChanged', function (data) { + setVolume(data.volume) + }) - mopidy.on("event:muteChanged", function(data) { - setMute(data.mute); - }); + mopidy.on('event:muteChanged', function (data) { + setMute(data.mute) + }) - mopidy.on("event:playbackStateChanged", function(data) { + mopidy.on('event:playbackStateChanged', function (data) { switch (data.new_state) { - case "paused": - case "stopped": - setPlayState(false); - break; - case "playing": - setPlayState(true); - break; + case 'paused': + case 'stopped': + setPlayState(false) + break + case 'playing': + setPlayState(true) + break } - }); + }) - mopidy.on("event:tracklistChanged", function(data) { - getCurrentPlaylist(); - }); + mopidy.on('event:tracklistChanged', function (data) { + getCurrentPlaylist() + }) - mopidy.on("event:seeked", function(data) { - setPosition(parseInt(data.time_position)); + mopidy.on('event:seeked', function (data) { + setPosition(parseInt(data.time_position)) if (play) { - startProgressTimer(); + startProgressTimer() } - }); + }) - mopidy.on("event:streamTitleChanged", function(data) { - setSongTitle(data.title, true); - }); + mopidy.on('event:streamTitleChanged', function (data) { + setSongTitle(data.title, true) + }) } -$(document).bind("pageinit", function() { - resizeMb(); +$(document).bind('pageinit', function () { + resizeMb() -}); +}) -/************** +/** ************ * gui stuff * **************/ -function toggleFullscreen() { - if (isMobileSafari) { alert ("To get this app in Full Screen, you have to add it to your home-screen using the Share button."); exit(); } - if (!isFullscreen() ) { // current working methods - var docElm = document.documentElement; +function toggleFullscreen () { + if (isMobileSafari) { alert('To get this app in Full Screen, you have to add it to your home-screen using the Share button.'); exit() } + if (!isFullscreen()) { // current working methods + var docElm = document.documentElement if (docElm.requestFullscreen) { - docElm.requestFullscreen(); + docElm.requestFullscreen() } else if (docElm.msRequestFullscreen) { - docElm.msRequestFullscreen(); + docElm.msRequestFullscreen() } else if (docElm.mozRequestFullScreen) { - docElm.mozRequestFullScreen(); + docElm.mozRequestFullScreen() } else if (docElm.webkitRequestFullScreen) { - docElm.webkitRequestFullScreen(); + docElm.webkitRequestFullScreen() } } else { if (document.exitFullscreen) { - document.exitFullscreen(); + document.exitFullscreen() } else if (document.msExitFullscreen) { - document.msExitFullscreen(); + document.msExitFullscreen() } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); + document.mozCancelFullScreen() } else if (document.webkitCancelFullScreen) { - document.webkitCancelFullScreen(); + document.webkitCancelFullScreen() } } } -function isFullscreen() { +function isFullscreen () { return (document.fullscreenElement || // alternative standard method - document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement ); // current working methods + document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) // current working methods } -function switchContent(divid, uri) { - var hash = divid; +function switchContent (divid, uri) { + var hash = divid if (uri) { - hash += "?" + uri; + hash += '?' + uri } - location.hash = "#" + hash; + location.hash = '#' + hash } -function setHeadline(site){ - site = site.trim(); - str = $('.mainNav').find('a[href$='+site+']').text(); - if(str === ""){ - str = site.charAt(0).toUpperCase() + site.slice(1); +function setHeadline (site) { + site = site.trim() + str = $('.mainNav').find('a[href$=' + site + ']').text() + if (str === '') { + str = site.charAt(0).toUpperCase() + site.slice(1) } - $('#contentHeadline').html('' + str + ''); + $('#contentHeadline').html('' + str + '') } -//update tracklist options. -function updateOptions() { - mopidy.tracklist.getRepeat().then(processRepeat, console.error); - mopidy.tracklist.getRandom().then(processRandom, console.error); - mopidy.tracklist.getConsume().then(processConsume, console.error); - mopidy.tracklist.getSingle().then(processSingle, console.error); +// update tracklist options. +function updateOptions () { + mopidy.tracklist.getRepeat().then(processRepeat, console.error) + mopidy.tracklist.getRandom().then(processRandom, console.error) + mopidy.tracklist.getConsume().then(processConsume, console.error) + mopidy.tracklist.getSingle().then(processSingle, console.error) } -//update everything as if reloaded -function updateStatusOfAll() { - mopidy.playback.getCurrentTlTrack().then(processCurrenttrack, console.error); - mopidy.playback.getTimePosition().then(processCurrentposition, console.error); - mopidy.playback.getState().then(processPlaystate, console.error); +// update everything as if reloaded +function updateStatusOfAll () { + mopidy.playback.getCurrentTlTrack().then(processCurrenttrack, console.error) + mopidy.playback.getTimePosition().then(processCurrentposition, console.error) + mopidy.playback.getState().then(processPlaystate, console.error) - updateOptions(); + updateOptions() - mopidy.playback.getVolume().then(processVolume, console.error); - mopidy.mixer.getMute().then(processMute, console.error); + mopidy.playback.getVolume().then(processVolume, console.error) + mopidy.mixer.getMute().then(processMute, console.error) } -function locationHashChanged() { - var hash = document.location.hash.split('?'); - //remove # - var divid = hash[0].substr(1); - setHeadline(divid); +function locationHashChanged () { + var hash = document.location.hash.split('?') + // remove # + var divid = hash[0].substr(1) + setHeadline(divid) - var uri = hash[1]; - $('.mainNav a').removeClass('ui-state-active ui-state-persist ui-btn-active'); - //i don't know why some li elements have those classes, but they do, so we need to remove them - $('.mainNav li').removeClass('ui-state-active ui-state-persist ui-btn-active'); + var uri = hash[1] + $('.mainNav a').removeClass('ui-state-active ui-state-persist ui-btn-active') + // i don't know why some li elements have those classes, but they do, so we need to remove them + $('.mainNav li').removeClass('ui-state-active ui-state-persist ui-btn-active') if ($(window).width() < 560) { - $("#panel").panel("close"); + $('#panel').panel('close') } - $('.pane').hide(); + $('.pane').hide() - $('#' + divid + 'pane').show(); + $('#' + divid + 'pane').show() - switch(divid) { + switch (divid) { case 'home': - $('#navhome a').addClass('ui-state-active ui-state-persist ui-btn-active'); - break; + $('#navhome a').addClass('ui-state-active ui-state-persist ui-btn-active') + break case 'nowPlaying': - $('#navnowPlaying a').addClass('ui-state-active ui-state-persist ui-btn-active'); - break; + $('#navnowPlaying a').addClass('ui-state-active ui-state-persist ui-btn-active') + break case 'current': - $('#navcurrent a').addClass('ui-state-active ui-state-persist ui-btn-active'); - getCurrentPlaylist(); - break; + $('#navcurrent a').addClass('ui-state-active ui-state-persist ui-btn-active') + getCurrentPlaylist() + break case 'playlists': - $('#navplaylists a').addClass('ui-state-active ui-state-persist ui-btn-active'); - break; + $('#navplaylists a').addClass('ui-state-active ui-state-persist ui-btn-active') + break case 'browse': - $('#navbrowse a').addClass('ui-state-active ui-state-persist ui-btn-active'); - break; + $('#navbrowse a').addClass('ui-state-active ui-state-persist ui-btn-active') + break case 'search': - $('#navsearch a').addClass($.mobile.activeBtnClass); - $("#searchinput").focus(); + $('#navsearch a').addClass($.mobile.activeBtnClass) + $('#searchinput').focus() if (customTracklists['mbw:allresultscache'] === '') { - initSearch($('#searchinput').val()); + initSearch($('#searchinput').val()) } - break; + break case 'stream': - $('#navstream a').addClass('ui-state-active ui-state-persist ui-btn-active'); - break; + $('#navstream a').addClass('ui-state-active ui-state-persist ui-btn-active') + break case 'artists': if (uri !== '') { - showArtist(uri); + showArtist(uri) } - break; + break case 'albums': if (uri !== '') { - showAlbum(uri); + showAlbum(uri) } - break; + break } - //switch the footer - switch(divid) { + // switch the footer + switch (divid) { case 'nowPlaying': - $('#normalFooter').hide(); - $('#nowPlayingFooter').show(); - break; + $('#normalFooter').hide() + $('#nowPlayingFooter').show() + break default: - $('#normalFooter').show(); - $('#nowPlayingFooter').hide(); + $('#normalFooter').show() + $('#nowPlayingFooter').hide() } // Set the page title based on the hash. - document.title = PROGRAM_NAME; - return false; + document.title = PROGRAM_NAME + return false } -/*********************** +/** ********************* * initialize software * ***********************/ -$(document).ready(function(event) { - //check for websockets +$(document).ready(function (event) { + // check for websockets if (!window.WebSocket) { - switchContent("playlists"); - $('#playlistspane').html('

    Old Browser

    Sorry. Your browser isn\'t modern enough for this webapp. Modern versions of Chrome, Firefox, Safari all will do. Maybe Opera and Internet Explorer 10 also work, but it\'s not tested.

    '); - exit; + switchContent('playlists') + $('#playlistspane').html('

    Old Browser

    Sorry. Your browser isn\'t modern enough for this webapp. Modern versions of Chrome, Firefox, Safari all will do. Maybe Opera and Internet Explorer 10 also work, but it\'s not tested.

    ') + exit } - //workaround for a bug in jQuery Mobile, without that the panel doesn't close on mobile devices... - $('.ui-panel-dismiss').on( "tap", function() { $("#panel").panel("close"); } ); - //end of workaround + // workaround for a bug in jQuery Mobile, without that the panel doesn't close on mobile devices... + $('.ui-panel-dismiss').on( 'tap', function () { $('#panel').panel('close') }) + // end of workaround - $(window).hashchange(); + $(window).hashchange() // Connect to server if (websocketUrl) { @@ -481,196 +481,196 @@ $(document).ready(function(event) { mopidy = new Mopidy({ webSocketUrl: websocketUrl, callingConvention: 'by-position-or-by-name' - }); + }) } catch (e) { - showOffline(true); + showOffline(true) } } else { try { - mopidy = new Mopidy({callingConvention: 'by-position-or-by-name'}); - } catch (e) { - showOffline(true); - } + mopidy = new Mopidy({callingConvention: 'by-position-or-by-name'}) + } catch (e) { + showOffline(true) + } } - //initialize events - initSocketevents(); + // initialize events + initSocketevents() progressTimer = new ProgressTimer({ callback: timerCallback, - //updateRate: 2000, - }); + // updateRate: 2000, + }) - resetSong(); + resetSong() if (location.hash.length < 2) { - switchContent("home"); + switchContent('home') } - initgui = false; - window.onhashchange = locationHashChanged; + initgui = false + window.onhashchange = locationHashChanged - //only show backbutton if in UIWebview + // only show backbutton if in UIWebview if (window.navigator.standalone) { - $("#btback").show(); + $('#btback').show() } else { - $("#btback").hide(); + $('#btback').hide() } - $(window).resize(function() { - resizeMb(); - }); + $(window).resize(function () { + resizeMb() + }) - //navigation temporary, rewrite this! - $('#songinfo').click(function() { - return switchContent('nowPlaying'); - }); - $('#controlspopupimage').click(function() { - return switchContent('current'); - }); - $('#navToggleFullscreen').click(function() { - toggleFullscreen(); - }); + // navigation temporary, rewrite this! + $('#songinfo').click(function () { + return switchContent('nowPlaying') + }) + $('#controlspopupimage').click(function () { + return switchContent('current') + }) + $('#navToggleFullscreen').click(function () { + toggleFullscreen() + }) // event handlers for full screen mode - $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange, MSFullscreenChange', function(e) { + $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange, MSFullscreenChange', function (e) { if (isFullscreen()) { - document.getElementById("toggletxt").innerHTML = "Exit Fullscreen"; + document.getElementById('toggletxt').innerHTML = "Exit Fullscreen" } else { - document.getElementById("toggletxt").innerHTML = "Fullscreen"; + document.getElementById('toggletxt').innerHTML = "Fullscreen" } - }); + }) // remove buttons only for MusicBox if (!isMusicBox) { - $('#navSettings').hide(); - $('#navshutdown').hide(); - $('#homesettings').hide(); - $('#homeshutdown').hide(); + $('#navSettings').hide() + $('#navshutdown').hide() + $('#homesettings').hide() + $('#homeshutdown').hide() } // remove Alarm Clock if it is not present if (!hasAlarmClock) { - $('#navAlarmClock').hide(); - $('#homeAlarmClock').hide(); - $('#homeAlarmClock').nextAll().find('.ui-block-a, .ui-block-b').toggleClass('ui-block-a').toggleClass('ui-block-b'); + $('#navAlarmClock').hide() + $('#homeAlarmClock').hide() + $('#homeAlarmClock').nextAll().find('.ui-block-a, .ui-block-b').toggleClass('ui-block-a').toggleClass('ui-block-b') } - //navigation stuff + // navigation stuff - $(document).keypress( function (event) { - //console.log('kp: '+event); - if (event.target.tagName != 'INPUT') { - var unicode=event.keyCode? event.keyCode : event.charCode; - var actualkey=String.fromCharCode(unicode); - switch(actualkey) { - case ' ': - doPlay(); - event.preventDefault(); - break; - case '>': - doNext(); - event.preventDefault(); - break; - case '<': - doPrevious(); - event.preventDefault(); - break; + $(document).keypress(function (event) { + // console.log('kp: '+event); + if (event.target.tagName != 'INPUT') { + var unicode = event.keyCode ? event.keyCode : event.charCode + var actualkey = String.fromCharCode(unicode) + switch (actualkey) { + case ' ': + doPlay() + event.preventDefault() + break + case '>': + doNext() + event.preventDefault() + break + case '<': + doPrevious() + event.preventDefault() + break } - return true; + return true } - }); + }) if ($(window).width() < 980) { - $("#panel").panel("close"); - }else{ - $("#panel").panel("open"); + $('#panel').panel('close') + } else { + $('#panel').panel('open') } - $.event.special.swipe.horizontalDistanceThreshold = 125; // (default: 30px) Swipe horizontal displacement must be more than this. - $.event.special.swipe.verticalDistanceThreshold = 50; // (default: 75px) Swipe vertical displacement must be less than this. - $.event.special.swipe.durationThreshold = 500; + $.event.special.swipe.horizontalDistanceThreshold = 125 // (default: 30px) Swipe horizontal displacement must be more than this. + $.event.special.swipe.verticalDistanceThreshold = 50 // (default: 75px) Swipe vertical displacement must be less than this. + $.event.special.swipe.durationThreshold = 500 // swipe songinfo and panel - $( "#normalFooter, #nowPlayingFooter" ).on( "swiperight", doPrevious ); - $( "#normalFooter, #nowPlayingFooter" ).on( "swipeleft", doNext ); - $( "#nowPlayingpane, .ui-body-c, #header, #panel, .pane" ).on( "swiperight", function() { - if(!$(event.target).is("#normalFooter") && !$(event.target).is("#nowPlayingFooter")) { - $("#panel").panel("open"); - event.stopImmediatePropagation(); } - } ); - $( "#nowPlayingpane, .ui-body-c, #header, #panel, .pane" ).on( "swipeleft", function() { - if(!$(event.target).is("#normalFooter") && !$(event.target).is("#nowPlayingFooter")) { - $("#panel").panel("close"); - event.stopImmediatePropagation(); } - } ); + $("#normalFooter, #nowPlayingFooter").on( 'swiperight', doPrevious) + $("#normalFooter, #nowPlayingFooter").on( 'swipeleft', doNext) + $("#nowPlayingpane, .ui-body-c, #header, #panel, .pane").on( 'swiperight', function () { + if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) { + $('#panel').panel('open') + event.stopImmediatePropagation() } + }) + $("#nowPlayingpane, .ui-body-c, #header, #panel, .pane").on( 'swipeleft', function () { + if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) { + $('#panel').panel('close') + event.stopImmediatePropagation() } + }) - $( "#trackslider" ).on( "slidestart", function() { - progressTimer.stop(); - $( "#trackslider" ).on( "change", function() { updatePosition( $(this).val() ); } ); - } ); + $("#trackslider").on( 'slidestart', function () { + progressTimer.stop() + $("#trackslider").on( 'change', function () { updatePosition($(this).val()) }) + }) - $( "#trackslider" ).on( "slidestop", function() { - $( "#trackslider" ).off( "change"); - doSeekPos( $(this).val() ); - } ); + $("#trackslider").on( 'slidestop', function () { + $("#trackslider").off( 'change') + doSeekPos($(this).val()) + }) - $( "#volumeslider" ).on( "slidestart", function() { volumeSliding = true; } ); - $( "#volumeslider" ).on( "slidestop", function() { volumeSliding = false; } ); - $( "#volumeslider" ).on( "change", function() { doVolume( $(this).val() ); } ); -}); + $("#volumeslider").on( 'slidestart', function () { volumeSliding = true }) + $("#volumeslider").on( 'slidestop', function () { volumeSliding = false }) + $("#volumeslider").on( 'change', function () { doVolume($(this).val()) }) +}) function updatePlayIcons (uri, tlid) { - //update styles of listviews - $('#currenttable li').each(function() { - var eachTlid = $(this).attr('tlid'); + // update styles of listviews + $('#currenttable li').each(function () { + var eachTlid = $(this).attr('tlid') if (typeof eachTlid != 'undefined') { - eachTlid = parseInt(eachTlid); + eachTlid = parseInt(eachTlid) } if (this.id == 'currenttable-' + uri && eachTlid == tlid) { - $(this).addClass('currenttrack'); + $(this).addClass('currenttrack') } else { - $(this).removeClass("currenttrack"); - } - }); + $(this).removeClass('currenttrack') + } + }) - $('#playlisttracks li').each(function() { + $('#playlisttracks li').each(function () { if (this.id == 'playlisttracks-' + uri) { - $(this).addClass('currenttrack2'); + $(this).addClass('currenttrack2') } else { - $(this).removeClass("currenttrack2"); - } - }); + $(this).removeClass('currenttrack2') + } + }) - $('#trackresulttable li').each(function() { + $('#trackresulttable li').each(function () { if (this.id == 'trackresulttable-' + uri) { - $(this).addClass('currenttrack2'); + $(this).addClass('currenttrack2') } else { - $(this).removeClass("currenttrack2"); - } - }); + $(this).removeClass('currenttrack2') + } + }) - $('#artiststable li').each(function() { + $('#artiststable li').each(function () { if (this.id == 'artiststable-' + uri) { - $(this).addClass('currenttrack2'); + $(this).addClass('currenttrack2') } else { - $(this).removeClass("currenttrack2"); - } - }); + $(this).removeClass('currenttrack2') + } + }) - $('#albumstable li').each(function() { + $('#albumstable li').each(function () { if (this.id == 'albumstable-' + uri) { - $(this).addClass('currenttrack2'); + $(this).addClass('currenttrack2') } else { - $(this).removeClass("currenttrack2"); - } - }); - $('#browselist li').each(function() { + $(this).removeClass('currenttrack2') + } + }) + $('#browselist li').each(function () { if (this.id == 'browselisttracks-' + uri) { - $(this).addClass('currenttrack2'); + $(this).addClass('currenttrack2') } else { - $(this).removeClass("currenttrack2"); - } - }); + $(this).removeClass('currenttrack2') + } + }) } diff --git a/mopidy_musicbox_webclient/static/js/images.js b/mopidy_musicbox_webclient/static/js/images.js index 652fae3..8cb61dc 100644 --- a/mopidy_musicbox_webclient/static/js/images.js +++ b/mopidy_musicbox_webclient/static/js/images.js @@ -2,91 +2,91 @@ * @author Wouter van Wijk */ -API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733'; -API_SECRET = '2c631802c2285d5d5d1502462fe42a2b'; +API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733' +API_SECRET = '2c631802c2285d5d5d1502462fe42a2b' -var fmcache; -var lastfm; +var fmcache +var lastfm $(window).load(function () { // create a Cache object - fmcache = new LastFMCache(); + fmcache = new LastFMCache() // create a LastFM object lastfm = new LastFM({ apiKey : API_KEY, apiSecret : API_SECRET, cache : fmcache - }); -}); + }) +}) -function getCover(uri, images, size) { - var defUrl = 'images/default_cover.png'; - $(images).attr('src', defUrl); +function getCover (uri, images, size) { + var defUrl = 'images/default_cover.png' + $(images).attr('src', defUrl) if (!uri) { - return; + return } - mopidy.library.getImages({'uris': [uri]}).then(function(imageResults) { - var uri = Object.keys(imageResults)[0]; + 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); + $(images).attr('src', imageResults[uri][0].uri) } else { // Also check deprecated 'album.images' in case backend does not // implement mopidy.library.getImages yet... - getCoverFromAlbum(uri, images, size); + 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'. -function getCoverFromAlbum(uri, images, size) { - mopidy.library.lookup({'uris': [uri]}).then(function(resultDict) { - var uri = Object.keys(resultDict)[0]; - var track = resultDict[uri][0]; - if (track.album && track.album.images && (track.album.images.length > 0) ) { - $(images).attr('src', track.album.images[0]); +function getCoverFromAlbum (uri, images, size) { + mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) { + var uri = Object.keys(resultDict)[0] + var track = resultDict[uri][0] + if (track.album && track.album.images && (track.album.images.length > 0)) { + $(images).attr('src', track.album.images[0]) } else { // Fallback to last.fm - getCoverFromLastFm(track, images, size); + getCoverFromLastFm(track, images, size) } - }); + }) } -function getCoverFromLastFm(track, images, size) { - var defUrl = 'images/default_cover.png'; +function getCoverFromLastFm (track, images, size) { + var defUrl = 'images/default_cover.png' if (!(track.album || track.artist)) { - return; + return } - var albumname = track.album.name || ''; - var artistname = ''; - if ( track.album.artists && (track.album.artists.length > 0) ) { + var albumname = track.album.name || '' + var artistname = '' + if (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) ) { + 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; + artistname = track.artists[0].name } - lastfm.album.getInfo( {artist: artistname, album: albumname}, - { success: function(data) { + 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); + if (data.album.image[i].size == size) { + $(images).attr('src', data.album.image[i]['#text'] || defUrl) } } } - }); + }) } -function getArtistImage(nwartist, image, size) { - var defUrl = 'images/user_24x32.png'; - lastfm.artist.getInfo({artist: nwartist}, {success: function(data){ +function getArtistImage (nwartist, image, size) { + var defUrl = 'images/user_24x32.png' + lastfm.artist.getInfo({artist: nwartist}, {success: function (data) { for (var i = 0; i < data.artist.image.length; i++) { - if ( data.artist.image[i].size == size) { - $(image).attr('src', data.artist.image[i]['#text'] || defUrl); + if (data.artist.image[i].size == size) { + $(image).attr('src', data.artist.image[i]['#text'] || defUrl) } } - }}); -} \ No newline at end of file + }}) +} diff --git a/mopidy_musicbox_webclient/static/js/library.js b/mopidy_musicbox_webclient/static/js/library.js index 3a62e05..e91e6bc 100644 --- a/mopidy_musicbox_webclient/static/js/library.js +++ b/mopidy_musicbox_webclient/static/js/library.js @@ -1,167 +1,167 @@ -/********************************* +/** ******************************* * Search *********************************/ -function searchPressed(key) { - var value = $('#searchinput').val(); - switchContent('search'); +function searchPressed (key) { + var value = $('#searchinput').val() + switchContent('search') if (key == 13) { - initSearch(); - return false; + initSearch() + return false } - return true; + return true } -//init search -function initSearch() { - var value = $('#searchinput').val(); - var searchService = $('#selectSearchService').val(); +// init search +function initSearch () { + var value = $('#searchinput').val() + var searchService = $('#selectSearchService').val() if ((value.length < 100) && (value.length > 0)) { - showLoading(true); - //hide ios/android keyboard - document.activeElement.blur(); - $("input").blur(); + showLoading(true) + // hide ios/android keyboard + document.activeElement.blur() + $('input').blur() - delete customTracklists[URI_SCHEME+':allresultscache']; - delete customTracklists[URI_SCHEME+':artistresultscache']; - delete customTracklists[URI_SCHEME+':albumresultscache']; - delete customTracklists[URI_SCHEME+':trackresultscache']; - $("#searchartists").hide(); - $("#searchalbums").hide(); - $("#searchtracks").hide(); + delete customTracklists[URI_SCHEME + ':allresultscache'] + delete customTracklists[URI_SCHEME + ':artistresultscache'] + delete customTracklists[URI_SCHEME + ':albumresultscache'] + delete customTracklists[URI_SCHEME + ':trackresultscache'] + $('#searchartists').hide() + $('#searchalbums').hide() + $('#searchtracks').hide() if (searchService != 'all') { - mopidy.library.search({'query': {any:[value]}, 'uris': [searchService + ':']}).then(processSearchResults, console.error); + mopidy.library.search({'query': {any:[value]}, 'uris': [searchService + ':']}).then(processSearchResults, console.error) } else { mopidy.getUriSchemes().then(function (schemes) { var query = {}, - uris = []; + uris = [] var regexp = $.map(schemes, function (scheme) { - return '^' + scheme + ':'; - }).join('|'); + return '^' + scheme + ':' + }).join('|') - var match = value.match(regexp); + var match = value.match(regexp) if (match) { - var scheme = match[0]; - query = {uri: [value]}; - uris = [scheme]; + var scheme = match[0] + query = {uri: [value]} + uris = [scheme] } else { - query = {any: [value]}; + query = {any: [value]} } - mopidy.library.search({'query': query, 'uris': uris}).then(processSearchResults, console.error); - }); + mopidy.library.search({'query': query, 'uris': uris}).then(processSearchResults, console.error) + }) } } } -/******************************************************** +/** ****************************************************** * process results of a search *********************************************************/ -//# speed clone http://jsperf.com/cloning-an-object/2 -function clone(obj) { - var target = {}; +// # speed clone http://jsperf.com/cloning-an-object/2 +function clone (obj) { + var target = {} for (var i in obj) { if (obj.hasOwnProperty(i)) { - target[i] = obj[i]; + target[i] = obj[i] } } - return target; + return target } -function processSearchResults(resultArr) { - $(SEARCH_TRACK_TABLE).empty(); - $(SEARCH_ARTIST_TABLE).empty(); - $(SEARCH_ALBUM_TABLE).empty(); +function processSearchResults (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 j, emptyResult = true; + var results = {'tracks': [], 'artists': [], 'albums': []} + var j, emptyResult = true for (var 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; + results.tracks.push(resultArr[i].tracks[j]) + emptyResult = false } } if (resultArr[i].artists) { for (j = 0; j < resultArr[i].artists.length; j++) { - results.artists.push(resultArr[i].artists[j]); - emptyResult = false; + 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; + results.albums.push(resultArr[i].albums[j]) + emptyResult = false } } } - customTracklists[URI_SCHEME+':trackresultscache'] = results.tracks; + customTracklists[URI_SCHEME + ':trackresultscache'] = results.tracks if (emptyResult) { - toast('No results'); - showLoading(false); - return false; + toast('No results') + showLoading(false) + return false } if (results.artists.length > 0) { - $("#searchartists").show(); + $('#searchartists').show() } if (results.albums.length > 0) { - $("#searchalbums").show(); + $('#searchalbums').show() } if (results.tracks.length > 0) { - $("#searchtracks").show(); + $('#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)]; - }); + function theme (template, tokens) { + return template.replace(/{[^}]+}/g, function (match) { + return tokens[match.slice(1, -1)] + }) } // 'Show more' pattern - var showMorePattern = '
  • Show {count} more
  • '; + var showMorePattern = '
  • Show {count} more
  • ' // Artist results - var child = ''; - var pattern = '
  • {name}
  • '; - var tokens; + var child = '' + var pattern = '
  • {name}
  • ' + var tokens for (var i = 0; i < results.artists.length; i++) { tokens = { 'id': results.artists[i].uri, 'name': results.artists[i].name, 'class': getMediaClass(results.artists[i].uri) - }; + } // Add 'Show all' item after a certain number of hits. if (i == 4 && results.artists.length > 5) { - child += theme(showMorePattern, {'count': results.artists.length - i}); - pattern = pattern.replace('
  • ', '
  • '); + child += theme(showMorePattern, {'count': results.artists.length - i}) + pattern = pattern.replace('
  • ', '
  • ') } - child += theme(pattern, tokens); + child += theme(pattern, tokens) } // Inject list items, refresh listview and hide superfluous items. - $(SEARCH_ARTIST_TABLE).html(child).listview('refresh').find('.overflow').hide(); + $(SEARCH_ARTIST_TABLE).html(child).listview('refresh').find('.overflow').hide() // Album results - child = ''; - pattern = '
  • '; - pattern += '
    {albumName}
    '; - pattern += '

    {artistName}

    '; - pattern += '
  • '; + child = '' + pattern = '
  • ' + pattern += '
    {albumName}
    ' + pattern += '

    {artistName}

    ' + pattern += '
  • ' for (var i = 0; i < results.albums.length; i++) { tokens = { @@ -170,175 +170,175 @@ function processSearchResults(resultArr) { 'artistName': '', 'albumYear': results.albums[i].date, 'class': getMediaClass(results.albums[i].uri) - }; + } if (results.albums[i].artists) { for (var j = 0; j < results.albums[i].artists.length; j++) { if (results.albums[i].artists[j].name) { - tokens.artistName += results.albums[i].artists[j].name + ' '; + tokens.artistName += results.albums[i].artists[j].name + ' ' } } } if (tokens.albumYear) { - tokens.artistName += '(' + tokens.albumYear + ')'; + tokens.artistName += '(' + tokens.albumYear + ')' } // Add 'Show all' item after a certain number of hits. if (i == 4 && results.albums.length > 5) { - child += theme(showMorePattern, {'count': results.albums.length - i}); - pattern = pattern.replace('
  • ', '
  • '); + child += theme(showMorePattern, {'count': results.albums.length - i}) + pattern = pattern.replace('
  • ', '
  • ') } - child += theme(pattern, tokens); + child += theme(pattern, tokens) } // Inject list items, refresh listview and hide superfluous items. - $(SEARCH_ALBUM_TABLE).html(child).listview('refresh').find('.overflow').hide(); + $(SEARCH_ALBUM_TABLE).html(child).listview('refresh').find('.overflow').hide() - $('#expandsearch').show(); + $('#expandsearch').show() // Track results - resultsToTables(results.tracks, SEARCH_TRACK_TABLE, URI_SCHEME+':trackresultscache'); + resultsToTables(results.tracks, SEARCH_TRACK_TABLE, URI_SCHEME + ':trackresultscache') - showLoading(false); + showLoading(false) } -function toggleSearch() { - $("#albumresulttable tr").removeClass('hidden'); - $("#artistresulttable tr").removeClass('hidden'); +function toggleSearch () { + $('#albumresulttable tr').removeClass('hidden') + $('#artistresulttable tr').removeClass('hidden') } -/********************************* +/** ******************************* * Playlists & Browse *********************************/ -function getPlaylists() { +function getPlaylists () { // get playlists without tracks - mopidy.playlists.asList().then(processGetPlaylists, console.error); + mopidy.playlists.asList().then(processGetPlaylists, console.error) } -function getBrowseDir(rootdir) { +function getBrowseDir (rootdir) { // get directory to browse - showLoading(true); + showLoading(true) if (!rootdir) { - browseStack.pop(); - rootdir = browseStack[browseStack.length - 1]; + browseStack.pop() + rootdir = browseStack[browseStack.length - 1] } else { - browseStack.push(rootdir); + browseStack.push(rootdir) } if (!rootdir) { - rootdir = null; + rootdir = null } - mopidy.library.browse({'uri': rootdir}).then(processBrowseDir, console.error); + mopidy.library.browse({'uri': rootdir}).then(processBrowseDir, console.error) } -function getCurrentPlaylist() { - mopidy.tracklist.getTlTracks().then(processCurrentPlaylist, console.error); +function getCurrentPlaylist () { + mopidy.tracklist.getTlTracks().then(processCurrentPlaylist, console.error) } -/******************************************************** +/** ****************************************************** * Show tracks of playlist ********************************************************/ -function togglePlaylists() { +function togglePlaylists () { if ($(window).width() <= 960) { $('#playlisttracksdiv').toggle(); - //Hide other div - ($('#playlisttracksdiv').is(":visible")) ? $('#playlistslistdiv').hide() : $('#playlistslistdiv').show(); + // Hide other div + ($('#playlisttracksdiv').is(':visible')) ? $('#playlistslistdiv').hide() : $('#playlistslistdiv').show() } else { - $('#playlisttracksdiv').show(); - $('#playlistslistdiv').show(); + $('#playlisttracksdiv').show() + $('#playlistslistdiv').show() } - return true; + return true } -function showTracklist(uri) { - $(PLAYLIST_TABLE).empty(); - togglePlaylists(); - var tracks = getPlaylistTracks(uri).then(function(tracks) { - resultsToTables(tracks, PLAYLIST_TABLE, uri); - }); - showLoading(false); - updatePlayIcons(uri); - $('#playlistslist li a').each(function() { - $(this).removeClass("playlistactive"); +function showTracklist (uri) { + $(PLAYLIST_TABLE).empty() + togglePlaylists() + var tracks = getPlaylistTracks(uri).then(function (tracks) { + resultsToTables(tracks, PLAYLIST_TABLE, uri) + }) + showLoading(false) + updatePlayIcons(uri) + $('#playlistslist li a').each(function () { + $(this).removeClass('playlistactive') if (this.id == uri) { - $(this).addClass('playlistactive'); + $(this).addClass('playlistactive') } - }); - return false; + }) + return false } -/****** +/** **** * Lookups */ -function showArtist(nwuri) { - $('#popupQueue').popup('close'); - $('#popupTracks').popup('close'); - $('#controlsmodal').popup('close'); - $(ARTIST_TABLE).empty(); +function showArtist (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; +// 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 } -function showAlbum(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); +function showAlbum (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); + 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; + // show page + switchContent('albums', uri) + scrollToTop() + return false } -function getSearchSchemes() { +function getSearchSchemes () { mopidy.getUriSchemes().then( - function(schemesArray) { - var humanIndex; - $("#selectSearchService").children().remove().end(); - $("#selectSearchService").append(new Option('All services', 'all')); + function (schemesArray) { + var humanIndex + $('#selectSearchService').children().remove().end() + $('#selectSearchService').append(new Option('All services', 'all')) for (var i = 0; i < schemesArray.length; i++) { for (var j = 0; j < uriHumanList.length; j++) { - if (uriHumanList[j][0] == schemesArray[i].toLowerCase() ) { - $("#selectSearchService").append(new Option(uriHumanList[j][1], schemesArray[i])); + if (uriHumanList[j][0] == schemesArray[i].toLowerCase()) { + $('#selectSearchService').append(new Option(uriHumanList[j][1], schemesArray[i])) } } } - $("#selectSearchService").selectmenu( "refresh", true ); + $('#selectSearchService').selectmenu( 'refresh', true) }, console.error - ); + ) } diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js index 605a398..d6c2711 100644 --- a/mopidy_musicbox_webclient/static/js/process_ws.js +++ b/mopidy_musicbox_webclient/static/js/process_ws.js @@ -5,249 +5,249 @@ * */ -/******************************************************** +/** ****************************************************** * process results of a (new) currently playing track *********************************************************/ -function processCurrenttrack(data) { - setSongInfo(data); +function processCurrenttrack (data) { + setSongInfo(data) } -/******************************************************** +/** ****************************************************** * process results of volume *********************************************************/ -function processVolume(data) { - setVolume(data); +function processVolume (data) { + setVolume(data) } -/******************************************************** +/** ****************************************************** * process results of mute *********************************************************/ -function processMute(data) { - setMute(data); +function processMute (data) { + setMute(data) } -/******************************************************** +/** ****************************************************** * process results of a repeat *********************************************************/ -function processRepeat(data) { - setRepeat(data); +function processRepeat (data) { + setRepeat(data) } -/******************************************************** +/** ****************************************************** * process results of random *********************************************************/ -function processRandom(data) { - setRandom(data); +function processRandom (data) { + setRandom(data) } -/******************************************************** +/** ****************************************************** * process results of consume *********************************************************/ -function processConsume(data) { - setConsume(data); +function processConsume (data) { + setConsume(data) } -/******************************************************** +/** ****************************************************** * process results of single *********************************************************/ -function processSingle(data) { - setSingle(data); +function processSingle (data) { + setSingle(data) } -/******************************************************** +/** ****************************************************** * process results of current position *********************************************************/ -function processCurrentposition(data) { - setPosition(parseInt(data)); +function processCurrentposition (data) { + setPosition(parseInt(data)) } -/******************************************************** +/** ****************************************************** * process results playstate *********************************************************/ -function processPlaystate(data) { +function processPlaystate (data) { if (data == 'playing') { - setPlayState(true); + setPlayState(true) } else { - setPlayState(false); + setPlayState(false) } } -/******************************************************** +/** ****************************************************** * process results of a browse list *********************************************************/ -function processBrowseDir(resultArr) { - var backHtml = '
  • Back

  • '; - if ( (!resultArr) || (resultArr === '') || (resultArr.length === 0) ) { - $('#browsepath').html('No tracks found...'); - $('#browselist').html(backHtml); - showLoading(false); - return; +function processBrowseDir (resultArr) { + var backHtml = '
  • Back

  • ' + if ((!resultArr) || (resultArr === '') || (resultArr.length === 0)) { + $('#browsepath').html('No tracks found...') + $('#browselist').html(backHtml) + showLoading(false) + return } - $('#browselist').empty(); + $('#browselist').empty() - var child = "", rooturi = "", uri = resultArr[0].uri; + var child = '', rooturi = '', uri = resultArr[0].uri - //check root uri - //find last : or / (spltting the result) - //do it twice, since. - var colonindex = uri.lastIndexOf(':'); - var slashindex = uri.lastIndexOf('/'); + // check root uri + // find last : or / (spltting the result) + // do it twice, since. + var colonindex = uri.lastIndexOf(':') + var slashindex = uri.lastIndexOf('/') - var lastindex = (colonindex > slashindex) ? colonindex : slashindex; - rooturi = uri.slice(0, lastindex); - if (resultArr[0].type == 'track' ) { - rooturi = rooturi.replace(":track:", ":directory:"); + var lastindex = (colonindex > slashindex) ? colonindex : slashindex + rooturi = uri.slice(0, lastindex) + if (resultArr[0].type == 'track') { + rooturi = rooturi.replace(':track:', ':directory:') } - colonindex = rooturi.lastIndexOf(':'); - slashindex = rooturi.lastIndexOf('/'); + colonindex = rooturi.lastIndexOf(':') + slashindex = rooturi.lastIndexOf('/') - lastindex = (colonindex > slashindex) ? colonindex : slashindex; - rooturi = rooturi.slice(0, lastindex); + lastindex = (colonindex > slashindex) ? colonindex : slashindex + rooturi = rooturi.slice(0, lastindex) if (browseStack.length > 0) { - child += backHtml; + child += backHtml } - browseTracks = []; + browseTracks = [] for (var i = 0, index = 0; i < resultArr.length; i++) { - iconClass = getMediaClass(resultArr[i].uri); + iconClass = getMediaClass(resultArr[i].uri) if (resultArr[i].type == 'track') { - //console.log(resultArr[i]); + // console.log(resultArr[i]); mopidy.library.lookup({'uris': [resultArr[i].uri]}).then(function (resultDict) { - var lookupUri = Object.keys(resultDict)[0]; - popupData[lookupUri] = resultDict[lookupUri][0]; - browseTracks.push(resultDict[lookupUri][0]); - }, console.error); + var lookupUri = Object.keys(resultDict)[0] + popupData[lookupUri] = resultDict[lookupUri][0] + browseTracks.push(resultDict[lookupUri][0]) + }, console.error) child += '
  • ' + '' + '' + '

    ' + resultArr[i].name + '

  • '; - index++; + '">

    ' + resultArr[i].name + '

    ' + index++ } else { if (browseStack.length > 0) { - iconClass="fa fa-folder-o"; + iconClass = "fa fa-folder-o" } child += '
  • ' + resultArr[i].name + '

  • '; + '"">

    ' + resultArr[i].name + '

    ' } } - $('#browselist').html(child); - if (browseStack.length > 0 ) { - child = getMediaHuman(uri); - iconClass = getMediaClass(uri); - $('#browsepath').html(' ' + child); + $('#browselist').html(child) + if (browseStack.length > 0) { + child = getMediaHuman(uri) + iconClass = getMediaClass(uri) + $('#browsepath').html(' ' + child) } else { - $('#browsepath').html(''); + $('#browsepath').html('') } - updatePlayIcons(songdata.track.uri, songdata.tlid); + updatePlayIcons(songdata.track.uri, songdata.tlid) - showLoading(false); + showLoading(false) } -/******************************************************** +/** ****************************************************** * process results of list of playlists of the user *********************************************************/ -function processGetPlaylists(resultArr) { +function processGetPlaylists (resultArr) { if ((!resultArr) || (resultArr === '')) { - $('#playlistslist').empty(); - return; + $('#playlistslist').empty() + return } - var tmp = '', favourites = '', starred = ''; + var tmp = '', favourites = '', starred = '' for (var i = 0; i < resultArr.length; i++) { - var li_html = '
  • '; - if(isSpotifyStarredPlaylist(resultArr[i])) { - starred = li_html + '★ Spotify Starred Tracks
  • ' + tmp; + var li_html = '
  • ' + if (isSpotifyStarredPlaylist(resultArr[i])) { + starred = li_html + '★ Spotify Starred Tracks
  • ' + tmp } else if (isFavouritesPlaylist(resultArr[i])) { - favourites = li_html + '♥ Musicbox Favourites'; + favourites = li_html + '♥ Musicbox Favourites' } else { - tmp = tmp + li_html + ' ' + resultArr[i].name + ''; + tmp = tmp + li_html + ' ' + resultArr[i].name + '' } } // Prepend the user's Spotify "Starred" playlist and favourites to the results. (like Spotify official client). - tmp = favourites + starred + tmp; - $('#playlistslist').html(tmp); - scrollToTracklist(); - showLoading(false); + tmp = favourites + starred + tmp + $('#playlistslist').html(tmp) + scrollToTracklist() + showLoading(false) } -/******************************************************** +/** ****************************************************** * process results of a returned list of playlist track refs *********************************************************/ -function processPlaylistItems(resultDict) { +function processPlaylistItems (resultDict) { if (resultDict.items.length === 0) { - console.log('Playlist', resultDict.uri, 'is empty'); - showLoading(false); - return; + console.log('Playlist', resultDict.uri, 'is empty') + showLoading(false) + return } - var trackUris = []; + var trackUris = [] for (i = 0; i < resultDict.items.length; i++) { - trackUris.push(resultDict.items[i].uri); + trackUris.push(resultDict.items[i].uri) } - return mopidy.library.lookup({'uris': trackUris}).then(function(tracks) { + return mopidy.library.lookup({'uris': trackUris}).then(function (tracks) { // Transform from dict to list and cache result - var newplaylisturi = resultDict.uri; - playlists[newplaylisturi] = {'uri':newplaylisturi, 'tracks':[]}; + var newplaylisturi = resultDict.uri + playlists[newplaylisturi] = {'uri':newplaylisturi, 'tracks':[]} for (i = 0; i < trackUris.length; i++) { - playlists[newplaylisturi].tracks.push(tracks[trackUris[i]][0]); + playlists[newplaylisturi].tracks.push(tracks[trackUris[i]][0]) } - showLoading(false); - return playlists[newplaylisturi].tracks; - }); + showLoading(false) + return playlists[newplaylisturi].tracks + }) } -/******************************************************** +/** ****************************************************** * process results of the queue, the current playlist *********************************************************/ -function processCurrentPlaylist(resultArr) { - currentplaylist = resultArr; - resultsToTables(currentplaylist, CURRENT_PLAYLIST_TABLE); - mopidy.playback.getCurrentTlTrack().then(processCurrenttrack, console.error); - updatePlayIcons(songdata.track.uri, songdata.tlid); +function processCurrentPlaylist (resultArr) { + currentplaylist = resultArr + resultsToTables(currentplaylist, CURRENT_PLAYLIST_TABLE) + mopidy.playback.getCurrentTlTrack().then(processCurrenttrack, console.error) + updatePlayIcons(songdata.track.uri, songdata.tlid) } -/******************************************************** +/** ****************************************************** * process results of an artist lookup *********************************************************/ -function processArtistResults(resultArr) { +function processArtistResults (resultArr) { if (!resultArr || (resultArr.length === 0)) { - $('#h_artistname').text('Artist not found...'); - getCover('', '#artistviewimage, #artistpopupimage', 'extralarge'); - showLoading(false); - return; + $('#h_artistname').text('Artist not found...') + getCover('', '#artistviewimage, #artistpopupimage', 'extralarge') + showLoading(false) + return } - customTracklists[resultArr.uri] = resultArr; + customTracklists[resultArr.uri] = resultArr - resultsToTables(resultArr, ARTIST_TABLE, resultArr.uri); - var artistname = getArtist(resultArr); - $('#h_artistname, #artistpopupname').html(artistname); - getArtistImage(artistname, '#artistviewimage, #artistpopupimage', 'extralarge'); - showLoading(false); + resultsToTables(resultArr, ARTIST_TABLE, resultArr.uri) + var artistname = getArtist(resultArr) + $('#h_artistname, #artistpopupname').html(artistname) + getArtistImage(artistname, '#artistviewimage, #artistpopupimage', 'extralarge') + showLoading(false) } -/******************************************************** +/** ****************************************************** * process results of an album lookup *********************************************************/ -function processAlbumResults(resultArr) { +function processAlbumResults (resultArr) { if (!resultArr || (resultArr.length === 0)) { - $('#h_albumname').text('Album not found...'); - getCover('', '#albumviewcover, #coverpopupimage', 'extralarge'); - showLoading(false); - return; + $('#h_albumname').text('Album not found...') + getCover('', '#albumviewcover, #coverpopupimage', 'extralarge') + showLoading(false) + return } - customTracklists[resultArr.uri] = resultArr; + customTracklists[resultArr.uri] = resultArr - albumTracksToTable(resultArr, ALBUM_TABLE, resultArr.uri); - var albumname = getAlbum(resultArr); - var artistname = getArtist(resultArr); - $('#h_albumname').html(albumname); - $('#h_albumartist').html(artistname); - $('#coverpopupalbumname').html(albumname); - $('#coverpopupartist').html(artistname); - getCover(resultArr[0].uri, '#albumviewcover, #coverpopupimage', 'extralarge'); - showLoading(false); + albumTracksToTable(resultArr, ALBUM_TABLE, resultArr.uri) + var albumname = getAlbum(resultArr) + var artistname = getArtist(resultArr) + $('#h_albumname').html(albumname) + $('#h_albumartist').html(artistname) + $('#coverpopupalbumname').html(albumname) + $('#coverpopupartist').html(artistname) + getCover(resultArr[0].uri, '#albumviewcover, #coverpopupimage', 'extralarge') + showLoading(false) } diff --git a/mopidy_musicbox_webclient/static/js/progress_timer.js b/mopidy_musicbox_webclient/static/js/progress_timer.js index 3e97fad..0a14116 100644 --- a/mopidy_musicbox_webclient/static/js/progress_timer.js +++ b/mopidy_musicbox_webclient/static/js/progress_timer.js @@ -1,26 +1,26 @@ -var progressTimer; -var progressElement = document.getElementById('trackslider'); -var positionNode = document.createTextNode(''); -var durationNode = document.createTextNode(''); +var progressTimer +var progressElement = document.getElementById('trackslider') +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 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; +var MAX_SYNCS = 5 // Maximum number of consecutive successful syncs to perform. +var syncsLeft = MAX_SYNCS +var synced = false +var consecutiveSyncs = 0 -document.getElementById('songelapsed').appendChild(positionNode); -document.getElementById('songlength').appendChild(durationNode); +document.getElementById('songelapsed').appendChild(positionNode) +document.getElementById('songlength').appendChild(durationNode) -function timerCallback(position, duration, isRunning) { - updateTimers(position, duration, isRunning); +function timerCallback (position, duration, isRunning) { + updateTimers(position, duration, isRunning) if (callbackHeartbeats === 0) { - callbackHeartbeats = getHeartbeat(); + callbackHeartbeats = getHeartbeat() } if (mopidy && position > 0) { @@ -29,102 +29,102 @@ function timerCallback(position, duration, isRunning) { // Get time position from Mopidy on every nth callback until // synced. mopidy.playback.getTimePosition().then( - function(mopidy_position) { - syncTimer(position, mopidy_position); + 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. +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'); + // 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); + durationNode.nodeValue = format(duration || Infinity) if (syncing) { if (!targetPosition) { // Waiting for Mopidy to provide a target position. - positionNode.nodeValue = '(wait)'; + positionNode.nodeValue = '(wait)' } else { // Busy seeking to new target position. - positionNode.nodeValue = '(sync)'; + positionNode.nodeValue = '(sync)' } } else if (synced || streaming) { - positionNode.nodeValue = format(position); + 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'); + $('#trackslider').val(position).slider('refresh') } } -function getHeartbeat() { +function getHeartbeat () { if (syncsLeft > 0 && callbackHeartbeats === 0) { // Step back exponentially while increasing heartbeat. - return Math.round(delay_exponential(5, 2, MAX_SYNCS - syncsLeft)); + 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; + return RUN_BEATS } else { - return START_BEATS; + return START_BEATS } } -function syncTimer(current, target) { +function syncTimer (current, target) { if (target) { - var drift = Math.abs(target - current); + var drift = Math.abs(target - current) if (drift <= 500) { - syncsLeft--; + 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; + synced = true + targetPosition = null + consecutiveSyncs = 0 } } else { // Drift is too large, re-sync with Mopidy. - reset(); - targetPosition = target; - progressTimer.set(targetPosition); + reset() + targetPosition = target + progressTimer.set(targetPosition) } } } -function toInt(value) { - return value.match(/^\w*\d+\w*$/) ? parseInt(value) : null; +function toInt (value) { + return value.match(/^\w*\d+\w*$/) ? parseInt(value) : null } -function format(milliseconds) { +function format (milliseconds) { if (milliseconds === Infinity) { - return '(n/a)'; + return '(n/a)' } else if (milliseconds === 0) { - return '0:00'; + return '0:00' } - var seconds = Math.floor(milliseconds / 1000); - var minutes = Math.floor(seconds / 60); - seconds = seconds % 60; + var seconds = Math.floor(milliseconds / 1000) + var minutes = Math.floor(seconds / 60) + seconds = seconds % 60 - seconds = seconds < 10 ? '0' + seconds : seconds; - return minutes + ':' + seconds; + 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. +function delay_exponential (base, growthFactor, attempts) { + /* Calculate number of beats between syncs based on exponential function. The format is:: base * growthFactor ^ (attempts - 1) @@ -134,43 +134,43 @@ function delay_exponential(base, growthFactor, attempts) { Base must be greater than 0. */ if (base == 'rand') { - base = Math.random(); + base = Math.random() } - beats = base * (Math.pow(growthFactor, (attempts - 1))); - return beats; + beats = base * (Math.pow(growthFactor, (attempts - 1))) + return beats } -function reset() { - synced = false; - consecutiveSyncs = 0; - syncsLeft = MAX_SYNCS; - callbackHeartbeats = START_BEATS; - targetPosition = null; +function reset () { + synced = false + consecutiveSyncs = 0 + syncsLeft = MAX_SYNCS + callbackHeartbeats = START_BEATS + targetPosition = null } -function setProgressTimer(pos) { - reset(); - targetPosition = pos; - progressTimer.set(pos); +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'); + positionNode.nodeValue = format(pos) + $('#trackslider').val(pos).slider('refresh') } } -function updatePosition(pos) { - positionNode.nodeValue = format(pos); +function updatePosition (pos) { + positionNode.nodeValue = format(pos) } -function startProgressTimer() { - reset(); - progressTimer.start(); +function startProgressTimer () { + reset() + progressTimer.start() } -function resetProgressTimer() { - progressTimer.reset(); - reset(); - targetPosition = 0; +function resetProgressTimer () { + progressTimer.reset() + reset() + targetPosition = 0 } From 979dc028c788d776078d2eb67d82b480a429a7e2 Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 5 Mar 2016 15:49:48 +0200 Subject: [PATCH 04/38] fox:Manual eslint rule violation fixes. --- .../static/js/controls.js | 50 +++-- .../static/js/functionsvars.js | 63 +++---- mopidy_musicbox_webclient/static/js/gui.js | 172 +++++++++--------- mopidy_musicbox_webclient/static/js/images.js | 10 +- .../static/js/library.js | 31 ++-- .../static/js/process_ws.js | 18 +- .../static/js/progress_timer.js | 10 +- mopidy_musicbox_webclient/static/mb.appcache | 2 +- 8 files changed, 179 insertions(+), 177 deletions(-) diff --git a/mopidy_musicbox_webclient/static/js/controls.js b/mopidy_musicbox_webclient/static/js/controls.js index daec3cc..3db4a6d 100644 --- a/mopidy_musicbox_webclient/static/js/controls.js +++ b/mopidy_musicbox_webclient/static/js/controls.js @@ -8,7 +8,7 @@ function playBrowsedTracks (action, trackIndex) { if (typeof trackIndex === 'undefined') { trackIndex = $('#popupBrowse').data('tlid') } - if (action == PLAY_ALL) { + if (action === PLAY_ALL) { mopidy.tracklist.clear() // Default for radio streams is to just add the selected URI. if (isStreamUri(browseTracks[trackIndex].uri)) { @@ -62,8 +62,8 @@ function playTrack (action) { var divid = hash[0].substr(1) // Search page default click behaviour adds and plays selected track only. - if (action == PLAY_NOW && divid == 'search') { - action = PLAY_NOW_SEARCH + if (action === PLAY_NOW && divid === 'search') { + action === PLAY_NOW_SEARCH } $('#popupTracks').popup('close') @@ -76,7 +76,7 @@ function playTrack (action) { var trackUris = getTracksFromUri(playlisturi) // find track that was selected for (var selected = 0; selected < trackUris.length; selected++) { - if (trackUris[selected] == uri) { + if (trackUris[selected] === uri) { break } } @@ -121,7 +121,7 @@ function playTrackByUri (track_uri, playlist_uri) { if (tlTracks.length > 0) { // Find track that was selected for (var selected = 0; selected < tlTracks.length; selected++) { - if (tlTracks[selected].track.uri == track_uri) { + if (tlTracks[selected].track.uri === track_uri) { mopidy.playback.play({'tl_track': tlTracks[selected]}) return } @@ -214,7 +214,7 @@ function clearQueue () { } function savePressed (key) { - if (key == 13) { + if (key === 13) { saveQueue() return false } @@ -228,7 +228,6 @@ function showSavePopup () { $('#popupSave').popup('open') } }) - } function saveQueue () { @@ -335,33 +334,33 @@ function backbt () { *************/ function setTracklistOption (name, new_value) { if (!new_value) { - $("#" + name+'bt').attr('style', 'color:#2489ce') + $('#' + name + 'bt').attr('style', 'color:#2489ce') } else { - $("#" + name+'bt').attr('style', 'color:#66DD33') + $('#' + name + 'bt').attr('style', 'color:#66DD33') } return new_value } function setRepeat (nwrepeat) { - if (repeat != nwrepeat) { + if (repeat !== nwrepeat) { repeat = setTracklistOption('repeat', nwrepeat) } } function setRandom (nwrandom) { - if (random != nwrandom) { + if (random !== nwrandom) { random = setTracklistOption('random', nwrandom) } } function setConsume (nwconsume) { - if (consume != nwconsume) { + if (consume !== nwconsume) { consume = setTracklistOption('consume', nwconsume) } } function setSingle (nwsingle) { - if (single != nwsingle) { + if (single !== nwsingle) { single = setTracklistOption('single', nwsingle) } } @@ -382,7 +381,6 @@ function doSingle () { mopidy.tracklist.setSingle({'value': !single}).then() } - /** ********************************************* * Track Slider * * Use a timer to prevent looping of commands * @@ -397,7 +395,7 @@ function doSeekPos (value) { } function setPosition (pos) { - if (!positionChanging && $('#trackslider').val() != pos) { + if (!positionChanging && $('#trackslider').val() !== pos) { setProgressTimer(pos) } } @@ -408,10 +406,10 @@ function setPosition (pos) { ***********************************************/ function setVolume (value) { - if (!volumeChanging && !volumeSliding && $('#volumeslider').val() != value) { - $("#volumeslider").off( 'change') - $("#volumeslider").val(value).slider('refresh') - $("#volumeslider").on( 'change', function () { doVolume($(this).val()) }) + if (!volumeChanging && !volumeSliding && $('#volumeslider').val() !== value) { + $('#volumeslider').off('change') + $('#volumeslider').val(value).slider('refresh') + $('#volumeslider').on('change', function () { doVolume($(this).val()) }) } } @@ -425,7 +423,7 @@ function doVolume (value) { } function setMute (nwmute) { - if (mute != nwmute) { + if (mute !== nwmute) { mute = nwmute if (mute) { $('#mutebt').attr('class', 'fa fa-volume-off') @@ -443,7 +441,7 @@ function doMute () { * Stream * ************/ function streamPressed (key) { - if (key == 13) { + if (key === 13) { playStreamUri() return false } @@ -530,8 +528,8 @@ function getFavourites () { STREAMS_PLAYLIST_SCHEME, true).then(function (playlist) { if (playlist) { - return getPlaylistFull(playlist.uri) - } + return getPlaylistFull(playlist.uri) + } return Mopidy.when(false) }) } @@ -552,11 +550,11 @@ function addToFavourites (newTracks) { } function addFavourite (uri, name) { - var uri = uri || $('#streamuriinput').val().trim() - var name = name || $('#streamnameinput').val().trim() + uri = uri || $('#streamuriinput').val().trim() + name = name || $('#streamnameinput').val().trim() mopidy.library.lookup({'uris': [uri]}).then(function (results) { var newTracks = results[uri] - if (newTracks.length == 1) { + if (newTracks.length === 1) { // TODO: Supporting adding an entire playlist? if (name) { newTracks[0].name = name // User overrides name. diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index 1b8f060..9d82bf3 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -42,11 +42,11 @@ var customTracklists = [] var browseStack = [] var browseTracks = [] -var ua = navigator.userAgent, - isMobileSafari = /Mac/.test(ua) && /Mobile/.test(ua), - isMobileWebkit = /WebKit/.test(ua) && /Mobile/.test(ua), - isMobile = /Mobile/.test(ua), - isWebkit = /WebKit/.test(ua) +var ua = navigator.userAgent +var isMobileSafari = /Mac/.test(ua) && /Mobile/.test(ua) +var isMobileWebkit = /WebKit/.test(ua) && /Mobile/.test(ua) +var isMobile = /Mobile/.test(ua) +var isWebkit = /WebKit/.test(ua) // constants PROGRAM_NAME = 'MusicBox' @@ -215,7 +215,7 @@ function resultsToTables (results, target, uri) { return } var tlids = [] - if (target == CURRENT_PLAYLIST_TABLE) { + if (target === CURRENT_PLAYLIST_TABLE) { for (i = 0; i < results.length; i++) { tlids[i] = results[i].tlid results[i] = results[i].track @@ -255,7 +255,7 @@ function resultsToTables (results, target, uri) { } // leave out unplayable items - if (results[i].name.substring(0, 12) == '[unplayable]') continue + if (results[i].name.substring(0, 12) === '[unplayable]') continue newalbum.push(results[i]) newtlids.push(tlids[i]) @@ -263,39 +263,40 @@ function resultsToTables (results, target, uri) { if ((i < length - 1) && results[i + 1].album && results[i + 1].album.name) { nextname = results[i + 1].album.name } - if (results[i].length == -1) { + if (results[i].length === -1) { html += '
  • ' + results[i].name + ' [Stream]

  • ' newalbum = [] newtlids = [] nextname = '' } else { - if ((results[i].album.name != nextname) || (nextname === '')) { + if ((results[i].album.name !== nextname) || (nextname === '')) { tableid = 'art' + i // render differently if only one track in the album - if (newalbum.length == 1) { + if (newalbum.length === 1) { + var liID = '' if (i !== 0) { html += '
  •  
  • ' } iconClass = getMediaClass(newalbum[0].uri) - var liID = targetmin + '-' + newalbum[0].uri - if (target == CURRENT_PLAYLIST_TABLE) { + liID = targetmin + '-' + newalbum[0].uri + if (target === CURRENT_PLAYLIST_TABLE) { html += '
  • ' + '' + '' + '' + - '

    ' + newalbum[0].name + "

    " + '

    ' + newalbum[0].name + '

    ' } else { html += '

  • ' + '' + '' + '' + - '

    ' + newalbum[0].name + "

    " + '

    ' + newalbum[0].name + '

    ' } if (newalbum[0].artists) { for (j = 0; j < newalbum[0].artists.length; j++) { html += newalbum[0].artists[j].name - html += (j == newalbum[0].artists.length - 1) ? '' : ' / ' + html += (j === newalbum[0].artists.length - 1) ? '' : ' / ' // stop after 3 if (j > 2) { html += '...' @@ -322,7 +323,7 @@ function resultsToTables (results, target, uri) { if (results[i].album.artists) { for (j = 0; j < results[i].album.artists.length; j++) { html += results[i].album.artists[j].name - html += (j == results[i].album.artists.length - 1) ? '' : ' / ' + html += (j === results[i].album.artists.length - 1) ? '' : ' / ' // stop after 3 if (j > 2) { child += '...' @@ -334,8 +335,8 @@ function resultsToTables (results, target, uri) { for (j = 0; j < newalbum.length; j++) { popupData[newalbum[j].uri] = newalbum[j] // hERE! - var liID = targetmin + '-' + newalbum[j].uri - if (target == CURRENT_PLAYLIST_TABLE) { + liID = targetmin + '-' + newalbum[j].uri + if (target === CURRENT_PLAYLIST_TABLE) { html += renderQueueSongLi(newalbum[j], liID, uri, newtlids[j]) } else { html += renderSongLi(newalbum[j], liID, uri) @@ -369,13 +370,13 @@ function playlisttotable (pl, target, uri) { if (pl[i]) { popupData[pl[i].uri] = pl[i] child = '

  • ' - child += '

    ' + pl[i].name + "

    " + child += '

    ' + pl[i].name + 'h1>' child += '

    ' child += '' + timeFromSeconds(pl[i].length / 1000) + '' for (var j = 0; j < pl[i].artists.length; j++) { if (pl[i].artists[j]) { child += pl[i].artists[j].name - child += (j == pl[i].artists.length - 1) ? '' : ' / ' + child += (j === pl[i].artists.length - 1) ? '' : ' / ' // stop after 3 if (j > 2) { child += '...' @@ -436,7 +437,7 @@ function timeFromSeconds (length) { /** ***** Toast ***/ function toast (message, delay, textOnly) { textOnl = textOnly || false - message = message || "Loading..." + message = message || 'Loading...' delay = delay || 1000 $.mobile.loading('show', { text: message, @@ -480,7 +481,6 @@ function showOffline (on) { } } - // from http://dzone.com/snippets/validate-url-regexp function validUri (str) { var regexp = /^(mms|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/ @@ -504,7 +504,7 @@ function isStreamUri (uri) { function getMediaClass (uri) { var scheme = getScheme(uri) for (var i = 0; i < uriClassList.length; i++) { - if (scheme == uriClassList[i][0]) { + if (scheme === uriClassList[i][0]) { return 'fa ' + uriClassList[i][1] } } @@ -514,7 +514,7 @@ function getMediaClass (uri) { function getMediaHuman (uri) { var scheme = getScheme(uri) for (var i = 0; i < uriHumanList.length; i++) { - if (scheme == uriHumanList[i][0]) { + if (scheme === uriHumanList[i][0]) { return uriHumanList[i][1] } } @@ -523,13 +523,14 @@ function getMediaHuman (uri) { function isServiceUri (uri) { var scheme = getScheme(uri) - for (var i = 0; i < uriClassList.length; i++) { - if (scheme == uriClassList[i][0]) { + var i = 0 + for (i = 0; i < uriClassList.length; i++) { + if (scheme === uriClassList[i][0]) { return true } } - for (var i = 0; i < radioExtensionsList.length; i++) { - if (scheme == radioExtensionsList[i]) { + for (i = 0; i < radioExtensionsList.length; i++) { + if (scheme === radioExtensionsList[i]) { return true } } @@ -537,11 +538,11 @@ function isServiceUri (uri) { } function isFavouritesPlaylist (playlist) { - return (playlist.name == STREAMS_PLAYLIST_NAME && - getScheme(playlist.uri) == STREAMS_PLAYLIST_SCHEME) + return (playlist.name === STREAMS_PLAYLIST_NAME && + getScheme(playlist.uri) === STREAMS_PLAYLIST_SCHEME) } function isSpotifyStarredPlaylist (playlist) { var starredRegex = /spotify:user:.*:starred/g - return (starredRegex.test(playlist.uri) && playlist.name == 'Starred') + return (starredRegex.test(playlist.uri) && playlist.name === 'Starred') } diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index 4827d1c..1654123 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -9,9 +9,9 @@ function resetSong () { setPlayState(false) setPosition(0) - var data = new Object({}) + var data = {} data.tlid = -1 - data.track = new Object({}) + data.track = {} data.track.name = '' data.track.artists = '' data.track.length = 0 @@ -60,7 +60,6 @@ function resizeMb () { $('#playlistslistdiv').show(); } - if (isMobileWebkit && ($(window).width() > 480)) { playlistslistScroll.refresh(); playlisttracksScroll.refresh(); @@ -79,7 +78,7 @@ function setSongTitle (title, refresh_ui) { function setSongInfo (data) { // console.log(data, songdata); if (!data) { return } - if (data.tlid == songdata.tlid) { return } + if (data.tlid === songdata.tlid) { return } if (!data.track.name || data.track.name === '') { var name = data.track.uri.split('/') data.track.name = decodeURI(name[name.length - 1]) @@ -92,7 +91,7 @@ function setSongInfo (data) { if (validUri(data.track.name)) { for (var key in streamUris) { rs = streamUris[key] - if (rs && rs[1] == data.track.name) { + if (rs && rs[1] === data.track.name) { data.track.name = (rs[0] || rs[1]) } } @@ -117,14 +116,14 @@ function setSongInfo (data) { var arttmp = '' if (data.track.artists) { - for (var j = 0; j < data.track.artists.length; j++) { - artistshtml += '' + data.track.artists[j].name + '' - artiststext += data.track.artists[j].name - if (j != data.track.artists.length - 1) { - artistshtml += ', ' - artiststext += ', ' - } - } + for (var j = 0; j < data.track.artists.length; j++) { + artistshtml += '' + data.track.artists[j].name + '' + artiststext += data.track.artists[j].name + if (j !== data.track.artists.length - 1) { + artistshtml += ', ' + artiststext += ', ' + } + } arttmp = artistshtml } if (data.track.album && data.track.album.name) { @@ -161,14 +160,15 @@ function closePopups () { } function popupTracks (e, listuri, trackuri, tlid) { - if (!e) + if (!e) { e = window.event + } $('.popupTrackName').html(popupData[trackuri].name) $('.popupAlbumName').html(popupData[trackuri].album.name) - var child = "" + var child = '' if (popupData[trackuri].artists) { - if (popupData[trackuri].artists.length == 1) { + if (popupData[trackuri].artists.length === 1) { child = 'Show Artist' $('.popupArtistName').html(popupData[trackuri].artists[0].name) $('.popupArtistHref').attr('onclick', 'showArtist("' + popupData[trackuri].artists[0].uri + '");') @@ -192,10 +192,10 @@ function popupTracks (e, listuri, trackuri, tlid) { var hash = document.location.hash.split('?') var divid = hash[0].substr(1) var popupName = '' - if (divid == 'current') { + if (divid === 'current') { $('.addqueue').hide() popupName = '#popupQueue' - } else if (divid == 'browse') { + } else if (divid === 'browse') { $('.addqueue').show() popupName = '#popupBrowse' } else { @@ -203,15 +203,15 @@ function popupTracks (e, listuri, trackuri, tlid) { popupName = '#popupTracks' } - if (typeof tlid != 'undefined' && tlid !== '') { + if (typeof tlid !== 'undefined' && tlid !== '') { $(popupName).data('list', listuri).data('track', trackuri).data('tlid', tlid).popup('open', { - x : e.pageX, - y : e.pageY + x: e.pageX, + y: e.pageY }) } else { $(popupName).data('list', listuri).data('track', trackuri).popup('open', { - x : e.pageX, - y : e.pageY + x: e.pageX, + y: e.pageY }) } @@ -311,7 +311,6 @@ function initSocketevents () { $(document).bind('pageinit', function () { resizeMb() - }) /** ************ @@ -392,7 +391,6 @@ function locationHashChanged () { var divid = hash[0].substr(1) setHeadline(divid) - var uri = hash[1] $('.mainNav a').removeClass('ui-state-active ui-state-persist ui-btn-active') // i don't know why some li elements have those classes, but they do, so we need to remove them @@ -470,7 +468,7 @@ $(document).ready(function (event) { } // workaround for a bug in jQuery Mobile, without that the panel doesn't close on mobile devices... - $('.ui-panel-dismiss').on( 'tap', function () { $('#panel').panel('close') }) + $('.ui-panel-dismiss').on('tap', function () { $('#panel').panel('close') }) // end of workaround $(window).hashchange() @@ -489,16 +487,15 @@ $(document).ready(function (event) { try { mopidy = new Mopidy({callingConvention: 'by-position-or-by-name'}) } catch (e) { - showOffline(true) - } + showOffline(true) + } } // initialize events initSocketevents() progressTimer = new ProgressTimer({ - callback: timerCallback, - // updateRate: 2000, + callback: timerCallback }) resetSong() @@ -535,9 +532,9 @@ $(document).ready(function (event) { // event handlers for full screen mode $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange, MSFullscreenChange', function (e) { if (isFullscreen()) { - document.getElementById('toggletxt').innerHTML = "Exit Fullscreen" + document.getElementById('toggletxt').innerHTML = 'Exit Fullscreen' } else { - document.getElementById('toggletxt').innerHTML = "Fullscreen" + document.getElementById('toggletxt').innerHTML = 'Fullscreen' } }) @@ -560,28 +557,27 @@ $(document).ready(function (event) { $(document).keypress(function (event) { // console.log('kp: '+event); - if (event.target.tagName != 'INPUT') { - var unicode = event.keyCode ? event.keyCode : event.charCode - var actualkey = String.fromCharCode(unicode) - switch (actualkey) { - case ' ': - doPlay() - event.preventDefault() - break - case '>': - doNext() - event.preventDefault() - break - case '<': - doPrevious() - event.preventDefault() - break - } - return true - } + if (event.target.tagName !== 'INPUT') { + var unicode = event.keyCode ? event.keyCode : event.charCode + var actualkey = String.fromCharCode(unicode) + switch (actualkey) { + case ' ': + doPlay() + event.preventDefault() + break + case '>': + doNext() + event.preventDefault() + break + case '<': + doPrevious() + event.preventDefault() + break + } + return true + } }) - if ($(window).width() < 980) { $('#panel').panel('close') } else { @@ -593,84 +589,86 @@ $(document).ready(function (event) { $.event.special.swipe.durationThreshold = 500 // swipe songinfo and panel - $("#normalFooter, #nowPlayingFooter").on( 'swiperight', doPrevious) - $("#normalFooter, #nowPlayingFooter").on( 'swipeleft', doNext) - $("#nowPlayingpane, .ui-body-c, #header, #panel, .pane").on( 'swiperight', function () { - if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) { - $('#panel').panel('open') - event.stopImmediatePropagation() } - }) - $("#nowPlayingpane, .ui-body-c, #header, #panel, .pane").on( 'swipeleft', function () { - if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) { - $('#panel').panel('close') - event.stopImmediatePropagation() } - }) - - $("#trackslider").on( 'slidestart', function () { - progressTimer.stop() - $("#trackslider").on( 'change', function () { updatePosition($(this).val()) }) + $('#normalFooter, #nowPlayingFooter').on('swiperight', doPrevious) + $('#normalFooter, #nowPlayingFooter').on('swipeleft', doNext) + $('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swiperight', function () { + if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) { + $('#panel').panel('open') + event.stopImmediatePropagation() + } + }) + $('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swipeleft', function () { + if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) { + $('#panel').panel('close') + event.stopImmediatePropagation() + } }) - $("#trackslider").on( 'slidestop', function () { - $("#trackslider").off( 'change') + $('#trackslider').on('slidestart', function () { + progressTimer.stop() + $('#trackslider').on('change', function () { updatePosition($(this).val()) }) + }) + + $('#trackslider').on('slidestop', function () { + $('#trackslider').off('change') doSeekPos($(this).val()) }) - $("#volumeslider").on( 'slidestart', function () { volumeSliding = true }) - $("#volumeslider").on( 'slidestop', function () { volumeSliding = false }) - $("#volumeslider").on( 'change', function () { doVolume($(this).val()) }) + $('#volumeslider').on('slidestart', function () { volumeSliding = true }) + $('#volumeslider').on('slidestop', function () { volumeSliding = false }) + $('#volumeslider').on('change', function () { doVolume($(this).val()) }) }) function updatePlayIcons (uri, tlid) { // update styles of listviews $('#currenttable li').each(function () { var eachTlid = $(this).attr('tlid') - if (typeof eachTlid != 'undefined') { + if (typeof eachTlid !== 'undefined') { eachTlid = parseInt(eachTlid) } - if (this.id == 'currenttable-' + uri && eachTlid == tlid) { + if (this.id === 'currenttable-' + uri && eachTlid === tlid) { $(this).addClass('currenttrack') } else { $(this).removeClass('currenttrack') - } + } }) $('#playlisttracks li').each(function () { - if (this.id == 'playlisttracks-' + uri) { + if (this.id === 'playlisttracks-' + uri) { $(this).addClass('currenttrack2') } else { $(this).removeClass('currenttrack2') - } + } }) $('#trackresulttable li').each(function () { - if (this.id == 'trackresulttable-' + uri) { + if (this.id === 'trackresulttable-' + uri) { $(this).addClass('currenttrack2') } else { - $(this).removeClass('currenttrack2') - } + $(this).removeClass('currenttrack2') + } }) $('#artiststable li').each(function () { - if (this.id == 'artiststable-' + uri) { + if (this.id === 'artiststable-' + uri) { $(this).addClass('currenttrack2') } else { $(this).removeClass('currenttrack2') - } + } }) $('#albumstable li').each(function () { - if (this.id == 'albumstable-' + uri) { + if (this.id === 'albumstable-' + uri) { $(this).addClass('currenttrack2') } else { $(this).removeClass('currenttrack2') - } + } }) $('#browselist li').each(function () { - if (this.id == 'browselisttracks-' + uri) { + if (this.id === 'browselisttracks-' + uri) { $(this).addClass('currenttrack2') } else { $(this).removeClass('currenttrack2') - } + } }) } diff --git a/mopidy_musicbox_webclient/static/js/images.js b/mopidy_musicbox_webclient/static/js/images.js index 8cb61dc..d8c6627 100644 --- a/mopidy_musicbox_webclient/static/js/images.js +++ b/mopidy_musicbox_webclient/static/js/images.js @@ -13,9 +13,9 @@ $(window).load(function () { fmcache = new LastFMCache() // create a LastFM object lastfm = new LastFM({ - apiKey : API_KEY, - apiSecret : API_SECRET, - cache : fmcache + apiKey: API_KEY, + apiSecret: API_SECRET, + cache: fmcache }) }) @@ -72,7 +72,7 @@ function getCoverFromLastFm (track, images, size) { 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) { + if (data.album.image[i].size === size) { $(images).attr('src', data.album.image[i]['#text'] || defUrl) } } @@ -84,7 +84,7 @@ function getArtistImage (nwartist, image, size) { var defUrl = 'images/user_24x32.png' lastfm.artist.getInfo({artist: nwartist}, {success: function (data) { for (var i = 0; i < data.artist.image.length; i++) { - if (data.artist.image[i].size == size) { + if (data.artist.image[i].size === size) { $(image).attr('src', data.artist.image[i]['#text'] || defUrl) } } diff --git a/mopidy_musicbox_webclient/static/js/library.js b/mopidy_musicbox_webclient/static/js/library.js index e91e6bc..9cba433 100644 --- a/mopidy_musicbox_webclient/static/js/library.js +++ b/mopidy_musicbox_webclient/static/js/library.js @@ -5,7 +5,7 @@ function searchPressed (key) { var value = $('#searchinput').val() switchContent('search') - if (key == 13) { + if (key === 13) { initSearch() return false } @@ -31,12 +31,12 @@ function initSearch () { $('#searchalbums').hide() $('#searchtracks').hide() - if (searchService != 'all') { - mopidy.library.search({'query': {any:[value]}, 'uris': [searchService + ':']}).then(processSearchResults, console.error) + if (searchService !== 'all') { + mopidy.library.search({'query': {any: [value]}, 'uris': [searchService + ':']}).then(processSearchResults, console.error) } else { mopidy.getUriSchemes().then(function (schemes) { - var query = {}, - uris = [] + var query = {} + var uris = [] var regexp = $.map(schemes, function (scheme) { return '^' + scheme + ':' @@ -79,9 +79,10 @@ function processSearchResults (resultArr) { // Merge results from different backends. // TODO should of coures have multiple tables var results = {'tracks': [], 'artists': [], 'albums': []} - var j, emptyResult = true + var i, j + var emptyResult = true - for (var i = 0; i < resultArr.length; i++) { + 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]) @@ -137,7 +138,7 @@ function processSearchResults (resultArr) { var pattern = '

  • {name}
  • ' var tokens - for (var i = 0; i < results.artists.length; i++) { + for (i = 0; i < results.artists.length; i++) { tokens = { 'id': results.artists[i].uri, 'name': results.artists[i].name, @@ -145,7 +146,7 @@ function processSearchResults (resultArr) { } // Add 'Show all' item after a certain number of hits. - if (i == 4 && results.artists.length > 5) { + if (i === 4 && results.artists.length > 5) { child += theme(showMorePattern, {'count': results.artists.length - i}) pattern = pattern.replace('
  • ', '
  • ') } @@ -163,7 +164,7 @@ function processSearchResults (resultArr) { pattern += '

    {artistName}

    ' pattern += '
  • ' - for (var i = 0; i < results.albums.length; i++) { + for (i = 0; i < results.albums.length; i++) { tokens = { 'albumId': results.albums[i].uri, 'albumName': results.albums[i].name, @@ -172,7 +173,7 @@ function processSearchResults (resultArr) { 'class': getMediaClass(results.albums[i].uri) } if (results.albums[i].artists) { - for (var j = 0; j < results.albums[i].artists.length; j++) { + 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 + ' ' } @@ -182,7 +183,7 @@ function processSearchResults (resultArr) { tokens.artistName += '(' + tokens.albumYear + ')' } // Add 'Show all' item after a certain number of hits. - if (i == 4 && results.albums.length > 5) { + if (i === 4 && results.albums.length > 5) { child += theme(showMorePattern, {'count': results.albums.length - i}) pattern = pattern.replace('
  • ', '
  • ') } @@ -258,7 +259,7 @@ function showTracklist (uri) { updatePlayIcons(uri) $('#playlistslist li a').each(function () { $(this).removeClass('playlistactive') - if (this.id == uri) { + if (this.id === uri) { $(this).addClass('playlistactive') } }) @@ -333,12 +334,12 @@ function getSearchSchemes () { $('#selectSearchService').append(new Option('All services', 'all')) for (var i = 0; i < schemesArray.length; i++) { for (var j = 0; j < uriHumanList.length; j++) { - if (uriHumanList[j][0] == schemesArray[i].toLowerCase()) { + if (uriHumanList[j][0] === schemesArray[i].toLowerCase()) { $('#selectSearchService').append(new Option(uriHumanList[j][1], schemesArray[i])) } } } - $('#selectSearchService').selectmenu( 'refresh', true) + $('#selectSearchService').selectmenu('refresh', true) }, console.error ) } diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js index d6c2711..c1279c3 100644 --- a/mopidy_musicbox_webclient/static/js/process_ws.js +++ b/mopidy_musicbox_webclient/static/js/process_ws.js @@ -65,7 +65,7 @@ function processCurrentposition (data) { * process results playstate *********************************************************/ function processPlaystate (data) { - if (data == 'playing') { + if (data === 'playing') { setPlayState(true) } else { setPlayState(false) @@ -86,7 +86,9 @@ function processBrowseDir (resultArr) { $('#browselist').empty() - var child = '', rooturi = '', uri = resultArr[0].uri + var child = '' + var rooturi = '' + var uri = resultArr[0].uri // check root uri // find last : or / (spltting the result) @@ -96,7 +98,7 @@ function processBrowseDir (resultArr) { var lastindex = (colonindex > slashindex) ? colonindex : slashindex rooturi = uri.slice(0, lastindex) - if (resultArr[0].type == 'track') { + if (resultArr[0].type === 'track') { rooturi = rooturi.replace(':track:', ':directory:') } colonindex = rooturi.lastIndexOf(':') @@ -112,7 +114,7 @@ function processBrowseDir (resultArr) { browseTracks = [] for (var i = 0, index = 0; i < resultArr.length; i++) { iconClass = getMediaClass(resultArr[i].uri) - if (resultArr[i].type == 'track') { + if (resultArr[i].type === 'track') { // console.log(resultArr[i]); mopidy.library.lookup({'uris': [resultArr[i].uri]}).then(function (resultDict) { var lookupUri = Object.keys(resultDict)[0] @@ -127,7 +129,7 @@ function processBrowseDir (resultArr) { index++ } else { if (browseStack.length > 0) { - iconClass = "fa fa-folder-o" + iconClass = 'fa fa-folder-o' } child += '
  • ' + resultArr[i].name + '

  • ' @@ -156,7 +158,9 @@ function processGetPlaylists (resultArr) { $('#playlistslist').empty() return } - var tmp = '', favourites = '', starred = '' + var tmp = '' + var favourites = '' + var starred = '' for (var i = 0; i < resultArr.length; i++) { var li_html = '
  • ' @@ -191,7 +195,7 @@ function processPlaylistItems (resultDict) { return mopidy.library.lookup({'uris': trackUris}).then(function (tracks) { // Transform from dict to list and cache result var newplaylisturi = resultDict.uri - playlists[newplaylisturi] = {'uri':newplaylisturi, 'tracks':[]} + playlists[newplaylisturi] = {'uri': newplaylisturi, 'tracks': []} for (i = 0; i < trackUris.length; i++) { playlists[newplaylisturi].tracks.push(tracks[trackUris[i]][0]) } diff --git a/mopidy_musicbox_webclient/static/js/progress_timer.js b/mopidy_musicbox_webclient/static/js/progress_timer.js index 0a14116..0b371c7 100644 --- a/mopidy_musicbox_webclient/static/js/progress_timer.js +++ b/mopidy_musicbox_webclient/static/js/progress_timer.js @@ -25,7 +25,7 @@ function timerCallback (position, duration, isRunning) { if (mopidy && position > 0) { // Mopidy and timer are both initialized. - if (callbackHeartbeats-- == 1) { + if (callbackHeartbeats-- === 1) { // Get time position from Mopidy on every nth callback until // synced. mopidy.playback.getTimePosition().then( @@ -38,8 +38,8 @@ function timerCallback (position, duration, isRunning) { } 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 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. @@ -88,7 +88,7 @@ function syncTimer (current, target) { if (drift <= 500) { syncsLeft-- // Less than 500ms == in sync. - if (++consecutiveSyncs == 2) { + if (++consecutiveSyncs === 2) { // Need at least two consecutive syncs to know that Mopidy // is progressing playback and we are in sync. synced = true @@ -133,7 +133,7 @@ function delay_exponential (base, growthFactor, attempts) { 0 and 1 will be used as the base. Base must be greater than 0. */ - if (base == 'rand') { + if (base === 'rand') { base = Math.random() } beats = base * (Math.pow(growthFactor, (attempts - 1))) diff --git a/mopidy_musicbox_webclient/static/mb.appcache b/mopidy_musicbox_webclient/static/mb.appcache index 0b24062..27d0536 100644 --- a/mopidy_musicbox_webclient/static/mb.appcache +++ b/mopidy_musicbox_webclient/static/mb.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# 2016-02-29:v1 +# 2016-03-05:v1 NETWORK: * From 9c0e1cc56e946ed114e891c363cc4d213b6febd2 Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 5 Mar 2016 18:08:37 +0200 Subject: [PATCH 05/38] fix:Don't create Mopidy models manually. Fixes #172. --- README.rst | 4 + .../static/js/functionsvars.js | 279 ++++++++---------- mopidy_musicbox_webclient/static/js/gui.js | 4 +- mopidy_musicbox_webclient/static/mb.appcache | 2 +- 4 files changed, 134 insertions(+), 155 deletions(-) diff --git a/README.rst b/README.rst index 2635fb2..423ea19 100644 --- a/README.rst +++ b/README.rst @@ -75,6 +75,10 @@ v2.3.0 (UNRELEASED) - Enhance build workflow to include style checks and syntax validation for HTML, CSS, and Javascript. +**Fixes** + +- Don't create Mopidy models manually. (Fixes: `#172 `_). + v2.2.0 (2016-03-01) ------------------- diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index 9d82bf3..cad2ac4 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -176,185 +176,158 @@ function albumTracksToTable (pl, target, uri) { $(target).attr('data', uri) } -function renderSongLi (song, liID, uri) { - var name - if (!song.name || song.name === '') { - name = uri.split('/') - name = decodeURI(name[name.length - 1]) +function renderSongLi (song, liID, uri, tlid, renderAlbumInfo) { + var name, iconClass + var tlidString = '' + var tlidParameter = '' + var onClick = '' + // Determine if the song line item will be rendered as part of an album. + if (!song.album || !song.album.name) { + iconClass = getMediaClass(song.uri) } else { - name = song.name + iconClass = 'trackname' } - songLi = '
  • ' + - '' + + // Play by tlid if available. + if (tlid) { + tlidString = '" tlid="' + tlid + tlidParameter = '\',\'' + tlid + onClick = 'return playTrackQueueByTlid(\'' + song.uri + '\',\'' + tlid + '\');' + } else { + onClick = 'return playTrackByUri(\'' + song.uri + '\',\'' + uri + '\');' + } + songLi = '
  • ' + + '' + '' + - '' + - '

    ' + name + '

    ' + - '
  • ' + '' + + '

    ' + song.name + '

    ' + if (renderAlbumInfo) { + songLi += '

    ' + songLi += renderSongLiTrackArtists(song) + if (song.album && song.album.name) { + songLi += ' - ' + songLi += '' + song.album.name + '

    ' + } + } + songLi += '

  • ' return songLi } -function renderQueueSongLi (song, liID, uri, tlid) { - var name - if (!song.name || song.name === '') { - name = uri.split('/') - name = decodeURI(name[name.length - 1]) - } else { - name = song.name +function renderSongLiTrackArtists (track) { + var html = '' + if (track.artists) { + for (var i = 0; i < track.artists.length; i++) { + html += track.artists[i].name + html += (i === track.artists.length - 1) ? '' : ' / ' + // Stop after 3 + if (i > 2) { + html += '...' + break + } + } } - songLi = '
  • ' + - '' + - '' + - '' + - '

    ' + name + '

    ' + - '
  • ' - return songLi + return html +} + +function isNewAlbumSection (track, previousTrack) { + // 'true' if album name is either not defined or has changed from the previous track. + return !track.album || !track.album.name || !previousTrack || !previousTrack.album || !previousTrack.album.name || + track.album.name !== previousTrack.album.name +} + +function isMultiTrackAlbum (track, nextTrack) { + // 'true' if there are more tracks of the same album after this one. + return nextTrack.album && nextTrack.album.name && track.album && track.album.name && track.album.name === nextTrack.album.name +} + +function validateTrackName (track, trackNumber) { + // Create name if there is none + var name = '' + if (!track.name || track.name === '') { + name = track.uri.split('/') + name = decodeURI(name[name.length - 1]) || 'Track ' + String(trackNumber) + } else { + name = track.name + } + return name } function resultsToTables (results, target, uri) { if (!results) { return } - var tlids = [] - if (target === CURRENT_PLAYLIST_TABLE) { - for (i = 0; i < results.length; i++) { - tlids[i] = results[i].tlid - results[i] = results[i].track - } - } - - var newalbum = [] - var newtlids = [] - // keep a list of track URIs for retrieving of covers - var coversList = [] - var nextname = '' - var count = 0 $(target).html('') - - // break into albums and put in tables - var html = '' - var tableid, j, artistname, alburi, name, iconClass - var targetmin = target.substr(1) $(target).attr('data', uri) + + var track, previousTrack, nextTrack, tlid + var albumTrackSeen = 0 + var renderAlbumInfo = true + var liID = '' + // Keep a list of track URIs for retrieving of covers + var coversList = [] + + var html = '' + var tableid, artistname, name, iconClass + var targetmin = target.substr(1) var length = 0 || results.length + + // Break into albums and put in tables for (i = 0; i < length; i++) { - // create album if none extists - if (!results[i].album) { - results[i].album = {'__model__': 'Album'} + previousTrack = track + track = results[i] + tlid = '' + if (i < length - 1) { + nextTrack = results[i + 1] } - // create album uri if there is none - if (!results[i].album.uri) { - results[i].album.uri = 'x' + if ('tlid' in results[i]) { + // Get track information from TlTrack instance + track = results[i].track + tlid = results[i].tlid + if (i < length - 1) { + nextTrack = results[i + 1].track + } } - if (!results[i].album.name) { - results[i].album.name = '' + track.name = validateTrackName(track, i) + // Leave out unplayable items + if (track.name.substring(0, 12) === '[unplayable]') { + continue } - // create name if there is no one - if (!results[i].name || results[i].name === '') { - name = results[i].uri.split('/') - results[i].name = decodeURI(name[name.length - 1]) || 'Track ' + String(i) + // Streams + if (track.length === -1) { + html += '
  • ' + track.name + ' [Stream]

  • ' + continue } - // leave out unplayable items - if (results[i].name.substring(0, 12) === '[unplayable]') continue - - newalbum.push(results[i]) - newtlids.push(tlids[i]) - nextname = '' - if ((i < length - 1) && results[i + 1].album && results[i + 1].album.name) { - nextname = results[i + 1].album.name - } - if (results[i].length === -1) { - html += '
  • ' + results[i].name + ' [Stream]

  • ' - newalbum = [] - newtlids = [] - nextname = '' - } else { - if ((results[i].album.name !== nextname) || (nextname === '')) { - tableid = 'art' + i - // render differently if only one track in the album - if (newalbum.length === 1) { - var liID = '' - if (i !== 0) { - html += '
  •  
  • ' - } - iconClass = getMediaClass(newalbum[0].uri) - liID = targetmin + '-' + newalbum[0].uri - if (target === CURRENT_PLAYLIST_TABLE) { - html += '
  • ' + - '' + - '' + - '' + - '

    ' + newalbum[0].name + '

    ' - } else { - html += '

  • ' + - '' + - '' + - '' + - '

    ' + newalbum[0].name + '

    ' - } - - if (newalbum[0].artists) { - for (j = 0; j < newalbum[0].artists.length; j++) { - html += newalbum[0].artists[j].name - html += (j === newalbum[0].artists.length - 1) ? '' : ' / ' - // stop after 3 - if (j > 2) { - html += '...' - break - } - } - } - if (newalbum[0].album.name !== '') { - html += ' / ' - } - html += '' + newalbum[0].album.name + '

    ' - html += '
  • ' - - popupData[newalbum[0].uri] = newalbum[0] - newalbum = [] - newtlids = [] - } else { // newalbum length - if (results[i].album.uri && results[i].album.name) { - iconClass = getMediaClass(newalbum[0].uri) - html += '
  • ' - html += '

    ' + results[i].album.name + '

    ' - } - if (results[i].album.artists) { - for (j = 0; j < results[i].album.artists.length; j++) { - html += results[i].album.artists[j].name - html += (j === results[i].album.artists.length - 1) ? '' : ' / ' - // stop after 3 - if (j > 2) { - child += '...' - break - } - } - } - html += '

  • ' - for (j = 0; j < newalbum.length; j++) { - popupData[newalbum[j].uri] = newalbum[j] - // hERE! - liID = targetmin + '-' + newalbum[j].uri - if (target === CURRENT_PLAYLIST_TABLE) { - html += renderQueueSongLi(newalbum[j], liID, uri, newtlids[j]) - } else { - html += renderSongLi(newalbum[j], liID, uri) - } - } - newalbum = [] - newtlids = [] - if (results[i].album) { - coversList.push([results[i].uri, i]) - } - } // newalbum length - } // albums name + if (isNewAlbumSection(track, previousTrack)) { + // Starting to render a new album in the list. + tableid = 'art' + i + // Render differently if part of an album + if (i < length - 1 && isMultiTrackAlbum(track, nextTrack)) { + // Large divider with album cover + renderAlbumInfo = false + html += '
  • ' + html += '' + + '' + + '

    ' + track.album.name + '

    ' + html += renderSongLiTrackArtists(track) + html += '

  • ' + coversList.push([track.uri, i]) + } else { + renderAlbumInfo = true + if (i > 0) { + // Small divider + html += '
  •  
  • ' + } + } + albumTrackSeen = 0 } + popupData[track.uri] = track + liID = targetmin + '-' + track.uri + html += renderSongLi(track, liID, uri, tlid, renderAlbumInfo) + albumTrackSeen += 1 } tableid = '#' + tableid $(target).html(html) - $(target).attr('data', uri) - // retrieve albumcovers + // Retrieve album covers for (i = 0; i < coversList.length; i++) { getCover(coversList[i][0], target + '-cover-' + coversList[i][1], 'small') } diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index 1654123..dba4241 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -164,7 +164,9 @@ function popupTracks (e, listuri, trackuri, tlid) { e = window.event } $('.popupTrackName').html(popupData[trackuri].name) - $('.popupAlbumName').html(popupData[trackuri].album.name) + if (popupData[trackuri].album && popupData[trackuri].album.name) { + $('.popupAlbumName').html(popupData[trackuri].album.name) + } var child = '' if (popupData[trackuri].artists) { diff --git a/mopidy_musicbox_webclient/static/mb.appcache b/mopidy_musicbox_webclient/static/mb.appcache index 27d0536..8501ba9 100644 --- a/mopidy_musicbox_webclient/static/mb.appcache +++ b/mopidy_musicbox_webclient/static/mb.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# 2016-03-05:v1 +# 2016-03-06:v2 NETWORK: * From 21f16fb17bed85dd1c9e511f292ec41c473be1b0 Mon Sep 17 00:00:00 2001 From: jcass Date: Tue, 8 Mar 2016 06:53:03 +0200 Subject: [PATCH 06/38] fix:Display album / artist information when browsing tracks. Fixes #99. Fixes #126. --- README.rst | 2 + .../static/css/webclient.css | 16 +- mopidy_musicbox_webclient/static/index.html | 26 +- .../static/js/functionsvars.js | 250 ++++++++++-------- mopidy_musicbox_webclient/static/js/gui.js | 57 ++-- mopidy_musicbox_webclient/static/js/images.js | 4 +- .../static/js/library.js | 11 +- .../static/js/process_ws.js | 112 ++++---- mopidy_musicbox_webclient/static/mb.appcache | 2 +- 9 files changed, 240 insertions(+), 240 deletions(-) diff --git a/README.rst b/README.rst index 423ea19..bc14847 100644 --- a/README.rst +++ b/README.rst @@ -74,10 +74,12 @@ v2.3.0 (UNRELEASED) ------------------- - Enhance build workflow to include style checks and syntax validation for HTML, CSS, and Javascript. +- Now displays album and artist info when browsing tracks. (Addresses: `#99 `_). **Fixes** - Don't create Mopidy models manually. (Fixes: `#172 `_). +- Context menu is now available for all tracks in browse pane. (Fixes: `#126 `_). v2.2.0 (2016-03-01) ------------------- diff --git a/mopidy_musicbox_webclient/static/css/webclient.css b/mopidy_musicbox_webclient/static/css/webclient.css index 68ece16..7a2ed05 100644 --- a/mopidy_musicbox_webclient/static/css/webclient.css +++ b/mopidy_musicbox_webclient/static/css/webclient.css @@ -36,7 +36,7 @@ margin-left: 10px; } - #playlisttracksback { + .backnav-optional { display: none; } @@ -74,7 +74,7 @@ width: 100%; } - #playlisttracksback { + .backnav-optional { display: block; } @@ -293,13 +293,17 @@ margin-left: 20px; } -.song .moreBtn{ +.song .moreBtn { float: right; padding: 15px 18px 12px 22px; display: inline-block; line-height: 100%; } +.backnav { + background-color: #ccc !important; +} + /********************** * Now Playing area * @@ -369,12 +373,14 @@ } #popupTracksLv li, -#popupQueueLv li { +#popupQueueLv li, +#popupBrowseLv li { border-bottom: 1px solid #aaa; } #popupTracksLv, -#popupQueueLv { +#popupQueueLv, +#popupBrowseLv li { border: 1px solid #aaa; } diff --git a/mopidy_musicbox_webclient/static/index.html b/mopidy_musicbox_webclient/static/index.html index 22a9158..02e92db 100644 --- a/mopidy_musicbox_webclient/static/index.html +++ b/mopidy_musicbox_webclient/static/index.html @@ -56,7 +56,6 @@ -

    Save current queue to a playlist. @@ -224,7 +221,6 @@

    -

    Overwrite existing playlist with same name? @@ -307,12 +303,10 @@

    -
    Album cover -

    -

    @@ -343,9 +337,6 @@
      -
      - Back -
        @@ -356,8 +347,9 @@

        Browse

        -
        -
          +
          +
            +
            @@ -385,7 +377,7 @@

            -
            +
              @@ -477,7 +469,6 @@
              -
              @@ -492,6 +483,7 @@
              +
              @@ -507,11 +499,9 @@
              -
              - diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index cad2ac4..8f31eaf 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -52,6 +52,7 @@ var isWebkit = /WebKit/.test(ua) PROGRAM_NAME = 'MusicBox' ARTIST_TABLE = '#artiststable' ALBUM_TABLE = '#albumstable' +BROWSE_TABLE = '#browsetable' PLAYLIST_TABLE = '#playlisttracks' CURRENT_PLAYLIST_TABLE = '#currenttable' SEARCH_ALL_TABLE = '#allresulttable' @@ -162,54 +163,69 @@ function artistsToString (artists, max) { * break up results and put them in album tables *********************************************************/ function albumTracksToTable (pl, target, uri) { - var tmp = '
                ' - var liId = '' - var targetmin = target.substr(1) + var track, previousTrack, nextTrack $(target).empty() - for (var i = 0; i < pl.length; i++) { - popupData[pl[i].uri] = pl[i] - liID = targetmin + '-' + pl[i].uri - tmp += renderSongLi(pl[i], liID, uri) - } - tmp += '
              ' - $(target).html(tmp) $(target).attr('data', uri) + for (var i = 0; i < pl.length; i++) { + previousTrack = track || undefined + nextTrack = i < pl.length - 1 ? pl[i + 1] : undefined + track = pl[i] + popupData[track.uri] = track + renderSongLi(previousTrack, track, nextTrack, uri, '', ALBUM_TABLE, i, pl.length) + } + updatePlayIcons(songdata.track.uri, songdata.tlid) } -function renderSongLi (song, liID, uri, tlid, renderAlbumInfo) { - var name, iconClass - var tlidString = '' +function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, currentIndex, listLength) { + var name var tlidParameter = '' var onClick = '' - // Determine if the song line item will be rendered as part of an album. - if (!song.album || !song.album.name) { - iconClass = getMediaClass(song.uri) - } else { - iconClass = 'trackname' + var targetmin = target.substr(1) + track.name = validateTrackName(track, currentIndex) + // Leave out unplayable items + if (track.name.substring(0, 12) === '[unplayable]') { + return + } + // Streams + if (track.length === -1) { + $(target).append('
            • ' + track.name + ' [Stream]

            • ') + return } // Play by tlid if available. - if (tlid) { - tlidString = '" tlid="' + tlid + // TODO: Need to consolidate all of the 'play...' functions + if (tlid && target === BROWSE_TABLE) { + onClick = 'return playBrowsedTracks(PLAY_ALL, ' + tlid + ');' + } else if (tlid) { tlidParameter = '\',\'' + tlid - onClick = 'return playTrackQueueByTlid(\'' + song.uri + '\',\'' + tlid + '\');' + onClick = 'return playTrackQueueByTlid(\'' + track.uri + '\',\'' + tlid + '\');' } else { - onClick = 'return playTrackByUri(\'' + song.uri + '\',\'' + uri + '\');' + onClick = 'return playTrackByUri(\'' + track.uri + '\',\'' + uri + '\');' } - songLi = '
            • ' + - '' + - '' + - '' + - '

              ' + song.name + '

              ' - if (renderAlbumInfo) { - songLi += '

              ' - songLi += renderSongLiTrackArtists(song) - if (song.album && song.album.name) { - songLi += ' - ' - songLi += '' + song.album.name + '

              ' - } + $(target).append( + '
            • ' + + '' + + '' + + '

              ' + track.name + '

            • ' + ) + if (listLength === 1 || !hasSameAlbum(previousTrack, track) && !hasSameAlbum(track, nextTrack)) { + renderSongLiAlbumInfo(track, target) } - songLi += '' - return songLi + // TODO: remove this hard-coded condition for 'ALBUM_TABLE' + if (target !== ALBUM_TABLE && !hasSameAlbum(previousTrack, track)) { + // Starting to render a new album in the list. + renderSongLiDivider(track, nextTrack, currentIndex, target) + } +} + +function renderSongLiAlbumInfo (track, target) { + var html = '

              ' + html += renderSongLiTrackArtists(track) + if (track.album && track.album.name) { + html += ' - ' + track.album.name + '

              ' + } + target = getjQueryID(target.substr(1) + '-', track.uri, true) + $(target).children('a').eq(1).append(html) + $(target + ' a h1 i').addClass(getMediaClass(track.uri)) } function renderSongLiTrackArtists (track) { @@ -228,15 +244,49 @@ function renderSongLiTrackArtists (track) { return html } -function isNewAlbumSection (track, previousTrack) { - // 'true' if album name is either not defined or has changed from the previous track. - return !track.album || !track.album.name || !previousTrack || !previousTrack.album || !previousTrack.album.name || - track.album.name !== previousTrack.album.name +function renderSongLiDivider (track, nextTrack, currentIndex, target) { + targetmin = target.substr(1) + target = getjQueryID(targetmin + '-', track.uri, true) + // Render differently if part of an album + if (hasSameAlbum(track, nextTrack)) { + // Large divider with album cover + $(target).before( + '
            • ' + + '' + + '

              ' + track.album.name + '

              ' + + renderSongLiTrackArtists(track) + '

            • ' + ) + // Retrieve album covers + getCover(track.uri, getjQueryID(targetmin + '-cover-', track.uri, true), 'small') + } else if (currentIndex > 0) { + // Small divider + $(target).before('
            •  
            • ') + } } -function isMultiTrackAlbum (track, nextTrack) { - // 'true' if there are more tracks of the same album after this one. - return nextTrack.album && nextTrack.album.name && track.album && track.album.name && track.album.name === nextTrack.album.name +function renderSongLiBackButton (results, target, onClick, optional) { + if (onClick && onClick.length > 0) { + if (!results || results.length === 0) { + $(target).empty() + $(target).append( + '
            • No tracks found...

            • ' + ) + } + var opt = '' + if (optional) { + opt = ' backnav-optional' + } + $(target).prepend( + '
            • Back

            • ' + ) + } +} + +function hasSameAlbum (track1, track2) { + // 'true' if album for each track exists and has the same name + var name1 = track1 ? (track1.album ? track1.album.name : undefined) : undefined + var name2 = track2 ? (track2.album ? track2.album.name : undefined) : undefined + return name1 && name2 && (name1 === name2) } function validateTrackName (track, trackNumber) { @@ -251,86 +301,33 @@ function validateTrackName (track, trackNumber) { return name } -function resultsToTables (results, target, uri) { - if (!results) { +function resultsToTables (results, target, uri, onClickBack, backIsOptional) { + $(target).empty() + renderSongLiBackButton(results, target, onClickBack, backIsOptional) + if (!results || results.length === 0) { return } - $(target).html('') $(target).attr('data', uri) var track, previousTrack, nextTrack, tlid - var albumTrackSeen = 0 - var renderAlbumInfo = true - var liID = '' - // Keep a list of track URIs for retrieving of covers - var coversList = [] - - var html = '' - var tableid, artistname, name, iconClass - var targetmin = target.substr(1) - var length = 0 || results.length // Break into albums and put in tables - for (i = 0; i < length; i++) { - previousTrack = track + for (i = 0; i < results.length; i++) { + previousTrack = track || undefined + nextTrack = i < results.length - 1 ? results[i + 1] : undefined track = results[i] - tlid = '' - if (i < length - 1) { - nextTrack = results[i + 1] - } - if ('tlid' in results[i]) { - // Get track information from TlTrack instance - track = results[i].track - tlid = results[i].tlid - if (i < length - 1) { - nextTrack = results[i + 1].track + if (track) { + if ('tlid' in track) { + // Get track information from TlTrack instance + tlid = track.tlid + track = track.track + nextTrack = nextTrack ? nextTrack.track : undefined } + popupData[track.uri] = track + renderSongLi(previousTrack, track, nextTrack, uri, tlid, target, i, results.length) } - track.name = validateTrackName(track, i) - // Leave out unplayable items - if (track.name.substring(0, 12) === '[unplayable]') { - continue - } - // Streams - if (track.length === -1) { - html += '
            • ' + track.name + ' [Stream]

            • ' - continue - } - - if (isNewAlbumSection(track, previousTrack)) { - // Starting to render a new album in the list. - tableid = 'art' + i - // Render differently if part of an album - if (i < length - 1 && isMultiTrackAlbum(track, nextTrack)) { - // Large divider with album cover - renderAlbumInfo = false - html += '
            • ' - html += '' + - '' + - '

              ' + track.album.name + '

              ' - html += renderSongLiTrackArtists(track) - html += '

            • ' - coversList.push([track.uri, i]) - } else { - renderAlbumInfo = true - if (i > 0) { - // Small divider - html += '
            •  
            • ' - } - } - albumTrackSeen = 0 - } - popupData[track.uri] = track - liID = targetmin + '-' + track.uri - html += renderSongLi(track, liID, uri, tlid, renderAlbumInfo) - albumTrackSeen += 1 - } - tableid = '#' + tableid - $(target).html(html) - // Retrieve album covers - for (i = 0; i < coversList.length; i++) { - getCover(coversList[i][0], target + '-cover-' + coversList[i][1], 'small') } + updatePlayIcons(songdata.track.uri, songdata.tlid) } // process updated playlist to gui @@ -519,3 +516,32 @@ function isSpotifyStarredPlaylist (playlist) { var starredRegex = /spotify:user:.*:starred/g return (starredRegex.test(playlist.uri) && playlist.name === 'Starred') } + +/** + * Converts a URI to a jQuery-safe identifier. jQuery identifiers need to be + * unique per page and cannot contain special characters. + * + * @param {string} identifier - Identifier string to prefix to the URI. Can + * be used to ensure that the generated ID will be unique for the page that + * it will be included on. Can be any string (e.g. ID of parent element). + * + * @param {string} uri - URI to encode, usually the URI of a Mopidy track. + * + * @param {boolean} includePrefix - Will prefix the generated identifier + * with the '#' character if set to 'true', ready to be passed to $() or + * jQuery(). + * + * @return {string} - a string in the format '[#]identifier-encodedURI' that + * is safe to use as a jQuery identifier. + */ +function getjQueryID (identifier, uri, includePrefix) { + var prefix = includePrefix ? '#' : '' + return prefix + identifier + fixedEncodeURIComponent(uri).replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '') +} + +// Strict URI encoding as per https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent +function fixedEncodeURIComponent (str) { + return encodeURIComponent(str).replace(/[!'()*]/g, function (c) { + return '%' + c.charCodeAt(0).toString(16) + }) +} diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index dba4241..a622a31 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -413,7 +413,6 @@ function locationHashChanged () { break case 'current': $('#navcurrent a').addClass('ui-state-active ui-state-persist ui-btn-active') - getCurrentPlaylist() break case 'playlists': $('#navplaylists a').addClass('ui-state-active ui-state-persist ui-btn-active') @@ -622,55 +621,29 @@ $(document).ready(function (event) { }) function updatePlayIcons (uri, tlid) { - // update styles of listviews - $('#currenttable li').each(function () { + // Update styles of listviews + var listviews = [PLAYLIST_TABLE, SEARCH_TRACK_TABLE, ARTIST_TABLE, ALBUM_TABLE, BROWSE_TABLE] + var target = CURRENT_PLAYLIST_TABLE.substr(1) + $(CURRENT_PLAYLIST_TABLE).children('li').each(function () { var eachTlid = $(this).attr('tlid') if (typeof eachTlid !== 'undefined') { eachTlid = parseInt(eachTlid) } - if (this.id === 'currenttable-' + uri && eachTlid === tlid) { + if (this.id === getjQueryID(target + '-', uri) && eachTlid === tlid) { $(this).addClass('currenttrack') } else { $(this).removeClass('currenttrack') } }) - $('#playlisttracks li').each(function () { - if (this.id === 'playlisttracks-' + uri) { - $(this).addClass('currenttrack2') - } else { - $(this).removeClass('currenttrack2') - } - }) - - $('#trackresulttable li').each(function () { - if (this.id === 'trackresulttable-' + uri) { - $(this).addClass('currenttrack2') - } else { - $(this).removeClass('currenttrack2') - } - }) - - $('#artiststable li').each(function () { - if (this.id === 'artiststable-' + uri) { - $(this).addClass('currenttrack2') - } else { - $(this).removeClass('currenttrack2') - } - }) - - $('#albumstable li').each(function () { - if (this.id === 'albumstable-' + uri) { - $(this).addClass('currenttrack2') - } else { - $(this).removeClass('currenttrack2') - } - }) - $('#browselist li').each(function () { - if (this.id === 'browselisttracks-' + uri) { - $(this).addClass('currenttrack2') - } else { - $(this).removeClass('currenttrack2') - } - }) + for (var i = 0; i < listviews.length; i++) { + target = listviews[i].substr(1) + $(listviews[i]).children('li').each(function () { + if (this.id === getjQueryID(target + '-', uri)) { + $(this).addClass('currenttrack2') + } else { + $(this).removeClass('currenttrack2') + } + }) + } } diff --git a/mopidy_musicbox_webclient/static/js/images.js b/mopidy_musicbox_webclient/static/js/images.js index d8c6627..5c16f18 100644 --- a/mopidy_musicbox_webclient/static/js/images.js +++ b/mopidy_musicbox_webclient/static/js/images.js @@ -77,7 +77,7 @@ function getCoverFromLastFm (track, images, size) { } } } - }) + }, $(images).attr('src', defUrl)) } function getArtistImage (nwartist, image, size) { @@ -88,5 +88,5 @@ function getArtistImage (nwartist, image, size) { $(image).attr('src', data.artist.image[i]['#text'] || defUrl) } } - }}) + }}, $(images).attr('src', defUrl)) } diff --git a/mopidy_musicbox_webclient/static/js/library.js b/mopidy_musicbox_webclient/static/js/library.js index 9cba433..5047c5d 100644 --- a/mopidy_musicbox_webclient/static/js/library.js +++ b/mopidy_musicbox_webclient/static/js/library.js @@ -106,6 +106,10 @@ function processSearchResults (resultArr) { customTracklists[URI_SCHEME + ':trackresultscache'] = results.tracks if (emptyResult) { + $('#searchtracks').show() + $(SEARCH_TRACK_TABLE).append( + '
            • No tracks found...

            • ' + ) toast('No results') showLoading(false) return false @@ -193,8 +197,6 @@ function processSearchResults (resultArr) { // Inject list items, refresh listview and hide superfluous items. $(SEARCH_ALBUM_TABLE).html(child).listview('refresh').find('.overflow').hide() - $('#expandsearch').show() - // Track results resultsToTables(results.tracks, SEARCH_TRACK_TABLE, URI_SCHEME + ':trackresultscache') @@ -250,12 +252,13 @@ function togglePlaylists () { } function showTracklist (uri) { + showLoading(true) $(PLAYLIST_TABLE).empty() togglePlaylists() var tracks = getPlaylistTracks(uri).then(function (tracks) { - resultsToTables(tracks, PLAYLIST_TABLE, uri) + resultsToTables(tracks, PLAYLIST_TABLE, uri, 'return togglePlaylists();', true) + showLoading(false) }) - showLoading(false) updatePlayIcons(uri) $('#playlistslist li a').each(function () { $(this).removeClass('playlistactive') diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js index c1279c3..1c6ed37 100644 --- a/mopidy_musicbox_webclient/static/js/process_ws.js +++ b/mopidy_musicbox_webclient/static/js/process_ws.js @@ -76,78 +76,78 @@ function processPlaystate (data) { * process results of a browse list *********************************************************/ function processBrowseDir (resultArr) { - var backHtml = '
            • Back

            • ' - if ((!resultArr) || (resultArr === '') || (resultArr.length === 0)) { - $('#browsepath').html('No tracks found...') - $('#browselist').html(backHtml) + $(BROWSE_TABLE).empty() + if (browseStack.length > 0) { + renderSongLiBackButton(resultArr, BROWSE_TABLE, 'return getBrowseDir();') + } + if (!resultArr || resultArr.length === 0) { showLoading(false) return } - - $('#browselist').empty() - - var child = '' - var rooturi = '' - var uri = resultArr[0].uri - - // check root uri - // find last : or / (spltting the result) - // do it twice, since. - var colonindex = uri.lastIndexOf(':') - var slashindex = uri.lastIndexOf('/') - - var lastindex = (colonindex > slashindex) ? colonindex : slashindex - rooturi = uri.slice(0, lastindex) - if (resultArr[0].type === 'track') { - rooturi = rooturi.replace(':track:', ':directory:') - } - colonindex = rooturi.lastIndexOf(':') - slashindex = rooturi.lastIndexOf('/') - - lastindex = (colonindex > slashindex) ? colonindex : slashindex - rooturi = rooturi.slice(0, lastindex) - - if (browseStack.length > 0) { - child += backHtml - } - browseTracks = [] + uris = [] + var ref, track, previousTrack, nextTrack + var uri = resultArr[0].uri + var length = 0 || resultArr.length + for (var i = 0, index = 0; i < resultArr.length; i++) { - iconClass = getMediaClass(resultArr[i].uri) if (resultArr[i].type === 'track') { - // console.log(resultArr[i]); - mopidy.library.lookup({'uris': [resultArr[i].uri]}).then(function (resultDict) { - var lookupUri = Object.keys(resultDict)[0] - popupData[lookupUri] = resultDict[lookupUri][0] - browseTracks.push(resultDict[lookupUri][0]) - }, console.error) - child += '
            • ' + - '' + - '' + - '

              ' + resultArr[i].name + '

            • ' + ref = resultArr[i] + popupData[ref.uri] = ref + browseTracks.push(ref) + uris.push(ref.uri) + + $(BROWSE_TABLE).append( + '
            • ' + + '' + + '' + + '' + + '

              ' + ref.name + '

            • ' + ) index++ } else { + var iconClass = '' if (browseStack.length > 0) { iconClass = 'fa fa-folder-o' + } else { + iconClass = getMediaClass(resultArr[i].uri) } - child += '
            • ' + resultArr[i].name + '

            • ' + $(BROWSE_TABLE).append( + '
            • ' + + '

              ' + resultArr[i].name + '

            • ' + ) } } - $('#browselist').html(child) - if (browseStack.length > 0) { - child = getMediaHuman(uri) - iconClass = getMediaClass(uri) - $('#browsepath').html(' ' + child) - } else { - $('#browsepath').html('') - } - updatePlayIcons(songdata.track.uri, songdata.tlid) - showLoading(false) + if (uris.length > 0) { + mopidy.library.lookup({'uris': uris}).then(function (resultDict) { + // Break into albums and put in tables + var track, previousTrack, nextTrack, uri + $.each(resultArr, function (i, ref) { + if (ref.type === 'track') { + previousTrack = track || undefined + if (i < resultArr.length - 1 && resultDict[resultArr[i + 1].uri]) { + nextTrack = resultDict[resultArr[i + 1].uri][0] + } else { + nextTrack = undefined + } + track = resultDict[ref.uri][0] + if (uris.length === 1 || (previousTrack && !hasSameAlbum(previousTrack, track) && !hasSameAlbum(track, nextTrack))) { + renderSongLiAlbumInfo(track, BROWSE_TABLE) + } + if (!hasSameAlbum(previousTrack, track)) { + // Starting to render a new album in the list. + renderSongLiDivider(track, nextTrack, i, BROWSE_TABLE) + } + } + }) + showLoading(false) + }, console.error) + } else { + showLoading(false) + } } /** ****************************************************** diff --git a/mopidy_musicbox_webclient/static/mb.appcache b/mopidy_musicbox_webclient/static/mb.appcache index 8501ba9..5844c18 100644 --- a/mopidy_musicbox_webclient/static/mb.appcache +++ b/mopidy_musicbox_webclient/static/mb.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# 2016-03-06:v2 +# 2016-03-14:v1 NETWORK: * From 7710d23567fd9ea6cc6815cf198cdd93e3687db5 Mon Sep 17 00:00:00 2001 From: jcass Date: Wed, 16 Mar 2016 16:43:27 +0200 Subject: [PATCH 07/38] Enhance getjQueryID to handle '#' prefix automatically. --- .../static/js/functionsvars.js | 22 ++++++++++--------- mopidy_musicbox_webclient/static/js/gui.js | 4 ++-- .../static/js/process_ws.js | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index 8f31eaf..636e7f9 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -180,7 +180,6 @@ function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, curre var name var tlidParameter = '' var onClick = '' - var targetmin = target.substr(1) track.name = validateTrackName(track, currentIndex) // Leave out unplayable items if (track.name.substring(0, 12) === '[unplayable]') { @@ -202,7 +201,7 @@ function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, curre onClick = 'return playTrackByUri(\'' + track.uri + '\',\'' + uri + '\');' } $(target).append( - '
            • ' + + '
            • ' + '' + '' + '

              ' + track.name + '

            • ' @@ -223,7 +222,7 @@ function renderSongLiAlbumInfo (track, target) { if (track.album && track.album.name) { html += ' - ' + track.album.name + '

              ' } - target = getjQueryID(target.substr(1) + '-', track.uri, true) + target = getjQueryID(target, track.uri, true) $(target).children('a').eq(1).append(html) $(target + ' a h1 i').addClass(getMediaClass(track.uri)) } @@ -245,19 +244,18 @@ function renderSongLiTrackArtists (track) { } function renderSongLiDivider (track, nextTrack, currentIndex, target) { - targetmin = target.substr(1) - target = getjQueryID(targetmin + '-', track.uri, true) + target = getjQueryID(target, track.uri, true) // Render differently if part of an album if (hasSameAlbum(track, nextTrack)) { // Large divider with album cover $(target).before( '
            • ' + - '' + + '' + '

              ' + track.album.name + '

              ' + renderSongLiTrackArtists(track) + '

            • ' ) // Retrieve album covers - getCover(track.uri, getjQueryID(targetmin + '-cover-', track.uri, true), 'small') + getCover(track.uri, getjQueryID(target + '-cover', track.uri, true), 'small') } else if (currentIndex > 0) { // Small divider $(target).before('
            •  
            • ') @@ -523,7 +521,7 @@ function isSpotifyStarredPlaylist (playlist) { * * @param {string} identifier - Identifier string to prefix to the URI. Can * be used to ensure that the generated ID will be unique for the page that - * it will be included on. Can be any string (e.g. ID of parent element). + * it will be included on. Also accepts jQuery identifiers starting with '#'. * * @param {string} uri - URI to encode, usually the URI of a Mopidy track. * @@ -535,8 +533,12 @@ function isSpotifyStarredPlaylist (playlist) { * is safe to use as a jQuery identifier. */ function getjQueryID (identifier, uri, includePrefix) { - var prefix = includePrefix ? '#' : '' - return prefix + identifier + fixedEncodeURIComponent(uri).replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '') + if (identifier.charAt(0) === '#' && !includePrefix) { + identifier = identifier.substr(1) + } else if (identifier.charAt(0) !== '#' && includePrefix) { + identifier = '#' + identifier + } + return identifier + '-' + fixedEncodeURIComponent(uri).replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '') } // Strict URI encoding as per https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index a622a31..104732e 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -629,7 +629,7 @@ function updatePlayIcons (uri, tlid) { if (typeof eachTlid !== 'undefined') { eachTlid = parseInt(eachTlid) } - if (this.id === getjQueryID(target + '-', uri) && eachTlid === tlid) { + if (this.id === getjQueryID(target, uri) && eachTlid === tlid) { $(this).addClass('currenttrack') } else { $(this).removeClass('currenttrack') @@ -639,7 +639,7 @@ function updatePlayIcons (uri, tlid) { for (var i = 0; i < listviews.length; i++) { target = listviews[i].substr(1) $(listviews[i]).children('li').each(function () { - if (this.id === getjQueryID(target + '-', uri)) { + if (this.id === getjQueryID(target, uri)) { $(this).addClass('currenttrack2') } else { $(this).removeClass('currenttrack2') diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js index 1c6ed37..e4228af 100644 --- a/mopidy_musicbox_webclient/static/js/process_ws.js +++ b/mopidy_musicbox_webclient/static/js/process_ws.js @@ -98,7 +98,7 @@ function processBrowseDir (resultArr) { uris.push(ref.uri) $(BROWSE_TABLE).append( - '
            • ' + + '
            • ' + '' + '' + '' + From f8db8be71f5b7d6eea8493b2c794017a036a26a9 Mon Sep 17 00:00:00 2001 From: jcass Date: Thu, 10 Mar 2016 17:21:13 +0200 Subject: [PATCH 08/38] Add Javascript test and test coverage frameworks. --- .gitignore | 1 + .travis.yml | 5 +- karma.conf.js | 81 +++++++ mopidy_musicbox_webclient/static/js/gui.js | 2 +- mopidy_musicbox_webclient/static/js/images.js | 154 ++++++++------ .../static/js/process_ws.js | 6 +- .../static/js/progress_timer.js | 7 +- package.json | 36 +++- tests/test_images.js | 201 ++++++++++++++++++ tox.ini | 14 +- 10 files changed, 421 insertions(+), 86 deletions(-) create mode 100644 karma.conf.js create mode 100644 tests/test_images.js diff --git a/.gitignore b/.gitignore index b754965..e2cf956 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ MANIFEST build/ cover/ +coverage/ coverage.xml dist/ docs/_build/ diff --git a/.travis.yml b/.travis.yml index 5ce0d3d..c1749cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ python: env: - TOX_ENV=py27 - TOX_ENV=flake8 + - TOX_ENV=test - TOX_ENV=eslint - TOX_ENV=csslint - TOX_ENV=tidy @@ -19,4 +20,6 @@ script: - "tox -e $TOX_ENV" after_success: - - "if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; fi" + # TODO: find a way to combine .py and .js coverage reports. + # - "if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; fi" + - "if [ $TOX_ENV == 'test' ]; then cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js; fi" diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..6104b24 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,81 @@ +// Karma configuration + +module.exports = function (config) { + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['browserify', 'mocha'], + + // list of files / patterns to load in the browser + files: [ + 'mopidy_musicbox_webclient/static/vendors/**/*.js', + // TODO: can remove the next line once JavaScript codebase has been modularized. + 'mopidy_musicbox_webclient/static/js/**/*.js', + 'tests/**/test_*.js' + ], + + // list of files to exclude + exclude: [ + ], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + 'tests/**/test_*.js': [ 'browserify' ], + 'mopidy_musicbox_webclient/static/js/**/*.js': ['coverage'] + }, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress', 'coverage'], + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['PhantomJS'], + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: Infinity, + + // add additional browserify configuration properties here + // such as transform and/or debug=true to generate source maps + browserify: { + debug: true, + transform: [ + 'babelify', + ['browserify-istanbul', { instrumenter: require('isparta') }] + ] + }, + + coverageReporter: { + // specify a common output directory + dir: 'coverage/', + reporters: [ + { type: 'lcov', subdir: '.' }, + { type: 'text'} + ] + } + }) +} diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index 104732e..7eff64c 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -128,7 +128,7 @@ function setSongInfo (data) { } if (data.track.album && data.track.album.name) { $('#modalalbum').html('' + data.track.album.name + '') - getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge') + coverArt.getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge') } else { $('#modalalbum').html('') $('#infocover').attr('src', 'images/default_cover.png') diff --git a/mopidy_musicbox_webclient/static/js/images.js b/mopidy_musicbox_webclient/static/js/images.js index 5c16f18..0a1b531 100644 --- a/mopidy_musicbox_webclient/static/js/images.js +++ b/mopidy_musicbox_webclient/static/js/images.js @@ -5,88 +5,106 @@ API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733' API_SECRET = '2c631802c2285d5d5d1502462fe42a2b' -var fmcache -var lastfm - -$(window).load(function () { - // create a Cache object - fmcache = new LastFMCache() - // create a LastFM object - lastfm = new LastFM({ +var coverArt = { + fmcache: new LastFMCache(), + lastfm: new LastFM({ apiKey: API_KEY, apiSecret: API_SECRET, - cache: fmcache - }) -}) + cache: this.fmcache + }), -function getCover (uri, images, size) { - var defUrl = 'images/default_cover.png' - $(images).attr('src', defUrl) - if (!uri) { - return - } - - mopidy.library.getImages({'uris': [uri]}).then(function (imageResults) { - var uri = Object.keys(imageResults)[0] - if (imageResults[uri].length > 0) { - $(images).attr('src', imageResults[uri][0].uri) - } else { - // Also check deprecated 'album.images' in case backend does not - // implement mopidy.library.getImages yet... - getCoverFromAlbum(uri, images, size) + getCover: function (uri, images, size) { + var defUrl = 'images/default_cover.png' + $(images).attr('src', defUrl) + if (!uri) { + return } - }) -} + mopidy.library.getImages({'uris': [uri]}).then(function (imageResults) { + var uri = Object.keys(imageResults)[0] + if (imageResults[uri].length > 0) { + $(images).attr('src', imageResults[uri][0].uri) + } else { + // Also check deprecated 'album.images' in case backend does not + // implement mopidy.library.getImages yet... + coverArt.getCoverFromAlbum(uri, images, size) + } + }) + }, -// Note that this approach has been deprecated in Mopidy -// TODO: Remove when Mopidy no longer supports getting images -// with 'album.images'. -function getCoverFromAlbum (uri, images, size) { - mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) { - var uri = Object.keys(resultDict)[0] - var track = resultDict[uri][0] - if (track.album && track.album.images && (track.album.images.length > 0)) { - $(images).attr('src', track.album.images[0]) - } else { - // Fallback to last.fm - getCoverFromLastFm(track, 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 + } + }) + }, -function getCoverFromLastFm (track, images, size) { - var defUrl = 'images/default_cover.png' - if (!(track.album || track.artist)) { - return - } - var albumname = track.album.name || '' - var artistname = '' - if (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 - } + 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 + } - lastfm.album.getInfo({artist: artistname, album: albumname}, - { success: function (data) { + 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.log('Error retrieving album info from last.fm', code, message) + }}) + }, + + getArtistImage: function (artist, images, size) { + var defUrl = 'images/user_24x32.png' + $(images).attr('src', defUrl) + if (!artist || artist.length === 0) { + return } - }, $(images).attr('src', defUrl)) + 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.log('Error retrieving artist info from last.fm', code, message) + }}) + } } -function getArtistImage (nwartist, image, size) { - var defUrl = 'images/user_24x32.png' - lastfm.artist.getInfo({artist: nwartist}, {success: function (data) { - for (var i = 0; i < data.artist.image.length; i++) { - if (data.artist.image[i].size === size) { - $(image).attr('src', data.artist.image[i]['#text'] || defUrl) - } - } - }}, $(images).attr('src', defUrl)) +$(document).ready(coverArt.init) + +// TODO: Remove this once JavaScript codebase has been completely modularized +// in favour of bundling everything using 'browserify'. +if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + module.exports = coverArt + } } diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js index e4228af..4c0d287 100644 --- a/mopidy_musicbox_webclient/static/js/process_ws.js +++ b/mopidy_musicbox_webclient/static/js/process_ws.js @@ -220,7 +220,7 @@ function processCurrentPlaylist (resultArr) { function processArtistResults (resultArr) { if (!resultArr || (resultArr.length === 0)) { $('#h_artistname').text('Artist not found...') - getCover('', '#artistviewimage, #artistpopupimage', 'extralarge') + coverArt.getCover('', '#artistviewimage, #artistpopupimage', 'extralarge') showLoading(false) return } @@ -239,7 +239,7 @@ function processArtistResults (resultArr) { function processAlbumResults (resultArr) { if (!resultArr || (resultArr.length === 0)) { $('#h_albumname').text('Album not found...') - getCover('', '#albumviewcover, #coverpopupimage', 'extralarge') + coverArt.getCover('', '#albumviewcover, #coverpopupimage', 'extralarge') showLoading(false) return } @@ -252,6 +252,6 @@ function processAlbumResults (resultArr) { $('#h_albumartist').html(artistname) $('#coverpopupalbumname').html(albumname) $('#coverpopupartist').html(artistname) - getCover(resultArr[0].uri, '#albumviewcover, #coverpopupimage', 'extralarge') + coverArt.getCover(resultArr[0].uri, '#albumviewcover, #coverpopupimage', 'extralarge') showLoading(false) } diff --git a/mopidy_musicbox_webclient/static/js/progress_timer.js b/mopidy_musicbox_webclient/static/js/progress_timer.js index 0b371c7..00ff6e9 100644 --- a/mopidy_musicbox_webclient/static/js/progress_timer.js +++ b/mopidy_musicbox_webclient/static/js/progress_timer.js @@ -1,5 +1,4 @@ var progressTimer -var progressElement = document.getElementById('trackslider') var positionNode = document.createTextNode('') var durationNode = document.createTextNode('') @@ -13,8 +12,10 @@ var syncsLeft = MAX_SYNCS var synced = false var consecutiveSyncs = 0 -document.getElementById('songelapsed').appendChild(positionNode) -document.getElementById('songlength').appendChild(durationNode) +$(document).ready(function () { + $('#songelapsed').append(positionNode) + $('#songlength').append(durationNode) +}) function timerCallback (position, duration, isRunning) { updateTimers(position, duration, isRunning) diff --git a/package.json b/package.json index ee6c8e3..97fb1ac 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "test": "tests" }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "eslint": "eslint mopidy_musicbox_webclient/static/js/**.js", + "test": "karma start karma.conf.js", + "eslint": "eslint mopidy_musicbox_webclient/static/js/**/**.js tests/**/test_*.js", "csslint": "csslint mopidy_musicbox_webclient/static/css/**.css", "tidy": "node tidy.js" }, @@ -22,12 +22,32 @@ "url": "https://github.com/pimusicbox/mopidy-musicbox-webclient/issues" }, "devDependencies": { - "eslint": "latest", - "eslint-config-standard": "latest", - "eslint-plugin-standard": "latest", - "eslint-plugin-promise": "latest", - "csslint": "latest", - "tidy-html5": "latest" + "babelify": "^7.2.0", + "browserify": "^13.0.0", + "browserify-istanbul": "^2.0.0", + "chai": "^3.5.0", + "chai-as-promised": "^5.2.0", + "chai-jquery": "^2.0.0", + "chai-string": "^1.2.0", + "coveralls": "^2.11.8", + "csslint": "^0.10.0", + "eslint": "^2.3.0", + "eslint-config-standard": "^5.1.0", + "eslint-plugin-promise": "^1.1.0", + "eslint-plugin-standard": "^1.3.2", + "install": "^0.5.6", + "isparta": "^4.0.0", + "karma": "^0.13.22", + "karma-browserify": "^5.0.2", + "karma-cli": "^0.1.2", + "karma-coverage": "^0.5.5", + "karma-mocha": "^0.2.2", + "karma-phantomjs-launcher": "^1.0.0", + "mocha": "^2.4.5", + "phantomjs-prebuilt": "^2.1.5", + "sinon": "^1.17.3", + "tidy-html5": "latest", + "watchify": "^3.7.0" }, "homepage": "https://github.com/pimusicbox/mopidy-musicbox-webclient#readme" } diff --git a/tests/test_images.js b/tests/test_images.js new file mode 100644 index 0000000..8d52232 --- /dev/null +++ b/tests/test_images.js @@ -0,0 +1,201 @@ +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 coverArt = require('../mopidy_musicbox_webclient/static/js/images.js') + +var images + +before(function () { + html = + '' + + '' + $(document).ready(function () { + $(document.body).add(html) + }) + mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'})) + images = $('') +}) + +describe('CoverArt', function () { + describe('#getCover()', function () { + beforeEach(function () { + $(images).removeAttr('src') + }) + + it('should use default image if no track URI is provided', function () { + coverArt.getCover('', images, '') + $(images).prop('src').should.endWith('images/default_cover.png') + }) + + it('should get image from Mopidy, if available', function () { + var getImagesResultMock = {'mock:track:uri': [{uri: 'mockImageUri'}]} + var library = { getImages: function () { return $.when(getImagesResultMock) } } + mopidy.library = library + + var getImagesSpy = sinon.spy(mopidy.library, 'getImages') + coverArt.getCover('mock:track:uri', images, '') + + assert(getImagesSpy.calledOnce) + $(images).prop('src').should.endWith('mockImageUri') + }) + + it('should fall back to retrieving image from deprecated track.album.images', function () { + var getImagesResultMock = {'mock:track:uri': []} + var lookupResultMock = {'mock:track:uri': [{album: {images: ['mockAlbumImageUri']}}]} + var library = { + getImages: function () { return $.when(getImagesResultMock) }, + lookup: function () { return $.when(lookupResultMock) } + } + mopidy.library = library + + var getImagesSpy = sinon.spy(mopidy.library, 'getImages') + var getCoverFromAlbumSpy = sinon.spy(coverArt, 'getCoverFromAlbum') + + coverArt.getCover('mock:track:uri', images, '') + + assert(getImagesSpy.calledOnce) + assert(getCoverFromAlbumSpy.calledOnce) + }) + }) + + describe('#getCoverFromAlbum()', function () { + beforeEach(function () { + $(images).removeAttr('src') + }) + + it('should use default image if no track URI is provided', function () { + coverArt.getCoverFromAlbum('', images, '') + $(images).prop('src').should.endWith('images/default_cover.png') + }) + + it('should get image from Mopidy track.album.images, if available', function () { + var lookupResultMock = {'mock:track:uri': [{album: {images: ['mockAlbumImageUri']}}]} + var library = { + lookup: function () { return $.when(lookupResultMock) } + } + mopidy.library = library + + var lookupSpy = sinon.spy(mopidy.library, 'lookup') + coverArt.getCoverFromAlbum('mock:track:uri', images, '') + + assert(lookupSpy.calledOnce) + $(images).prop('src').should.endWith('mockAlbumImageUri') + }) + + it('should use default image if track.album or track.artist is not available', function () { + var lookupResultMock = {'mock:track:uri': []} + var library = { + lookup: function () { return $.when(lookupResultMock) } + } + mopidy.library = library + + var lookupSpy = sinon.spy(mopidy.library, 'lookup') + coverArt.getCoverFromAlbum('mock:track:uri', images, '') + + assert(lookupSpy.calledOnce) + $(images).prop('src').should.endWith('images/default_cover.png') + }) + + it('should fall back to retrieving image from last.fm if none provided by Mopidy', function () { + var lookupResultMock = {'mock:track:uri': [{album: {images: []}}]} + var library = { + lookup: function () { return $.when(lookupResultMock) } + } + mopidy.library = library + + var getCoverFromLastFmSpy = sinon.spy(coverArt, 'getCoverFromLastFm') + coverArt.getCoverFromAlbum('mock:track:uri', images, '') + + assert(getCoverFromLastFmSpy.calledOnce) + }) + }) + + describe('#getCoverFromLastFm()', function () { + beforeEach(function () { + $(images).removeAttr('src') + }) + + it('should use default image if no track is provided', function () { + coverArt.getCoverFromLastFm(undefined, images, '') + $(images).prop('src').should.endWith('images/default_cover.png') + }) + + it('should fall back to using track artist if album artist is not available', function () { + var track = {artists: [{name: 'artistMock'}]} + var getInfoResultMock = {album: {image: []}} + + var getInfoStub = sinon.stub(coverArt.lastfm.album, 'getInfo') + getInfoStub.returns($.when(getInfoResultMock)) + + coverArt.getCoverFromLastFm(track, images, '') + var args = getInfoStub.args + assert(args[0][0].artist === 'artistMock') + getInfoStub.restore() + }) + + it('should get album info from last.fm', function () { + var track = {album: {artists: [{name: 'albumMock'}]}} + var getInfoResultMock = {album: {image: [{'#text': 'mockAlbumImageUri', size: 'small'}]}} + + var getInfoStub = sinon.stub(coverArt.lastfm.album, 'getInfo') + getInfoStub.yieldsTo('success', getInfoResultMock) + + coverArt.getCoverFromLastFm(track, images, 'small') + $(images).prop('src').should.endWith('mockAlbumImageUri') + getInfoStub.restore() + }) + + it('should log errors', function () { + var track = {album: {artists: [{name: 'albumMock'}]}} + var getInfoStub = sinon.stub(coverArt.lastfm.album, 'getInfo') + getInfoStub.yieldsTo('error', 'code', 'message') + + var consoleSpy = sinon.spy(console, 'log') + coverArt.getCoverFromLastFm(track, images, '') + + assert(consoleSpy.calledOnce) + getInfoStub.restore() + consoleSpy.restore() + }) + }) + + describe('#getArtistImage()', function () { + beforeEach(function () { + $(images).removeAttr('src') + }) + + it('should use default image if no artist is provided', function () { + coverArt.getArtistImage('', images, '') + $(images).prop('src').should.endWith('images/user_24x32.png') + }) + + it('should get artist info from last.fm', function () { + var getInfoResultMock = {artist: {image: [{'#text': 'mockArtistImageUri', size: 'small'}]}} + + var getInfoStub = sinon.stub(coverArt.lastfm.artist, 'getInfo') + getInfoStub.yieldsTo('success', getInfoResultMock) + + coverArt.getArtistImage('mockArtist', images, 'small') + $(images).prop('src').should.endWith('mockArtistImageUri') + getInfoStub.restore() + }) + + it('should log errors', function () { + var getInfoStub = sinon.stub(coverArt.lastfm.artist, 'getInfo') + getInfoStub.yieldsTo('error', 'code', 'message') + + var consoleSpy = sinon.spy(console, 'log') + + coverArt.getArtistImage('mockArtist', images, 'small') + assert(consoleSpy.calledOnce) + getInfoStub.restore() + consoleSpy.restore() + }) + }) +}) diff --git a/tox.ini b/tox.ini index 96a5099..b6a3ba3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, flake8, eslint, csslint, tidy +envlist = py27, flake8, test, eslint, csslint, tidy [testenv] deps = @@ -20,7 +20,17 @@ deps = flake8 flake8-import-order skip_install = true -commands = flake8 {posargs:mopidy_musicbox_webclient} +commands = flake8 {posargs:mopidy_musicbox_webclient tests} + +[testenv:test] +whitelist_externals = + /bin/bash +deps = + nodeenv +skip_install = true +commands = + - nodeenv --prebuilt {toxworkdir}/node_env + bash -c '. {toxworkdir}/node_env/bin/activate; npm install; npm test' [testenv:eslint] whitelist_externals = From 7944acecb9c89960f07181718baef874f1f8bb6b Mon Sep 17 00:00:00 2001 From: jcass Date: Wed, 16 Mar 2016 21:54:49 +0200 Subject: [PATCH 09/38] Add missing object literal reference that was missed during merge. --- mopidy_musicbox_webclient/static/js/functionsvars.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index 636e7f9..89ee24b 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -255,7 +255,7 @@ function renderSongLiDivider (track, nextTrack, currentIndex, target) { renderSongLiTrackArtists(track) + '

            • ' ) // Retrieve album covers - getCover(track.uri, getjQueryID(target + '-cover', track.uri, true), 'small') + coverArt.getCover(track.uri, getjQueryID(target + '-cover', track.uri, true), 'small') } else if (currentIndex > 0) { // Small divider $(target).before('
            •  
            • ') From db006f6c54c63f506430bf1474d117c4aa8cb82f Mon Sep 17 00:00:00 2001 From: jcass Date: Fri, 18 Mar 2016 22:03:27 +0200 Subject: [PATCH 10/38] Remember selection of which backend to search. Fixes #130. --- README.rst | 2 + karma.conf.js | 2 +- mopidy_musicbox_webclient/static/index.html | 10 +- .../static/js/functionsvars.js | 12 +- mopidy_musicbox_webclient/static/js/gui.js | 34 +- mopidy_musicbox_webclient/static/js/images.js | 4 +- .../static/js/library.js | 612 +++++++++--------- .../static/js/process_ws.js | 6 +- mopidy_musicbox_webclient/static/mb.appcache | 2 +- tests/test_images.js | 10 +- tests/test_library.js | 54 ++ 11 files changed, 395 insertions(+), 353 deletions(-) create mode 100644 tests/test_library.js diff --git a/README.rst b/README.rst index bc14847..58c12b0 100644 --- a/README.rst +++ b/README.rst @@ -75,6 +75,8 @@ v2.3.0 (UNRELEASED) - Enhance build workflow to include style checks and syntax validation for HTML, CSS, and Javascript. - Now displays album and artist info when browsing tracks. (Addresses: `#99 `_). +- Now remembers which backend was searched previously, and automatically selects that backend as the default search target. + (Addresses: `#130 `_). **Fixes** diff --git a/karma.conf.js b/karma.conf.js index 6104b24..c64ba37 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -74,7 +74,7 @@ module.exports = function (config) { dir: 'coverage/', reporters: [ { type: 'lcov', subdir: '.' }, - { type: 'text'} + { type: 'text' } ] } }) diff --git a/mopidy_musicbox_webclient/static/index.html b/mopidy_musicbox_webclient/static/index.html index 02e92db..225ae79 100644 --- a/mopidy_musicbox_webclient/static/index.html +++ b/mopidy_musicbox_webclient/static/index.html @@ -395,14 +395,10 @@

              Search for artists, albums, or specific tracks. - + - diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index 89ee24b..525809d 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -95,9 +95,9 @@ var uriClassList = [ var uriHumanList = [ ['spotify', 'Spotify'], - ['spotifytunigo', 'Spotify Browse'], - ['local', 'Local Files'], - ['m3u', 'Local Playlists'], + ['spotifytunigo', 'Spotify browse'], + ['local', 'Local files'], + ['m3u', 'Local playlists'], ['podcast', 'Podcasts'], ['dirble', 'Dirble'], ['tunein', 'TuneIn'], @@ -249,7 +249,7 @@ function renderSongLiDivider (track, nextTrack, currentIndex, target) { if (hasSameAlbum(track, nextTrack)) { // Large divider with album cover $(target).before( - '

            • ' + + '
            • ' + '' + '

              ' + track.album.name + '

              ' + renderSongLiTrackArtists(track) + '

            • ' @@ -482,11 +482,11 @@ function getMediaClass (uri) { function getMediaHuman (uri) { var scheme = getScheme(uri) for (var i = 0; i < uriHumanList.length; i++) { - if (scheme === uriHumanList[i][0]) { + if (scheme.toLowerCase() === uriHumanList[i][0].toLowerCase()) { return uriHumanList[i][1] } } - return '' + return uri } function isServiceUri (uri) { diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index 7eff64c..57fedd5 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -117,7 +117,7 @@ function setSongInfo (data) { if (data.track.artists) { for (var j = 0; j < data.track.artists.length; j++) { - artistshtml += '' + data.track.artists[j].name + '' + artistshtml += '' + data.track.artists[j].name + '' artiststext += data.track.artists[j].name if (j !== data.track.artists.length - 1) { artistshtml += ', ' @@ -127,7 +127,7 @@ function setSongInfo (data) { arttmp = artistshtml } if (data.track.album && data.track.album.name) { - $('#modalalbum').html('' + data.track.album.name + '') + $('#modalalbum').html('' + data.track.album.name + '') coverArt.getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge') } else { $('#modalalbum').html('') @@ -171,14 +171,14 @@ function popupTracks (e, listuri, trackuri, tlid) { if (popupData[trackuri].artists) { if (popupData[trackuri].artists.length === 1) { - child = 'Show Artist' + child = 'Show Artist' $('.popupArtistName').html(popupData[trackuri].artists[0].name) - $('.popupArtistHref').attr('onclick', 'showArtist("' + popupData[trackuri].artists[0].uri + '");') + $('.popupArtistHref').attr('onclick', 'library.showArtist("' + popupData[trackuri].artists[0].uri + '");') $('.popupArtistsDiv').hide() $('.popupArtistsLi').show() } else { for (var j = 0; j < popupData[trackuri].artists.length; j++) { - child += '
            • ' + popupData[trackuri].artists[j].name + '
            • ' + child += '
            • ' + popupData[trackuri].artists[j].name + '
            • ' } $('.popupArtistsLi').hide() $('.popupArtistsLv').html(child).show() @@ -222,7 +222,7 @@ function popupTracks (e, listuri, trackuri, tlid) { function showAlbumPopup (popupId) { uri = $(popupId).data('track') - showAlbum(popupData[uri].album.uri) + library.showAlbum(popupData[uri].album.uri) } /** ******************** @@ -232,14 +232,14 @@ function showAlbumPopup (popupId) { function initSocketevents () { mopidy.on('state:online', function () { showOffline(false) - getCurrentPlaylist() + library.getCurrentPlaylist() updateStatusOfAll() - getPlaylists() + library.getPlaylists() getUriSchemes().then(function () { showFavourites() }) - getBrowseDir() - getSearchSchemes() + library.getBrowseDir() + library.getSearchSchemes() showLoading(false) $(window).hashchange() }) @@ -258,21 +258,21 @@ function initSocketevents () { mopidy.on('event:playlistsLoaded', function (data) { showLoading(true) - getPlaylists() + library.getPlaylists() }) mopidy.on('event:playlistChanged', function (data) { $('#playlisttracksdiv').hide() $('#playlistslistdiv').show() delete playlists[data.playlist.uri] - getPlaylists() + library.getPlaylists() }) mopidy.on('event:playlistDeleted', function (data) { $('#playlisttracksdiv').hide() $('#playlistslistdiv').show() delete playlists[data.uri] - getPlaylists() + library.getPlaylists() }) mopidy.on('event:volumeChanged', function (data) { @@ -296,7 +296,7 @@ function initSocketevents () { }) mopidy.on('event:tracklistChanged', function (data) { - getCurrentPlaylist() + library.getCurrentPlaylist() }) mopidy.on('event:seeked', function (data) { @@ -424,7 +424,7 @@ function locationHashChanged () { $('#navsearch a').addClass($.mobile.activeBtnClass) $('#searchinput').focus() if (customTracklists['mbw:allresultscache'] === '') { - initSearch($('#searchinput').val()) + library.initSearch($('#searchinput').val()) } break case 'stream': @@ -432,12 +432,12 @@ function locationHashChanged () { break case 'artists': if (uri !== '') { - showArtist(uri) + library.showArtist(uri) } break case 'albums': if (uri !== '') { - showAlbum(uri) + library.showAlbum(uri) } break } diff --git a/mopidy_musicbox_webclient/static/js/images.js b/mopidy_musicbox_webclient/static/js/images.js index 0a1b531..7da092b 100644 --- a/mopidy_musicbox_webclient/static/js/images.js +++ b/mopidy_musicbox_webclient/static/js/images.js @@ -77,7 +77,7 @@ var coverArt = { } } }, error: function (code, message) { - console.log('Error retrieving album info from last.fm', code, message) + console.error('Error retrieving album info from last.fm', code, message) }}) }, @@ -94,7 +94,7 @@ var coverArt = { } } }, error: function (code, message) { - console.log('Error retrieving artist info from last.fm', code, message) + console.error('Error retrieving artist info from last.fm', code, message) }}) } } diff --git a/mopidy_musicbox_webclient/static/js/library.js b/mopidy_musicbox_webclient/static/js/library.js index 5047c5d..4bf0387 100644 --- a/mopidy_musicbox_webclient/static/js/library.js +++ b/mopidy_musicbox_webclient/static/js/library.js @@ -1,348 +1,344 @@ +var library = { + /** ******************************* * Search *********************************/ -function searchPressed (key) { - var value = $('#searchinput').val() - switchContent('search') + searchPressed: function (key) { + var value = $('#searchinput').val() + switchContent('search') - if (key === 13) { - initSearch() - return false - } - return true -} - -// init search -function initSearch () { - var value = $('#searchinput').val() - var searchService = $('#selectSearchService').val() - - if ((value.length < 100) && (value.length > 0)) { - showLoading(true) - // hide ios/android keyboard - document.activeElement.blur() - $('input').blur() - - delete customTracklists[URI_SCHEME + ':allresultscache'] - delete customTracklists[URI_SCHEME + ':artistresultscache'] - delete customTracklists[URI_SCHEME + ':albumresultscache'] - delete customTracklists[URI_SCHEME + ':trackresultscache'] - $('#searchartists').hide() - $('#searchalbums').hide() - $('#searchtracks').hide() - - if (searchService !== 'all') { - mopidy.library.search({'query': {any: [value]}, 'uris': [searchService + ':']}).then(processSearchResults, console.error) - } else { - mopidy.getUriSchemes().then(function (schemes) { - var query = {} - var uris = [] - - var regexp = $.map(schemes, function (scheme) { - return '^' + scheme + ':' - }).join('|') - - var match = value.match(regexp) - if (match) { - var scheme = match[0] - query = {uri: [value]} - uris = [scheme] - } else { - query = {any: [value]} - } - mopidy.library.search({'query': query, 'uris': uris}).then(processSearchResults, console.error) - }) + if (key === 13) { + library.initSearch() + return false } - } -} + return true + }, + + // init search + initSearch: function () { + var value = $('#searchinput').val() + var searchService = $('#selectSearchService').val() + $.cookie('searchScheme', searchService, { expires: 365 }) + + if ((value.length < 100) && (value.length > 0)) { + showLoading(true) + // hide ios/android keyboard + document.activeElement.blur() + $('input').blur() + + delete customTracklists[URI_SCHEME + ':allresultscache'] + delete customTracklists[URI_SCHEME + ':artistresultscache'] + delete customTracklists[URI_SCHEME + ':albumresultscache'] + delete customTracklists[URI_SCHEME + ':trackresultscache'] + $('#searchartists').hide() + $('#searchalbums').hide() + $('#searchtracks').hide() + + if (searchService !== 'all') { + mopidy.library.search({'query': {any: [value]}, 'uris': [searchService + ':']}).then(library.processSearchResults, console.error) + } else { + mopidy.getUriSchemes().then(function (schemes) { + var query = {} + var uris = [] + + var regexp = $.map(schemes, function (scheme) { + return '^' + scheme + ':' + }).join('|') + + var match = value.match(regexp) + if (match) { + var scheme = match[0] + query = {uri: [value]} + uris = [scheme] + } else { + query = {any: [value]} + } + mopidy.library.search({'query': query, 'uris': uris}).then(library.processSearchResults, console.error) + }) + } + } + }, /** ****************************************************** * process results of a search *********************************************************/ + processSearchResults: function (resultArr) { + $(SEARCH_TRACK_TABLE).empty() + $(SEARCH_ARTIST_TABLE).empty() + $(SEARCH_ALBUM_TABLE).empty() -// # speed clone http://jsperf.com/cloning-an-object/2 -function clone (obj) { - var target = {} - for (var i in obj) { - if (obj.hasOwnProperty(i)) { - target[i] = obj[i] - } - } - return target -} + // Merge results from different backends. + // TODO should of coures have multiple tables + var results = {'tracks': [], 'artists': [], 'albums': []} + var i, j + var emptyResult = true -function processSearchResults (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 + 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 + } } - } - 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].artists) { + for (j = 0; j < resultArr[i].artists.length; j++) { + results.artists.push(resultArr[i].artists[j]) + emptyResult = false + } } - } - if (resultArr[i].albums) { - for (j = 0; j < resultArr[i].albums.length; j++) { - results.albums.push(resultArr[i].albums[j]) - emptyResult = false - } - } - } - - customTracklists[URI_SCHEME + ':trackresultscache'] = results.tracks - - if (emptyResult) { - $('#searchtracks').show() - $(SEARCH_TRACK_TABLE).append( - '
            • No tracks found...

            • ' - ) - toast('No results') - showLoading(false) - return false - } - - if (results.artists.length > 0) { - $('#searchartists').show() - } - - if (results.albums.length > 0) { - $('#searchalbums').show() - } - - if (results.tracks.length > 0) { - $('#searchtracks').show() - } - - // Returns a string where {x} in template is replaced by tokens[x]. - function theme (template, tokens) { - return template.replace(/{[^}]+}/g, function (match) { - return tokens[match.slice(1, -1)] - }) - } - - // 'Show more' pattern - var showMorePattern = '
            • Show {count} more
            • ' - - // Artist results - var child = '' - var pattern = '
            • {name}
            • ' - var tokens - - for (i = 0; i < results.artists.length; i++) { - tokens = { - 'id': results.artists[i].uri, - 'name': results.artists[i].name, - 'class': getMediaClass(results.artists[i].uri) - } - - // Add 'Show all' item after a certain number of hits. - if (i === 4 && results.artists.length > 5) { - child += theme(showMorePattern, {'count': results.artists.length - i}) - pattern = pattern.replace('
            • ', '
            • ') - } - - child += theme(pattern, tokens) - } - - // Inject list items, refresh listview and hide superfluous items. - $(SEARCH_ARTIST_TABLE).html(child).listview('refresh').find('.overflow').hide() - - // Album results - child = '' - pattern = '
            • ' - pattern += '
              {albumName}
              ' - pattern += '

              {artistName}

              ' - pattern += '
            • ' - - for (i = 0; i < results.albums.length; i++) { - tokens = { - 'albumId': results.albums[i].uri, - 'albumName': results.albums[i].name, - 'artistName': '', - 'albumYear': results.albums[i].date, - 'class': getMediaClass(results.albums[i].uri) - } - if (results.albums[i].artists) { - for (j = 0; j < results.albums[i].artists.length; j++) { - if (results.albums[i].artists[j].name) { - tokens.artistName += results.albums[i].artists[j].name + ' ' + if (resultArr[i].albums) { + for (j = 0; j < resultArr[i].albums.length; j++) { + results.albums.push(resultArr[i].albums[j]) + emptyResult = false } } } - 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('
            • ', '
            • ') + + customTracklists[URI_SCHEME + ':trackresultscache'] = results.tracks + + if (emptyResult) { + $('#searchtracks').show() + $(SEARCH_TRACK_TABLE).append( + '
            • No tracks found...

            • ' + ) + toast('No results') + showLoading(false) + return false } - child += theme(pattern, tokens) - } - // Inject list items, refresh listview and hide superfluous items. - $(SEARCH_ALBUM_TABLE).html(child).listview('refresh').find('.overflow').hide() + if (results.artists.length > 0) { + $('#searchartists').show() + } - // Track results - resultsToTables(results.tracks, SEARCH_TRACK_TABLE, URI_SCHEME + ':trackresultscache') + if (results.albums.length > 0) { + $('#searchalbums').show() + } - showLoading(false) -} + if (results.tracks.length > 0) { + $('#searchtracks').show() + } -function toggleSearch () { - $('#albumresulttable tr').removeClass('hidden') - $('#artistresulttable tr').removeClass('hidden') -} + // Returns a string where {x} in template is replaced by tokens[x]. + function theme (template, tokens) { + return template.replace(/{[^}]+}/g, function (match) { + return tokens[match.slice(1, -1)] + }) + } + + // 'Show more' pattern + var showMorePattern = '
            • Show {count} more
            • ' + + // Artist results + var child = '' + var pattern = '
            • {name}
            • ' + var tokens + + for (i = 0; i < results.artists.length; i++) { + tokens = { + 'id': results.artists[i].uri, + 'name': results.artists[i].name, + 'class': getMediaClass(results.artists[i].uri) + } + + // Add 'Show all' item after a certain number of hits. + if (i === 4 && results.artists.length > 5) { + child += theme(showMorePattern, {'count': results.artists.length - i}) + pattern = pattern.replace('
            • ', '
            • ') + } + + child += theme(pattern, tokens) + } + + // Inject list items, refresh listview and hide superfluous items. + $(SEARCH_ARTIST_TABLE).html(child).listview('refresh').find('.overflow').hide() + + // Album results + child = '' + pattern = '
            • ' + pattern += '
              {albumName}
              ' + pattern += '

              {artistName}

              ' + pattern += '
            • ' + + for (i = 0; i < results.albums.length; i++) { + tokens = { + 'albumId': results.albums[i].uri, + 'albumName': results.albums[i].name, + 'artistName': '', + 'albumYear': results.albums[i].date, + 'class': getMediaClass(results.albums[i].uri) + } + if (results.albums[i].artists) { + for (j = 0; j < results.albums[i].artists.length; j++) { + if (results.albums[i].artists[j].name) { + tokens.artistName += results.albums[i].artists[j].name + ' ' + } + } + } + if (tokens.albumYear) { + tokens.artistName += '(' + tokens.albumYear + ')' + } + // Add 'Show all' item after a certain number of hits. + if (i === 4 && results.albums.length > 5) { + child += theme(showMorePattern, {'count': results.albums.length - i}) + pattern = pattern.replace('
            • ', '
            • ') + } + + child += theme(pattern, tokens) + } + // 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) + }, -function getPlaylists () { - // 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) + }, -function getBrowseDir (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) -} - -function getCurrentPlaylist () { - mopidy.tracklist.getTlTracks().then(processCurrentPlaylist, console.error) -} + getCurrentPlaylist: function () { + mopidy.tracklist.getTlTracks().then(processCurrentPlaylist, console.error) + }, /** ****************************************************** * Show tracks of playlist ********************************************************/ -function togglePlaylists () { - if ($(window).width() <= 960) { - $('#playlisttracksdiv').toggle(); - // Hide other div - ($('#playlisttracksdiv').is(':visible')) ? $('#playlistslistdiv').hide() : $('#playlistslistdiv').show() - } else { - $('#playlisttracksdiv').show() - $('#playlistslistdiv').show() - } - return true -} - -function showTracklist (uri) { - showLoading(true) - $(PLAYLIST_TABLE).empty() - togglePlaylists() - var tracks = getPlaylistTracks(uri).then(function (tracks) { - resultsToTables(tracks, PLAYLIST_TABLE, uri, 'return togglePlaylists();', true) - showLoading(false) - }) - updatePlayIcons(uri) - $('#playlistslist li a').each(function () { - $(this).removeClass('playlistactive') - if (this.id === uri) { - $(this).addClass('playlistactive') + togglePlaylists: function () { + if ($(window).width() <= 960) { + $('#playlisttracksdiv').toggle(); + // Hide other div + ($('#playlisttracksdiv').is(':visible')) ? $('#playlistslistdiv').hide() : $('#playlistslistdiv').show() + } else { + $('#playlisttracksdiv').show() + $('#playlistslistdiv').show() } - }) - return false -} + return true + }, -/** **** +/** ********** * Lookups - */ - -function showArtist (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 -} - -function showAlbum (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 { + ************/ + showTracklist: function (uri) { 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) + $(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 () { + var backendName + var searchScheme = $.cookie('searchScheme') + if (searchScheme) { + searchScheme = searchScheme.replace(/"/g, '') + } else { + searchScheme = 'all' + } + $('#selectSearchService').empty() + $('#selectSearchService').append(new Option('All backends', 'all')) + mopidy.getUriSchemes().then(function (schemesArray) { + for (var i = 0; i < schemesArray.length; i++) { + backendName = getMediaHuman(schemesArray[i]) + backendName = backendName.charAt(0).toUpperCase() + backendName.slice(1) + $('#selectSearchService').append(new Option(backendName, schemesArray[i])) + } + $('#selectSearchService').val(searchScheme) + $('#selectSearchService').selectmenu('refresh', true) }, console.error) } - // show page - switchContent('albums', uri) - scrollToTop() - return false } -function getSearchSchemes () { - mopidy.getUriSchemes().then( - function (schemesArray) { - var humanIndex - $('#selectSearchService').children().remove().end() - $('#selectSearchService').append(new Option('All services', 'all')) - for (var i = 0; i < schemesArray.length; i++) { - for (var j = 0; j < uriHumanList.length; j++) { - if (uriHumanList[j][0] === schemesArray[i].toLowerCase()) { - $('#selectSearchService').append(new Option(uriHumanList[j][1], schemesArray[i])) - } - } - } - $('#selectSearchService').selectmenu('refresh', true) - }, console.error - ) +// TODO: Remove this once JavaScript codebase has been completely modularized +// in favour of bundling everything using 'browserify'. +if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + module.exports = library + } } diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js index 4c0d287..bbbb7d4 100644 --- a/mopidy_musicbox_webclient/static/js/process_ws.js +++ b/mopidy_musicbox_webclient/static/js/process_ws.js @@ -78,7 +78,7 @@ function processPlaystate (data) { function processBrowseDir (resultArr) { $(BROWSE_TABLE).empty() if (browseStack.length > 0) { - renderSongLiBackButton(resultArr, BROWSE_TABLE, 'return getBrowseDir();') + renderSongLiBackButton(resultArr, BROWSE_TABLE, 'return library.getBrowseDir();') } if (!resultArr || resultArr.length === 0) { showLoading(false) @@ -113,7 +113,7 @@ function processBrowseDir (resultArr) { iconClass = getMediaClass(resultArr[i].uri) } $(BROWSE_TABLE).append( - '
            • ' + + '
            • ' + '

              ' + resultArr[i].name + '

            • ' ) } @@ -163,7 +163,7 @@ function processGetPlaylists (resultArr) { var starred = '' for (var i = 0; i < resultArr.length; i++) { - var li_html = '
            • ' + var li_html = '
            • ' if (isSpotifyStarredPlaylist(resultArr[i])) { starred = li_html + '★ Spotify Starred Tracks
            • ' + tmp } else if (isFavouritesPlaylist(resultArr[i])) { diff --git a/mopidy_musicbox_webclient/static/mb.appcache b/mopidy_musicbox_webclient/static/mb.appcache index 5844c18..a40ec97 100644 --- a/mopidy_musicbox_webclient/static/mb.appcache +++ b/mopidy_musicbox_webclient/static/mb.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# 2016-03-14:v1 +# 2016-03-19:v1 NETWORK: * diff --git a/tests/test_images.js b/tests/test_images.js index 8d52232..0d8769f 100644 --- a/tests/test_images.js +++ b/tests/test_images.js @@ -12,12 +12,6 @@ var coverArt = require('../mopidy_musicbox_webclient/static/js/images.js') var images before(function () { - html = - '' + - '' - $(document).ready(function () { - $(document.body).add(html) - }) mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'})) images = $('') }) @@ -156,7 +150,7 @@ describe('CoverArt', function () { var getInfoStub = sinon.stub(coverArt.lastfm.album, 'getInfo') getInfoStub.yieldsTo('error', 'code', 'message') - var consoleSpy = sinon.spy(console, 'log') + var consoleSpy = sinon.spy(console, 'error') coverArt.getCoverFromLastFm(track, images, '') assert(consoleSpy.calledOnce) @@ -190,7 +184,7 @@ describe('CoverArt', function () { var getInfoStub = sinon.stub(coverArt.lastfm.artist, 'getInfo') getInfoStub.yieldsTo('error', 'code', 'message') - var consoleSpy = sinon.spy(console, 'log') + var consoleSpy = sinon.spy(console, 'error') coverArt.getArtistImage('mockArtist', images, 'small') assert(consoleSpy.calledOnce) diff --git a/tests/test_library.js b/tests/test_library.js new file mode 100644 index 0000000..7895cb2 --- /dev/null +++ b/tests/test_library.js @@ -0,0 +1,54 @@ +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 coverArt = require('../mopidy_musicbox_webclient/static/js/library.js') + +var selectID = '#selectSearchService' +var schemesArray + +before(function () { + $(document.body).append('') + $('#selectSearchService').selectmenu() +}) +describe('Library', function () { + describe('#getSearchSchemes()', function () { + beforeEach(function () { + schemesArray = ['mockScheme1', 'mockScheme2', 'mockScheme3'] + mopidy = { + getUriSchemes: function () { return $.when(schemesArray) } + } + $(selectID).empty() + }) + + it('should add human-readable options for backend schemes', function () { + uriHumanList = [['mockScheme2', 'mockUriHuman2']] + + library.getSearchSchemes() + assert($(selectID).children().length === schemesArray.length + 1) + $(selectID).children(':eq(2)').should.have.text('MockUriHuman2') + }) + + it('should get default value from cookie', function () { + $.cookie('searchScheme', 'mockScheme3') + library.getSearchSchemes() + $(selectID + ' option:selected').should.have.value('mockScheme3') + }) + + it('should default to "all" backends if no cookie is available', function () { + $.removeCookie('searchScheme') + library.getSearchSchemes() + $(selectID + ' option:selected').should.have.value('all') + }) + + it('should capitalize first character of backend schema', function () { + library.getSearchSchemes() + $(selectID).children(':eq(1)').should.have.text('MockScheme1') + }) + }) +}) From 411a3781fdab270984299e47f136abec8395ff57 Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 28 Mar 2016 05:46:26 +0200 Subject: [PATCH 11/38] Combine .js and .py coverage reports. Remove services that cannot be searched from search dropdown. Refactoring and additional unit tests. --- .gitignore | 2 +- .travis.yml | 19 ++++- karma.conf.js | 3 +- mopidy_musicbox_webclient/__init__.py | 4 +- mopidy_musicbox_webclient/static/index.html | 31 ++------ .../static/js/custom_scripting.js | 8 ++ .../static/js/functionsvars.js | 13 ++++ mopidy_musicbox_webclient/static/js/gui.js | 9 ++- .../static/js/library.js | 5 +- mopidy_musicbox_webclient/static/mb.appcache | 4 +- mopidy_musicbox_webclient/web.py | 44 +++++------ mopidy_musicbox_webclient/webclient.py | 46 +++++++++++ setup.py | 2 +- tests/test_extension.py | 39 +++++++--- tests/test_library.js | 2 +- tests/test_web.py | 67 ++++++++++++++++ tests/test_webclient.py | 78 +++++++++++++++++++ tox.ini | 15 +++- 18 files changed, 312 insertions(+), 79 deletions(-) create mode 100644 mopidy_musicbox_webclient/static/js/custom_scripting.js create mode 100644 mopidy_musicbox_webclient/webclient.py create mode 100644 tests/test_web.py create mode 100644 tests/test_webclient.py diff --git a/.gitignore b/.gitignore index e2cf956..e66dbe5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ MANIFEST build/ cover/ -coverage/ +.karma_coverage/ coverage.xml dist/ docs/_build/ diff --git a/.travis.yml b/.travis.yml index c1749cf..c6548b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,18 @@ -sudo: false +sudo: required +dist: trusty language: python python: - "2.7_with_system_site_packages" +addons: + apt: + sources: + - mopidy-stable + packages: + - mopidy + env: - TOX_ENV=py27 - TOX_ENV=flake8 @@ -13,6 +21,11 @@ env: - TOX_ENV=csslint - TOX_ENV=tidy +before_install: + - "sudo sed -i '/127.0.1.1/d' /etc/hosts" # Workaround https://github.com/tornadoweb/tornado/issues/1573 + - "sudo apt-get update -qq" + - "sudo apt-get install -y gstreamer0.10-plugins-good python-gst0.10" + install: - "pip install tox" @@ -20,6 +33,4 @@ script: - "tox -e $TOX_ENV" after_success: - # TODO: find a way to combine .py and .js coverage reports. - # - "if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; fi" - - "if [ $TOX_ENV == 'test' ]; then cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js; fi" + - "if [ $TOX_ENV == 'test' ]; then pip install coveralls; coveralls --merge=./karma_coverage/coverage-final.json; fi" diff --git a/karma.conf.js b/karma.conf.js index c64ba37..11ab253 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -71,9 +71,10 @@ module.exports = function (config) { coverageReporter: { // specify a common output directory - dir: 'coverage/', + dir: '.karma_coverage/', reporters: [ { type: 'lcov', subdir: '.' }, + { type: 'json', subdir: '.' }, { type: 'text' } ] } diff --git a/mopidy_musicbox_webclient/__init__.py b/mopidy_musicbox_webclient/__init__.py index 1884794..89f4c5d 100644 --- a/mopidy_musicbox_webclient/__init__.py +++ b/mopidy_musicbox_webclient/__init__.py @@ -7,7 +7,7 @@ from mopidy import config, ext __version__ = '2.2.0' -class MusicBoxExtension(ext.Extension): +class Extension(ext.Extension): dist_name = 'Mopidy-MusicBox-Webclient' ext_name = 'musicbox_webclient' @@ -18,7 +18,7 @@ class MusicBoxExtension(ext.Extension): return config.read(conf_file) def get_config_schema(self): - schema = super(MusicBoxExtension, self).get_config_schema() + schema = super(Extension, self).get_config_schema() schema['musicbox'] = config.Boolean(optional=True) schema['websocket_host'] = config.Hostname(optional=True) schema['websocket_port'] = config.Port(optional=True) diff --git a/mopidy_musicbox_webclient/static/index.html b/mopidy_musicbox_webclient/static/index.html index 225ae79..ee58e9e 100644 --- a/mopidy_musicbox_webclient/static/index.html +++ b/mopidy_musicbox_webclient/static/index.html @@ -1,30 +1,14 @@ - Musicbox + {{ title }} - - - + + + + + @@ -52,7 +36,7 @@ - +
              @@ -501,7 +485,6 @@
              - diff --git a/mopidy_musicbox_webclient/static/js/custom_scripting.js b/mopidy_musicbox_webclient/static/js/custom_scripting.js new file mode 100644 index 0000000..2b544b6 --- /dev/null +++ b/mopidy_musicbox_webclient/static/js/custom_scripting.js @@ -0,0 +1,8 @@ +// jQuery Mobile configuration options +// see: http://api.jquerymobile.com/1.3/global-config/ +$(document).bind('mobileinit', function () { + $.extend($.mobile, { + ajaxEnabled: false, + hashListeningEnabled: false + }) +}) diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index 525809d..f270607 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -110,6 +110,19 @@ var uriHumanList = [ ['subsonic', 'Subsonic'] ] +// List of Mopidy URI schemes that should not be searched directly. +// Also blacklists 'yt' in favour of using the other 'youtube' supported scheme. +var searchBlacklist = [ + 'file', + 'http', + 'https', + 'mms', + 'rtmp', + 'rtmps', + 'rtsp', + 'yt' +] + function scrollToTop () { var divtop = 0 $('body,html').animate({ diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index 57fedd5..a1e1be6 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -475,6 +475,7 @@ $(document).ready(function (event) { $(window).hashchange() // Connect to server + var websocketUrl = $(document.body).data('websocket-url') if (websocketUrl) { try { mopidy = new Mopidy({ @@ -539,16 +540,16 @@ $(document).ready(function (event) { } }) - // remove buttons only for MusicBox - if (!isMusicBox) { + // Remove MusicBox only content (e.g. settings, system pages) + if (!$(document.body).data('is-musicbox')) { $('#navSettings').hide() $('#navshutdown').hide() $('#homesettings').hide() $('#homeshutdown').hide() } - // remove Alarm Clock if it is not present - if (!hasAlarmClock) { + // Remove Alarm Clock icons if it is not present + if (!$(document.body).data('has-alarmclock')) { $('#navAlarmClock').hide() $('#homeAlarmClock').hide() $('#homeAlarmClock').nextAll().find('.ui-block-a, .ui-block-b').toggleClass('ui-block-a').toggleClass('ui-block-b') diff --git a/mopidy_musicbox_webclient/static/js/library.js b/mopidy_musicbox_webclient/static/js/library.js index 4bf0387..7abd995 100644 --- a/mopidy_musicbox_webclient/static/js/library.js +++ b/mopidy_musicbox_webclient/static/js/library.js @@ -322,8 +322,11 @@ var library = { searchScheme = 'all' } $('#selectSearchService').empty() - $('#selectSearchService').append(new Option('All backends', 'all')) + $('#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) diff --git a/mopidy_musicbox_webclient/static/mb.appcache b/mopidy_musicbox_webclient/static/mb.appcache index a40ec97..c825720 100644 --- a/mopidy_musicbox_webclient/static/mb.appcache +++ b/mopidy_musicbox_webclient/static/mb.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# 2016-03-19:v1 +# 2016-03-28:v1 NETWORK: * @@ -19,8 +19,8 @@ images/icons/play_alt_12x12.png images/icons/play_alt_16x16.png images/loader.gif images/user_24x32.png -index.html js/controls.js +js/custom_scripting.js js/functionsvars.js js/gui.js js/images.js diff --git a/mopidy_musicbox_webclient/web.py b/mopidy_musicbox_webclient/web.py index 093f4f6..e562fde 100644 --- a/mopidy_musicbox_webclient/web.py +++ b/mopidy_musicbox_webclient/web.py @@ -1,10 +1,13 @@ -from __future__ import unicode_literals +from __future__ import absolute_import, division, print_function, unicode_literals + +import json import logging +import string import tornado.web -from . import MusicBoxExtension +import mopidy_musicbox_webclient.webclient as mmw logger = logging.getLogger(__name__) @@ -21,40 +24,31 @@ class StaticHandler(tornado.web.StaticFileHandler): @classmethod def get_version(cls, settings, path): - return MusicBoxExtension.version + return mmw.Extension.version class IndexHandler(tornado.web.RequestHandler): def initialize(self, config, path): - ext_config = config[MusicBoxExtension.ext_name] - host, port = ext_config['websocket_host'], ext_config['websocket_port'] - ws_url = '' - if host or port: - if not host: - host = self.request.host.partition(':')[0] - logger.warning('Musicbox websocket_host not specified, ' - 'using %s', host) - elif not port: - port = config['http']['port'] - logger.warning('Musicbox websocket_port not specified, ' - 'using %s', port) - protocol = 'ws' - if self.request.protocol == 'https': - protocol = 'wss' - ws_url = "%s://%s:%d/mopidy/ws" % (protocol, host, port) + + webclient = mmw.Webclient(config) self.__dict = { - 'version': MusicBoxExtension.version, - 'musicbox': ext_config.get('musicbox', False), - 'useWebsocketUrl': ws_url != '', - 'websocket_url': ws_url, - 'alarmclock': config.get('alarmclock', {}).get('enabled', False), + 'isMusicBox': json.dumps(webclient.is_music_box()), + 'websocketUrl': webclient.get_websocket_url(self.request), + 'hasAlarmClock': json.dumps(webclient.has_alarm_clock()), } self.__path = path + self.__title = string.Template('MusicBox on $hostname') def get(self, path): - return self.render('index.html', **self.__dict) + return self.render(path, title=self.get_title(), **self.__dict) + + def get_title(self): + hostname, sep, port = self.request.host.rpartition(':') + if not sep or not port.isdigit(): + hostname, port = self.request.host, '80' + return self.__title.safe_substitute(hostname=hostname, port=port) def get_template_path(self): return self.__path diff --git a/mopidy_musicbox_webclient/webclient.py b/mopidy_musicbox_webclient/webclient.py new file mode 100644 index 0000000..6db7903 --- /dev/null +++ b/mopidy_musicbox_webclient/webclient.py @@ -0,0 +1,46 @@ +from __future__ import absolute_import, division, print_function, unicode_literals + +import logging + +from mopidy_musicbox_webclient import Extension + +logger = logging.getLogger(__name__) + + +class Webclient(object): + + def __init__(self, config): + self.config = config + + @property + def ext_config(self): + return self.config.get(Extension.ext_name, {}) + + @classmethod + def get_version(cls): + return Extension.version + + def get_websocket_url(self, request): + host, port = self.ext_config['websocket_host'], self.ext_config['websocket_port'] + ws_url = '' + if host or port: + if not host: + host = request.host.partition(':')[0] + logger.warning('Musicbox websocket_host not specified, ' + 'using %s', host) + elif not port: + port = self.config['http']['port'] + logger.warning('Musicbox websocket_port not specified, ' + 'using %s', port) + protocol = 'ws' + if request.protocol == 'https': + protocol = 'wss' + ws_url = "%s://%s:%d/mopidy/ws" % (protocol, host, port) + + return ws_url + + def has_alarm_clock(self): + return self.ext_config.get('alarmclock', {}).get('enabled', False) + + def is_music_box(self): + return self.ext_config.get('musicbox', False) diff --git a/setup.py b/setup.py index 17b680e..286077f 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ setup( ], entry_points={ 'mopidy.ext': [ - 'musicbox_webclient = mopidy_musicbox_webclient:MusicBoxExtension', + 'musicbox_webclient = mopidy_musicbox_webclient:Extension', ], }, classifiers=[ diff --git a/tests/test_extension.py b/tests/test_extension.py index 31c685e..cfaa39d 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -1,22 +1,37 @@ -from __future__ import unicode_literals +from __future__ import absolute_import, division, print_function, unicode_literals -from mopidy_musicbox_webclient import MusicBoxExtension +import unittest + +import mock + +from mopidy_musicbox_webclient import Extension -def test_get_default_config(): - ext = MusicBoxExtension() +class ExtensionTests(unittest.TestCase): - config = ext.get_default_config() + def test_get_default_config(self): + ext = Extension() - assert '[musicbox_webclient]' in config - assert 'enabled = true' in config + config = ext.get_default_config() + assert '[musicbox_webclient]' in config + assert 'enabled = true' in config + assert 'websocket_host =' in config + assert 'websocket_port =' in config -def test_get_config_schema(): - ext = MusicBoxExtension() + def test_get_config_schema(self): + ext = Extension() - schema = ext.get_config_schema() - assert 'musicbox' in schema + schema = ext.get_config_schema() + assert 'musicbox' in schema + assert 'websocket_host' in schema + assert 'websocket_port' in schema -# TODO Write more tests + def test_setup(self): + registry = mock.Mock() + + ext = Extension() + ext.setup(registry) + calls = [mock.call('http:app', {'name': ext.ext_name, 'factory': ext.factory})] + registry.add.assert_has_calls(calls, any_order=True) diff --git a/tests/test_library.js b/tests/test_library.js index 7895cb2..1d3b19f 100644 --- a/tests/test_library.js +++ b/tests/test_library.js @@ -7,7 +7,7 @@ chai.use(require('chai-jquery')) var sinon = require('sinon') -var coverArt = require('../mopidy_musicbox_webclient/static/js/library.js') +var library = require('../mopidy_musicbox_webclient/static/js/library.js') var selectID = '#selectSearchService' var schemesArray diff --git a/tests/test_web.py b/tests/test_web.py new file mode 100644 index 0000000..a77484c --- /dev/null +++ b/tests/test_web.py @@ -0,0 +1,67 @@ +from __future__ import absolute_import, division, print_function, unicode_literals + +import mock + +import mopidy.config as config + +import tornado.testing +import tornado.web +import tornado.websocket + +from mopidy_musicbox_webclient import Extension +from mopidy_musicbox_webclient.web import StaticHandler + + +class BaseTest(tornado.testing.AsyncHTTPTestCase): + + def get_app(self): + extension = Extension() + self.config = config.Proxy({'musicbox_webclient': { + 'enabled': True, + 'musicbox': True, + 'websocket_host': '', + 'websocket_port': '', + } + }) + return tornado.web.Application(extension.factory(self.config, mock.Mock())) + + +class StaticFileHandlerTest(BaseTest): + + def test_static_handler(self): + response = self.fetch('/vendors/mopidy/mopidy.js', method='GET') + + assert response.code == 200 + + def test_get_version(self): + assert StaticHandler.get_version(None, None) == Extension.version + + +class RedirectHandlerTest(BaseTest): + + def test_redirect_handler(self): + response = self.fetch('/', method='GET', follow_redirects=False) + + assert response.code == 301 + response.headers['Location'].endswith('index.html') + + +class IndexHandlerTest(BaseTest): + + def test_index_handler(self): + response = self.fetch('/index.html', method='GET') + assert response.code == 200 + + def test_get_title(self): + response = self.fetch('/index.html', method='GET') + body = tornado.escape.to_unicode(response.body) + + assert 'MusicBox on localhost' in body + + def test_initialize_sets_dictionary_objects(self): + response = self.fetch('/index.html', method='GET') + body = tornado.escape.to_unicode(response.body) + + assert 'data-is-musicbox="true"' in body + assert 'data-has-alarmclock="false"' in body + assert 'data-websocket-url=""' in body diff --git a/tests/test_webclient.py b/tests/test_webclient.py new file mode 100644 index 0000000..1ac2657 --- /dev/null +++ b/tests/test_webclient.py @@ -0,0 +1,78 @@ +from __future__ import absolute_import, division, print_function, unicode_literals + +import unittest + +import mock + +import mopidy.config as mopidy_config + +from mopidy_musicbox_webclient import Extension +from mopidy_musicbox_webclient.webclient import Webclient + + +class WebclientTests(unittest.TestCase): + + def setUp(self): + config = mopidy_config.Proxy( + { + 'musicbox_webclient': { + 'enabled': True, + 'musicbox': False, + 'websocket_host': 'host_mock', + 'websocket_port': 999, + } + }) + + self.ext = Extension() + self.mmw = Webclient(config) + + def test_get_version(self): + assert self.mmw.get_version() == self.ext.version + + def test_get_websocket_url_uses_config_file(self): + assert self.mmw.get_websocket_url(mock.Mock()) == 'ws://host_mock:999/mopidy/ws' + + def test_get_websocket_url_uses_request_host(self): + config = mopidy_config.Proxy( + { + 'musicbox_webclient': { + 'enabled': True, + 'musicbox': False, + 'websocket_host': '', + 'websocket_port': 999, + } + }) + + request_mock = mock.Mock(spec='tornado.HTTPServerRequest') + request_mock.host = '127.0.0.1' + request_mock.protocol = 'https' + + self.mmw.config = config + assert self.mmw.get_websocket_url(request_mock) == 'wss://127.0.0.1:999/mopidy/ws' + + def test_get_websocket_url_uses_http_port(self): + config = mopidy_config.Proxy( + { + 'http': { + 'port': 999 + }, + 'musicbox_webclient': { + 'enabled': True, + 'musicbox': False, + 'websocket_host': '127.0.0.1', + 'websocket_port': '', + } + }) + + request_mock = mock.Mock(spec='tornado.HTTPServerRequest') + request_mock.host = '127.0.0.1' + request_mock.protocol = 'https' + + self.mmw.config = config + assert self.mmw.get_websocket_url(request_mock) == 'wss://127.0.0.1:999/mopidy/ws' + + def test_has_alarmclock(self): + assert not self.mmw.has_alarm_clock() + + def test_is_musicbox(self): + assert not self.mmw.is_music_box() diff --git a/tox.ini b/tox.ini index b6a3ba3..73fd03c 100644 --- a/tox.ini +++ b/tox.ini @@ -2,12 +2,18 @@ envlist = py27, flake8, test, eslint, csslint, tidy [testenv] +sitepackages = true +whitelist_externals = + py.test deps = mock mopidy pytest + pytest-capturelog pytest-cov pytest-xdist + responses +install_command = pip install --allow-unverified=mopidy --pre {opts} {packages} commands = py.test \ --basetemp={envtmpdir} \ @@ -16,13 +22,17 @@ commands = {posargs:tests/} [testenv:flake8] +sitepackages = false deps = flake8 flake8-import-order + pep8-naming skip_install = true -commands = flake8 {posargs:mopidy_musicbox_webclient tests} +commands = flake8 --show-source --statistics --max-line-length 120 {posargs:mopidy_musicbox_webclient tests} [testenv:test] +envdir = py27 +sitepackages = false whitelist_externals = /bin/bash deps = @@ -33,6 +43,7 @@ commands = bash -c '. {toxworkdir}/node_env/bin/activate; npm install; npm test' [testenv:eslint] +sitepackages = false whitelist_externals = /bin/bash deps = @@ -43,6 +54,7 @@ commands = bash -c '. {toxworkdir}/node_env/bin/activate; npm install; npm run eslint' [testenv:csslint] +sitepackages = false whitelist_externals = /bin/bash deps = @@ -53,6 +65,7 @@ commands = bash -c '. {toxworkdir}/node_env/bin/activate; npm install; npm run csslint' [testenv:tidy] +sitepackages = false whitelist_externals = /bin/bash deps = From 7563a52c076e2f1b976f05ebb40cca934bc1e5e3 Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 28 Mar 2016 07:00:56 +0200 Subject: [PATCH 12/38] Fix travis path to coverage report. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c6548b0..1991ff1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,4 +33,4 @@ script: - "tox -e $TOX_ENV" after_success: - - "if [ $TOX_ENV == 'test' ]; then pip install coveralls; coveralls --merge=./karma_coverage/coverage-final.json; fi" + - "if [ $TOX_ENV == 'test' ]; then pip install coveralls; coveralls --merge=.karma_coverage/coverage-final.json; fi" From aa772ce955db9f5fcb6c12460c296de2e3b86ff4 Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 28 Mar 2016 09:04:33 +0200 Subject: [PATCH 13/38] Combine 'py27' and 'test' runs. --- .travis.yml | 12 +++++++++--- karma.conf.js | 1 - tox.ini | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1991ff1..ac120d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,9 +14,9 @@ addons: - mopidy env: - - TOX_ENV=py27 + # Need to run 'py27' and 'test' together so that we can share coverage reports. + - TOX_ENV=py27,test - TOX_ENV=flake8 - - TOX_ENV=test - TOX_ENV=eslint - TOX_ENV=csslint - TOX_ENV=tidy @@ -28,9 +28,15 @@ before_install: install: - "pip install tox" + - "gem install coveralls-lcov" + +before_script: + - "./autogen.sh" + - "./configure --enable-coverage" script: + - "make check" - "tox -e $TOX_ENV" after_success: - - "if [ $TOX_ENV == 'test' ]; then pip install coveralls; coveralls --merge=.karma_coverage/coverage-final.json; fi" + - "if [ $TOX_ENV == 'py27,test' ]; then coveralls-lcov -v -n .karma_coverage/lcov.info > .karma_coverage/lcov.json; pip install coveralls; coveralls --merge=.karma_coverage/lcov.json; fi" diff --git a/karma.conf.js b/karma.conf.js index 11ab253..57f7fa3 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -74,7 +74,6 @@ module.exports = function (config) { dir: '.karma_coverage/', reporters: [ { type: 'lcov', subdir: '.' }, - { type: 'json', subdir: '.' }, { type: 'text' } ] } diff --git a/tox.ini b/tox.ini index 73fd03c..22ff53e 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = py27, flake8, test, eslint, csslint, tidy [testenv] +passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH sitepackages = true whitelist_externals = py.test @@ -31,7 +32,6 @@ skip_install = true commands = flake8 --show-source --statistics --max-line-length 120 {posargs:mopidy_musicbox_webclient tests} [testenv:test] -envdir = py27 sitepackages = false whitelist_externals = /bin/bash From a21dc9a71787bcc727639b53c9963c8b9830e845 Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 28 Mar 2016 09:10:43 +0200 Subject: [PATCH 14/38] Combine 'py27' and 'test' runs. --- .travis.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index ac120d5..0c77fdd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,15 +28,10 @@ before_install: install: - "pip install tox" - - "gem install coveralls-lcov" - -before_script: - - "./autogen.sh" - - "./configure --enable-coverage" script: - - "make check" - "tox -e $TOX_ENV" after_success: - - "if [ $TOX_ENV == 'py27,test' ]; then coveralls-lcov -v -n .karma_coverage/lcov.info > .karma_coverage/lcov.json; pip install coveralls; coveralls --merge=.karma_coverage/lcov.json; fi" + # We just use 'coveralls-lcov' to convert lcov.info to JSON format. + - "if [ $TOX_ENV == 'py27,test' ]; then gem install coveralls-lcov; coveralls-lcov -v -n .karma_coverage/lcov.info > .karma_coverage/lcov.json; pip install coveralls; coveralls --merge=.karma_coverage/lcov.json; fi" From 7eea023310333dbfbae4bf1e846ef3dd8d029a7d Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 28 Mar 2016 09:15:34 +0200 Subject: [PATCH 15/38] Convert lcov.info to JSON format before submitting to coveralls. --- .travis.yml | 7 ++++--- karma.conf.js | 1 - tox.ini | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1991ff1..0c77fdd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,9 +14,9 @@ addons: - mopidy env: - - TOX_ENV=py27 + # Need to run 'py27' and 'test' together so that we can share coverage reports. + - TOX_ENV=py27,test - TOX_ENV=flake8 - - TOX_ENV=test - TOX_ENV=eslint - TOX_ENV=csslint - TOX_ENV=tidy @@ -33,4 +33,5 @@ script: - "tox -e $TOX_ENV" after_success: - - "if [ $TOX_ENV == 'test' ]; then pip install coveralls; coveralls --merge=.karma_coverage/coverage-final.json; fi" + # We just use 'coveralls-lcov' to convert lcov.info to JSON format. + - "if [ $TOX_ENV == 'py27,test' ]; then gem install coveralls-lcov; coveralls-lcov -v -n .karma_coverage/lcov.info > .karma_coverage/lcov.json; pip install coveralls; coveralls --merge=.karma_coverage/lcov.json; fi" diff --git a/karma.conf.js b/karma.conf.js index 11ab253..57f7fa3 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -74,7 +74,6 @@ module.exports = function (config) { dir: '.karma_coverage/', reporters: [ { type: 'lcov', subdir: '.' }, - { type: 'json', subdir: '.' }, { type: 'text' } ] } diff --git a/tox.ini b/tox.ini index 73fd03c..22ff53e 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = py27, flake8, test, eslint, csslint, tidy [testenv] +passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH sitepackages = true whitelist_externals = py.test @@ -31,7 +32,6 @@ skip_install = true commands = flake8 --show-source --statistics --max-line-length 120 {posargs:mopidy_musicbox_webclient tests} [testenv:test] -envdir = py27 sitepackages = false whitelist_externals = /bin/bash From 9347c7d5575b9a32f34d9f28b4a068f9bca99a21 Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 28 Mar 2016 13:57:25 +0200 Subject: [PATCH 16/38] Upgrade media progress timer. Modularise progress_timer.js. --- README.rst | 1 + karma.conf.js | 1 - mopidy_musicbox_webclient/static/index.html | 2 +- .../static/js/controls.js | 6 +- .../static/js/functionsvars.js | 3 +- mopidy_musicbox_webclient/static/js/gui.js | 24 +- mopidy_musicbox_webclient/static/js/images.js | 194 +++--- .../static/js/library.js | 639 +++++++++--------- .../static/js/process_ws.js | 4 +- .../static/js/progress_timer.js | 177 ----- .../static/js/synced_timer.js | 218 ++++++ mopidy_musicbox_webclient/static/mb.appcache | 4 +- .../vendors/media_progress_timer/timer.js | 265 ++++---- tests/{ => js}/test_images.js | 94 ++- tests/js/test_library.js | 56 ++ tests/js/test_synced_timer.js | 352 ++++++++++ tests/test_library.js | 54 -- 17 files changed, 1253 insertions(+), 841 deletions(-) delete mode 100644 mopidy_musicbox_webclient/static/js/progress_timer.js create mode 100644 mopidy_musicbox_webclient/static/js/synced_timer.js rename tests/{ => js}/test_images.js (71%) create mode 100644 tests/js/test_library.js create mode 100644 tests/js/test_synced_timer.js delete mode 100644 tests/test_library.js diff --git a/README.rst b/README.rst index 58c12b0..5d3a973 100644 --- a/README.rst +++ b/README.rst @@ -77,6 +77,7 @@ v2.3.0 (UNRELEASED) - Now displays album and artist info when browsing tracks. (Addresses: `#99 `_). - Now remembers which backend was searched previously, and automatically selects that backend as the default search target. (Addresses: `#130 `_). +- Upgrade Media Progress Timer to version 3.0.0. **Fixes** diff --git a/karma.conf.js b/karma.conf.js index 57f7fa3..7f456f9 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -13,7 +13,6 @@ module.exports = function (config) { // list of files / patterns to load in the browser files: [ 'mopidy_musicbox_webclient/static/vendors/**/*.js', - // TODO: can remove the next line once JavaScript codebase has been modularized. 'mopidy_musicbox_webclient/static/js/**/*.js', 'tests/**/test_*.js' ], diff --git a/mopidy_musicbox_webclient/static/index.html b/mopidy_musicbox_webclient/static/index.html index ee58e9e..4c4b832 100644 --- a/mopidy_musicbox_webclient/static/index.html +++ b/mopidy_musicbox_webclient/static/index.html @@ -486,7 +486,7 @@ - + diff --git a/mopidy_musicbox_webclient/static/js/controls.js b/mopidy_musicbox_webclient/static/js/controls.js index 3db4a6d..b1bcc9b 100644 --- a/mopidy_musicbox_webclient/static/js/controls.js +++ b/mopidy_musicbox_webclient/static/js/controls.js @@ -288,13 +288,13 @@ function setPlayState (nwplay) { $('#btplay >i').removeClass('fa-play').addClass('fa-pause') $('#btplay').attr('title', 'Pause') mopidy.playback.getTimePosition().then(processCurrentposition, console.error) - startProgressTimer() + syncedProgressTimer.start() } else { $('#btplayNowPlaying >i').removeClass('fa-pause').addClass('fa-play') $('#btplayNowPlaying').attr('title', 'Play') $('#btplay >i').removeClass('fa-pause').addClass('fa-play') $('#btplay').attr('title', 'Play') - progressTimer.stop() + syncedProgressTimer.stop() } play = nwplay } @@ -396,7 +396,7 @@ function doSeekPos (value) { function setPosition (pos) { if (!positionChanging && $('#trackslider').val() !== pos) { - setProgressTimer(pos) + syncedProgressTimer.set(pos) } } diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index f270607..ae1634a 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -5,6 +5,7 @@ */ var mopidy +var syncedProgressTimer // values for controls var play = false @@ -268,7 +269,7 @@ function renderSongLiDivider (track, nextTrack, currentIndex, target) { renderSongLiTrackArtists(track) + '

              ' ) // Retrieve album covers - coverArt.getCover(track.uri, getjQueryID(target + '-cover', track.uri, true), 'small') + coverArt.getCover(track.uri, getjQueryID(target + '-cover', track.uri, true), 'small', mopidy) } else if (currentIndex > 0) { // Small divider $(target).before('
            •  
            • ') diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index a1e1be6..932ee1a 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -128,7 +128,7 @@ function setSongInfo (data) { } if (data.track.album && data.track.album.name) { $('#modalalbum').html('' + data.track.album.name + '') - coverArt.getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge') + coverArt.getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge', mopidy) } else { $('#modalalbum').html('') $('#infocover').attr('src', 'images/default_cover.png') @@ -139,10 +139,9 @@ function setSongInfo (data) { $('#trackslider').attr('min', 0) $('#trackslider').attr('max', songlength) - resetProgressTimer() - progressTimer.set(0, songlength) + syncedProgressTimer.reset().set(0, songlength) if (play) { - startProgressTimer() + syncedProgressTimer.start() } resizeMb() @@ -239,7 +238,7 @@ function initSocketevents () { showFavourites() }) library.getBrowseDir() - library.getSearchSchemes() + library.getSearchSchemes(searchBlacklist, mopidy) showLoading(false) $(window).hashchange() }) @@ -302,7 +301,7 @@ function initSocketevents () { mopidy.on('event:seeked', function (data) { setPosition(parseInt(data.time_position)) if (play) { - startProgressTimer() + syncedProgressTimer.start() } }) @@ -496,9 +495,7 @@ $(document).ready(function (event) { // initialize events initSocketevents() - progressTimer = new ProgressTimer({ - callback: timerCallback - }) + syncedProgressTimer = new SyncedProgressTimer(8, mopidy) resetSong() @@ -593,13 +590,13 @@ $(document).ready(function (event) { // swipe songinfo and panel $('#normalFooter, #nowPlayingFooter').on('swiperight', doPrevious) $('#normalFooter, #nowPlayingFooter').on('swipeleft', doNext) - $('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swiperight', function () { + $('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swiperight', function (event) { if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) { $('#panel').panel('open') event.stopImmediatePropagation() } }) - $('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swipeleft', function () { + $('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swipeleft', function (event) { if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) { $('#panel').panel('close') event.stopImmediatePropagation() @@ -607,12 +604,13 @@ $(document).ready(function (event) { }) $('#trackslider').on('slidestart', function () { - progressTimer.stop() - $('#trackslider').on('change', function () { updatePosition($(this).val()) }) + syncedProgressTimer.stop() + $('#trackslider').on('change', function () { syncedProgressTimer.updatePosition($(this).val()) }) }) $('#trackslider').on('slidestop', function () { $('#trackslider').off('change') + syncedProgressTimer.updatePosition($(this).val()) doSeekPos($(this).val()) }) diff --git a/mopidy_musicbox_webclient/static/js/images.js b/mopidy_musicbox_webclient/static/js/images.js index 7da092b..8bef0c3 100644 --- a/mopidy_musicbox_webclient/static/js/images.js +++ b/mopidy_musicbox_webclient/static/js/images.js @@ -1,110 +1,108 @@ -/** - * @author Wouter van Wijk - */ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory) + } else if (typeof module === 'object' && module.exports) { + module.exports = factory() + } else { + root.coverArt = factory() + } +}(this, function () { + 'use strict' -API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733' -API_SECRET = '2c631802c2285d5d5d1502462fe42a2b' + var API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733' + var API_SECRET = '2c631802c2285d5d5d1502462fe42a2b' -var coverArt = { - fmcache: new LastFMCache(), - lastfm: new LastFM({ - apiKey: API_KEY, - apiSecret: API_SECRET, - cache: this.fmcache - }), + var coverArt = { + lastfm: new LastFM({ + apiKey: API_KEY, + apiSecret: API_SECRET, + cache: new LastFMCache() + }), - getCover: function (uri, images, size) { - var defUrl = 'images/default_cover.png' - $(images).attr('src', defUrl) - if (!uri) { - return - } - mopidy.library.getImages({'uris': [uri]}).then(function (imageResults) { - var uri = Object.keys(imageResults)[0] - if (imageResults[uri].length > 0) { - $(images).attr('src', imageResults[uri][0].uri) - } else { - // Also check deprecated 'album.images' in case backend does not - // implement mopidy.library.getImages yet... - coverArt.getCoverFromAlbum(uri, images, size) - } - }) - }, - - // Note that this approach has been deprecated in Mopidy - // TODO: Remove when Mopidy no longer supports getting images - // with 'album.images'. - getCoverFromAlbum: function (uri, images, size) { - var defUrl = 'images/default_cover.png' - $(images).attr('src', defUrl) - if (!uri) { - return - } - mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) { - var uri = Object.keys(resultDict)[0] - var track = resultDict[uri][0] - if (track && track.album && track.album.images && track.album.images.length > 0) { - $(images).attr('src', track.album.images[0]) - } else if (track && (track.album || track.artist)) { - // Fallback to last.fm - coverArt.getCoverFromLastFm(track, images, size) - } else { + getCover: function (uri, images, size, mopidy) { + var defUrl = 'images/default_cover.png' + $(images).attr('src', defUrl) + if (!uri) { return } - }) - }, - - getCoverFromLastFm: function (track, images, size) { - var defUrl = 'images/default_cover.png' - $(images).attr('src', defUrl) - if (!track || !(track.album || track.artists)) { - return - } - var albumname = (track.album && track.album.name) ? track.album.name : '' - var artistname = '' - if (track.album && track.album.artists && track.album.artists.length > 0) { - // First look for the artist in the album - artistname = track.album.artists[0].name - } else if (track.artists && (track.artists.length > 0)) { - // Fallback to using artists for specific track - artistname = track.artists[0].name - } - - this.lastfm.album.getInfo({artist: artistname, album: albumname}, {success: function (data) { - for (var i = 0; i < data.album.image.length; i++) { - if (data.album.image[i].size === size) { - $(images).attr('src', data.album.image[i]['#text'] || defUrl) + mopidy.library.getImages({'uris': [uri]}).then(function (imageResults) { + var uri = Object.keys(imageResults)[0] + if (imageResults[uri].length > 0) { + $(images).attr('src', imageResults[uri][0].uri) + } else { + // Also check deprecated 'album.images' in case backend does not + // implement mopidy.library.getImages yet... + coverArt.getCoverFromAlbum(uri, images, size, mopidy) } - } - }, error: function (code, message) { - console.error('Error retrieving album info from last.fm', code, message) - }}) - }, + }) + }, - getArtistImage: function (artist, images, size) { - var defUrl = 'images/user_24x32.png' - $(images).attr('src', defUrl) - if (!artist || artist.length === 0) { - return - } - this.lastfm.artist.getInfo({artist: artist}, {success: function (data) { - for (var i = 0; i < data.artist.image.length; i++) { - if (data.artist.image[i].size === size) { - $(images).attr('src', data.artist.image[i]['#text'] || defUrl) + // Note that this approach has been deprecated in Mopidy + // TODO: Remove when Mopidy no longer supports getting images + // with 'album.images'. + getCoverFromAlbum: function (uri, images, size, mopidy) { + var defUrl = 'images/default_cover.png' + $(images).attr('src', defUrl) + if (!uri) { + return + } + mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) { + var uri = Object.keys(resultDict)[0] + var track = resultDict[uri][0] + if (track && track.album && track.album.images && track.album.images.length > 0) { + $(images).attr('src', track.album.images[0]) + } else if (track && (track.album || track.artist)) { + // Fallback to last.fm + coverArt.getCoverFromLastFm(track, images, size) + } else { + return } + }) + }, + + getCoverFromLastFm: function (track, images, size) { + var defUrl = 'images/default_cover.png' + $(images).attr('src', defUrl) + if (!track || !(track.album || track.artists)) { + return + } + var albumname = (track.album && track.album.name) ? track.album.name : '' + var artistname = '' + if (track.album && track.album.artists && track.album.artists.length > 0) { + // First look for the artist in the album + artistname = track.album.artists[0].name + } else if (track.artists && (track.artists.length > 0)) { + // Fallback to using artists for specific track + artistname = track.artists[0].name } - }, error: function (code, message) { - console.error('Error retrieving artist info from last.fm', code, message) - }}) - } -} -$(document).ready(coverArt.init) + this.lastfm.album.getInfo({artist: artistname, album: albumname}, {success: function (data) { + for (var i = 0; i < data.album.image.length; i++) { + if (data.album.image[i].size === size) { + $(images).attr('src', data.album.image[i]['#text'] || defUrl) + } + } + }, error: function (code, message) { + console.error('Error retrieving album info from last.fm', code, message) + }}) + }, -// TODO: Remove this once JavaScript codebase has been completely modularized -// in favour of bundling everything using 'browserify'. -if (typeof exports !== 'undefined') { - if (typeof module !== 'undefined' && module.exports) { - module.exports = coverArt + getArtistImage: function (artist, images, size) { + var defUrl = 'images/user_24x32.png' + $(images).attr('src', defUrl) + if (!artist || artist.length === 0) { + return + } + this.lastfm.artist.getInfo({artist: artist}, {success: function (data) { + for (var i = 0; i < data.artist.image.length; i++) { + if (data.artist.image[i].size === size) { + $(images).attr('src', data.artist.image[i]['#text'] || defUrl) + } + } + }, error: function (code, message) { + console.error('Error retrieving artist info from last.fm', code, message) + }}) + } } -} + return coverArt +})) diff --git a/mopidy_musicbox_webclient/static/js/library.js b/mopidy_musicbox_webclient/static/js/library.js index 7abd995..dad0606 100644 --- a/mopidy_musicbox_webclient/static/js/library.js +++ b/mopidy_musicbox_webclient/static/js/library.js @@ -1,347 +1,352 @@ -var library = { +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory) + } else if (typeof module === 'object' && module.exports) { + module.exports = factory() + } else { + root.library = factory() + } +}(this, function () { + 'use strict' -/** ******************************* - * Search - *********************************/ - searchPressed: function (key) { - var value = $('#searchinput').val() - switchContent('search') + var library = { - if (key === 13) { - library.initSearch() - return false - } - return true - }, + /** ******************************* + * Search + *********************************/ + searchPressed: function (key) { + var value = $('#searchinput').val() + switchContent('search') - // init search - initSearch: function () { - var value = $('#searchinput').val() - var searchService = $('#selectSearchService').val() - $.cookie('searchScheme', searchService, { expires: 365 }) + if (key === 13) { + library.initSearch() + return false + } + return true + }, - if ((value.length < 100) && (value.length > 0)) { - showLoading(true) - // hide ios/android keyboard - document.activeElement.blur() - $('input').blur() + // init search + initSearch: function () { + var value = $('#searchinput').val() + var searchService = $('#selectSearchService').val() + $.cookie('searchScheme', searchService, { expires: 365 }) - delete customTracklists[URI_SCHEME + ':allresultscache'] - delete customTracklists[URI_SCHEME + ':artistresultscache'] - delete customTracklists[URI_SCHEME + ':albumresultscache'] - delete customTracklists[URI_SCHEME + ':trackresultscache'] - $('#searchartists').hide() - $('#searchalbums').hide() - $('#searchtracks').hide() + if ((value.length < 100) && (value.length > 0)) { + showLoading(true) + // hide ios/android keyboard + document.activeElement.blur() + $('input').blur() - if (searchService !== 'all') { - mopidy.library.search({'query': {any: [value]}, 'uris': [searchService + ':']}).then(library.processSearchResults, console.error) - } else { - mopidy.getUriSchemes().then(function (schemes) { - var query = {} - var uris = [] + delete customTracklists[URI_SCHEME + ':allresultscache'] + delete customTracklists[URI_SCHEME + ':artistresultscache'] + delete customTracklists[URI_SCHEME + ':albumresultscache'] + delete customTracklists[URI_SCHEME + ':trackresultscache'] + $('#searchartists').hide() + $('#searchalbums').hide() + $('#searchtracks').hide() - var regexp = $.map(schemes, function (scheme) { - return '^' + scheme + ':' - }).join('|') + if (searchService !== 'all') { + mopidy.library.search({'query': {any: [value]}, 'uris': [searchService + ':']}).then(library.processSearchResults, console.error) + } else { + mopidy.getUriSchemes().then(function (schemes) { + var query = {} + var uris = [] - var match = value.match(regexp) - if (match) { - var scheme = match[0] - query = {uri: [value]} - uris = [scheme] - } else { - query = {any: [value]} + var regexp = $.map(schemes, function (scheme) { + return '^' + scheme + ':' + }).join('|') + + var match = value.match(regexp) + if (match) { + var scheme = match[0] + query = {uri: [value]} + uris = [scheme] + } else { + query = {any: [value]} + } + mopidy.library.search({'query': query, 'uris': uris}).then(library.processSearchResults, console.error) + }) + } + } + }, + + /** ****************************************************** + * process results of a search + *********************************************************/ + processSearchResults: function (resultArr) { + $(SEARCH_TRACK_TABLE).empty() + $(SEARCH_ARTIST_TABLE).empty() + $(SEARCH_ALBUM_TABLE).empty() + + // Merge results from different backends. + // TODO should of coures have multiple tables + var results = {'tracks': [], 'artists': [], 'albums': []} + var i, j + var emptyResult = true + + for (i = 0; i < resultArr.length; i++) { + if (resultArr[i].tracks) { + for (j = 0; j < resultArr[i].tracks.length; j++) { + results.tracks.push(resultArr[i].tracks[j]) + emptyResult = false } - mopidy.library.search({'query': query, 'uris': uris}).then(library.processSearchResults, console.error) + } + if (resultArr[i].artists) { + for (j = 0; j < resultArr[i].artists.length; j++) { + results.artists.push(resultArr[i].artists[j]) + emptyResult = false + } + } + if (resultArr[i].albums) { + for (j = 0; j < resultArr[i].albums.length; j++) { + results.albums.push(resultArr[i].albums[j]) + emptyResult = false + } + } + } + + customTracklists[URI_SCHEME + ':trackresultscache'] = results.tracks + + if (emptyResult) { + $('#searchtracks').show() + $(SEARCH_TRACK_TABLE).append( + '
            • No tracks found...

            • ' + ) + toast('No results') + showLoading(false) + return false + } + + if (results.artists.length > 0) { + $('#searchartists').show() + } + + if (results.albums.length > 0) { + $('#searchalbums').show() + } + + if (results.tracks.length > 0) { + $('#searchtracks').show() + } + + // Returns a string where {x} in template is replaced by tokens[x]. + function theme (template, tokens) { + return template.replace(/{[^}]+}/g, function (match) { + return tokens[match.slice(1, -1)] }) } - } - }, -/** ****************************************************** - * process results of a search - *********************************************************/ - processSearchResults: function (resultArr) { - $(SEARCH_TRACK_TABLE).empty() - $(SEARCH_ARTIST_TABLE).empty() - $(SEARCH_ALBUM_TABLE).empty() + // 'Show more' pattern + var showMorePattern = '
            • Show {count} more
            • ' - // Merge results from different backends. - // TODO should of coures have multiple tables - var results = {'tracks': [], 'artists': [], 'albums': []} - var i, j - var emptyResult = true + // Artist results + var child = '' + var pattern = '
            • {name}
            • ' + var tokens - for (i = 0; i < resultArr.length; i++) { - if (resultArr[i].tracks) { - for (j = 0; j < resultArr[i].tracks.length; j++) { - results.tracks.push(resultArr[i].tracks[j]) - emptyResult = false + for (i = 0; i < results.artists.length; i++) { + tokens = { + 'id': results.artists[i].uri, + 'name': results.artists[i].name, + 'class': getMediaClass(results.artists[i].uri) } - } - if (resultArr[i].artists) { - for (j = 0; j < resultArr[i].artists.length; j++) { - results.artists.push(resultArr[i].artists[j]) - emptyResult = false + + // Add 'Show all' item after a certain number of hits. + if (i === 4 && results.artists.length > 5) { + child += theme(showMorePattern, {'count': results.artists.length - i}) + pattern = pattern.replace('
            • ', '
            • ') } + + child += theme(pattern, tokens) } - if (resultArr[i].albums) { - for (j = 0; j < resultArr[i].albums.length; j++) { - results.albums.push(resultArr[i].albums[j]) - emptyResult = false + + // Inject list items, refresh listview and hide superfluous items. + $(SEARCH_ARTIST_TABLE).html(child).listview('refresh').find('.overflow').hide() + + // Album results + child = '' + pattern = '
            • ' + pattern += '
              {albumName}
              ' + pattern += '

              {artistName}

              ' + pattern += '
            • ' + + for (i = 0; i < results.albums.length; i++) { + tokens = { + 'albumId': results.albums[i].uri, + 'albumName': results.albums[i].name, + 'artistName': '', + 'albumYear': results.albums[i].date, + 'class': getMediaClass(results.albums[i].uri) } - } - } - - customTracklists[URI_SCHEME + ':trackresultscache'] = results.tracks - - if (emptyResult) { - $('#searchtracks').show() - $(SEARCH_TRACK_TABLE).append( - '
            • No tracks found...

            • ' - ) - toast('No results') - showLoading(false) - return false - } - - if (results.artists.length > 0) { - $('#searchartists').show() - } - - if (results.albums.length > 0) { - $('#searchalbums').show() - } - - if (results.tracks.length > 0) { - $('#searchtracks').show() - } - - // Returns a string where {x} in template is replaced by tokens[x]. - function theme (template, tokens) { - return template.replace(/{[^}]+}/g, function (match) { - return tokens[match.slice(1, -1)] - }) - } - - // 'Show more' pattern - var showMorePattern = '
            • Show {count} more
            • ' - - // Artist results - var child = '' - var pattern = '
            • {name}
            • ' - var tokens - - for (i = 0; i < results.artists.length; i++) { - tokens = { - 'id': results.artists[i].uri, - 'name': results.artists[i].name, - 'class': getMediaClass(results.artists[i].uri) - } - - // Add 'Show all' item after a certain number of hits. - if (i === 4 && results.artists.length > 5) { - child += theme(showMorePattern, {'count': results.artists.length - i}) - pattern = pattern.replace('
            • ', '
            • ') - } - - child += theme(pattern, tokens) - } - - // Inject list items, refresh listview and hide superfluous items. - $(SEARCH_ARTIST_TABLE).html(child).listview('refresh').find('.overflow').hide() - - // Album results - child = '' - pattern = '
            • ' - pattern += '
              {albumName}
              ' - pattern += '

              {artistName}

              ' - pattern += '
            • ' - - for (i = 0; i < results.albums.length; i++) { - tokens = { - 'albumId': results.albums[i].uri, - 'albumName': results.albums[i].name, - 'artistName': '', - 'albumYear': results.albums[i].date, - 'class': getMediaClass(results.albums[i].uri) - } - if (results.albums[i].artists) { - for (j = 0; j < results.albums[i].artists.length; j++) { - if (results.albums[i].artists[j].name) { - tokens.artistName += results.albums[i].artists[j].name + ' ' + if (results.albums[i].artists) { + for (j = 0; j < results.albums[i].artists.length; j++) { + if (results.albums[i].artists[j].name) { + tokens.artistName += results.albums[i].artists[j].name + ' ' + } } } + if (tokens.albumYear) { + tokens.artistName += '(' + tokens.albumYear + ')' + } + // Add 'Show all' item after a certain number of hits. + if (i === 4 && results.albums.length > 5) { + child += theme(showMorePattern, {'count': results.albums.length - i}) + pattern = pattern.replace('
            • ', '
            • ') + } + + child += theme(pattern, tokens) } - if (tokens.albumYear) { - tokens.artistName += '(' + tokens.albumYear + ')' - } - // Add 'Show all' item after a certain number of hits. - if (i === 4 && results.albums.length > 5) { - child += theme(showMorePattern, {'count': results.albums.length - i}) - pattern = pattern.replace('
            • ', '
            • ') - } + // Inject list items, refresh listview and hide superfluous items. + $(SEARCH_ALBUM_TABLE).html(child).listview('refresh').find('.overflow').hide() - child += theme(pattern, tokens) - } - // Inject list items, refresh listview and hide superfluous items. - $(SEARCH_ALBUM_TABLE).html(child).listview('refresh').find('.overflow').hide() + // Track results + resultsToTables(results.tracks, SEARCH_TRACK_TABLE, URI_SCHEME + ':trackresultscache') - // Track results - resultsToTables(results.tracks, SEARCH_TRACK_TABLE, URI_SCHEME + ':trackresultscache') - - showLoading(false) - }, - -/** ******************************* - * Playlists & Browse - *********************************/ - getPlaylists: function () { - // get playlists without tracks - mopidy.playlists.asList().then(processGetPlaylists, console.error) - }, - - getBrowseDir: function (rootdir) { - // get directory to browse - showLoading(true) - if (!rootdir) { - browseStack.pop() - rootdir = browseStack[browseStack.length - 1] - } else { - browseStack.push(rootdir) - } - if (!rootdir) { - rootdir = null - } - mopidy.library.browse({'uri': rootdir}).then(processBrowseDir, console.error) - }, - - getCurrentPlaylist: function () { - mopidy.tracklist.getTlTracks().then(processCurrentPlaylist, console.error) - }, - -/** ****************************************************** - * Show tracks of playlist - ********************************************************/ - togglePlaylists: function () { - if ($(window).width() <= 960) { - $('#playlisttracksdiv').toggle(); - // Hide other div - ($('#playlisttracksdiv').is(':visible')) ? $('#playlistslistdiv').hide() : $('#playlistslistdiv').show() - } else { - $('#playlisttracksdiv').show() - $('#playlistslistdiv').show() - } - return true - }, - -/** ********** - * Lookups - ************/ - showTracklist: function (uri) { - showLoading(true) - $(PLAYLIST_TABLE).empty() - library.togglePlaylists() - var tracks = getPlaylistTracks(uri).then(function (tracks) { - resultsToTables(tracks, PLAYLIST_TABLE, uri, 'return library.togglePlaylists();', true) showLoading(false) - }) - updatePlayIcons(uri) - $('#playlistslist li a').each(function () { - $(this).removeClass('playlistactive') - if (this.id === uri) { - $(this).addClass('playlistactive') - } - }) - return false - }, + }, - showArtist: function (nwuri) { - $('#popupQueue').popup('close') - $('#popupTracks').popup('close') - $('#controlsmodal').popup('close') - $(ARTIST_TABLE).empty() + /** ******************************* + * Playlists & Browse + *********************************/ + getPlaylists: function () { + // get playlists without tracks + mopidy.playlists.asList().then(processGetPlaylists, console.error) + }, - // TODO cache - $('#h_artistname').html('') - showLoading(true) - mopidy.library.lookup({'uris': [nwuri]}).then(function (resultDict) { - var resultArr = resultDict[nwuri] - resultArr.uri = nwuri - processArtistResults(resultArr) - }, console.error) - switchContent('artists', nwuri) - scrollToTop() - return false - }, - - showAlbum: function (uri) { - $('#popupQueue').popup('close') - $('#popupTracks').popup('close') - $('#controlsmodal').popup('close') - $(ALBUM_TABLE).empty() - // fill from cache - var pl = getTracksFromUri(uri, true) - if (pl.length > 0) { - albumTracksToTable(pl, ALBUM_TABLE, uri) - var albumname = getAlbum(pl) - var artistname = getArtist(pl) - $('#h_albumname').html(albumname) - $('#h_albumartist').html(artistname) - $('#coverpopupalbumname').html(albumname) - $('#coverpopupartist').html(artistname) - showLoading(false) - mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) { - var resultArr = resultDict[uri] - resultArr.uri = uri - processAlbumResults(resultArr) - }, console.error) - } else { + getBrowseDir: function (rootdir) { + // get directory to browse showLoading(true) - $('#h_albumname').html('') - $('#h_albumartist').html('') - mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) { - var resultArr = resultDict[uri] - resultArr.uri = uri - processAlbumResults(resultArr) + if (!rootdir) { + browseStack.pop() + rootdir = browseStack[browseStack.length - 1] + } else { + browseStack.push(rootdir) + } + if (!rootdir) { + rootdir = null + } + mopidy.library.browse({'uri': rootdir}).then(processBrowseDir, console.error) + }, + + getCurrentPlaylist: function () { + mopidy.tracklist.getTlTracks().then(processCurrentPlaylist, console.error) + }, + + /** ****************************************************** + * Show tracks of playlist + ********************************************************/ + togglePlaylists: function () { + if ($(window).width() <= 960) { + $('#playlisttracksdiv').toggle(); + // Hide other div + ($('#playlisttracksdiv').is(':visible')) ? $('#playlistslistdiv').hide() : $('#playlistslistdiv').show() + } else { + $('#playlisttracksdiv').show() + $('#playlistslistdiv').show() + } + return true + }, + + /** ********** + * Lookups + ************/ + showTracklist: function (uri) { + showLoading(true) + $(PLAYLIST_TABLE).empty() + library.togglePlaylists() + var tracks = getPlaylistTracks(uri).then(function (tracks) { + resultsToTables(tracks, PLAYLIST_TABLE, uri, 'return library.togglePlaylists();', true) + showLoading(false) + }) + updatePlayIcons(uri) + $('#playlistslist li a').each(function () { + $(this).removeClass('playlistactive') + if (this.id === uri) { + $(this).addClass('playlistactive') + } + }) + return false + }, + + showArtist: function (nwuri) { + $('#popupQueue').popup('close') + $('#popupTracks').popup('close') + $('#controlsmodal').popup('close') + $(ARTIST_TABLE).empty() + + // TODO cache + $('#h_artistname').html('') + showLoading(true) + mopidy.library.lookup({'uris': [nwuri]}).then(function (resultDict) { + var resultArr = resultDict[nwuri] + resultArr.uri = nwuri + processArtistResults(resultArr) + }, console.error) + switchContent('artists', nwuri) + scrollToTop() + return false + }, + + showAlbum: function (uri) { + $('#popupQueue').popup('close') + $('#popupTracks').popup('close') + $('#controlsmodal').popup('close') + $(ALBUM_TABLE).empty() + // fill from cache + var pl = getTracksFromUri(uri, true) + if (pl.length > 0) { + albumTracksToTable(pl, ALBUM_TABLE, uri) + var albumname = getAlbum(pl) + var artistname = getArtist(pl) + $('#h_albumname').html(albumname) + $('#h_albumartist').html(artistname) + $('#coverpopupalbumname').html(albumname) + $('#coverpopupartist').html(artistname) + showLoading(false) + mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) { + var resultArr = resultDict[uri] + resultArr.uri = uri + processAlbumResults(resultArr) + }, console.error) + } else { + showLoading(true) + $('#h_albumname').html('') + $('#h_albumartist').html('') + mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) { + var resultArr = resultDict[uri] + resultArr.uri = uri + processAlbumResults(resultArr) + }, console.error) + } + // show page + switchContent('albums', uri) + scrollToTop() + return false + }, + + getSearchSchemes: function (searchBlacklist, mopidy) { + var backendName + var searchScheme = $.cookie('searchScheme') + if (searchScheme) { + searchScheme = searchScheme.replace(/"/g, '') + } else { + searchScheme = 'all' + } + $('#selectSearchService').empty() + $('#selectSearchService').append(new Option('All services', 'all')) + mopidy.getUriSchemes().then(function (schemesArray) { + schemesArray = schemesArray.filter(function (el) { + return searchBlacklist.indexOf(el) < 0 + }) + for (var i = 0; i < schemesArray.length; i++) { + backendName = getMediaHuman(schemesArray[i]) + backendName = backendName.charAt(0).toUpperCase() + backendName.slice(1) + $('#selectSearchService').append(new Option(backendName, schemesArray[i])) + } + $('#selectSearchService').val(searchScheme) + $('#selectSearchService').selectmenu('refresh', true) }, console.error) } - // show page - switchContent('albums', uri) - scrollToTop() - return false - }, - - getSearchSchemes: function () { - var backendName - var searchScheme = $.cookie('searchScheme') - if (searchScheme) { - searchScheme = searchScheme.replace(/"/g, '') - } else { - searchScheme = 'all' - } - $('#selectSearchService').empty() - $('#selectSearchService').append(new Option('All services', 'all')) - mopidy.getUriSchemes().then(function (schemesArray) { - schemesArray = schemesArray.filter(function (el) { - return searchBlacklist.indexOf(el) < 0 - }) - for (var i = 0; i < schemesArray.length; i++) { - backendName = getMediaHuman(schemesArray[i]) - backendName = backendName.charAt(0).toUpperCase() + backendName.slice(1) - $('#selectSearchService').append(new Option(backendName, schemesArray[i])) - } - $('#selectSearchService').val(searchScheme) - $('#selectSearchService').selectmenu('refresh', true) - }, console.error) } -} - -// TODO: Remove this once JavaScript codebase has been completely modularized -// in favour of bundling everything using 'browserify'. -if (typeof exports !== 'undefined') { - if (typeof module !== 'undefined' && module.exports) { - module.exports = library - } -} + return library +})) diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js index bbbb7d4..a3ccba3 100644 --- a/mopidy_musicbox_webclient/static/js/process_ws.js +++ b/mopidy_musicbox_webclient/static/js/process_ws.js @@ -220,7 +220,7 @@ function processCurrentPlaylist (resultArr) { function processArtistResults (resultArr) { if (!resultArr || (resultArr.length === 0)) { $('#h_artistname').text('Artist not found...') - coverArt.getCover('', '#artistviewimage, #artistpopupimage', 'extralarge') + coverArt.getCover('', '#artistviewimage, #artistpopupimage', 'extralarge', mopidy) showLoading(false) return } @@ -239,7 +239,7 @@ function processArtistResults (resultArr) { function processAlbumResults (resultArr) { if (!resultArr || (resultArr.length === 0)) { $('#h_albumname').text('Album not found...') - coverArt.getCover('', '#albumviewcover, #coverpopupimage', 'extralarge') + coverArt.getCover('', '#albumviewcover, #coverpopupimage', 'extralarge', mopidy) showLoading(false) return } diff --git a/mopidy_musicbox_webclient/static/js/progress_timer.js b/mopidy_musicbox_webclient/static/js/progress_timer.js deleted file mode 100644 index 00ff6e9..0000000 --- a/mopidy_musicbox_webclient/static/js/progress_timer.js +++ /dev/null @@ -1,177 +0,0 @@ -var progressTimer -var positionNode = document.createTextNode('') -var durationNode = document.createTextNode('') - -var START_BEATS = 5 // 0.5 seconds, needs to be less than 1s to prevent unwanted updates. -var RUN_BEATS = 300 // 30 seconds assuming default timer update rate of 100ms -var callbackHeartbeats = 0 // Timer will check syncs on every n-number of calls. -var targetPosition = null - -var MAX_SYNCS = 5 // Maximum number of consecutive successful syncs to perform. -var syncsLeft = MAX_SYNCS -var synced = false -var consecutiveSyncs = 0 - -$(document).ready(function () { - $('#songelapsed').append(positionNode) - $('#songlength').append(durationNode) -}) - -function timerCallback (position, duration, isRunning) { - updateTimers(position, duration, isRunning) - - if (callbackHeartbeats === 0) { - callbackHeartbeats = getHeartbeat() - } - - if (mopidy && position > 0) { - // Mopidy and timer are both initialized. - if (callbackHeartbeats-- === 1) { - // Get time position from Mopidy on every nth callback until - // synced. - mopidy.playback.getTimePosition().then( - function (mopidy_position) { - syncTimer(position, mopidy_position) - } - ) - } - } -} - -function updateTimers (position, duration, isRunning) { - var ready = !(duration === Infinity && position === 0 && !isRunning) // Timer has been properly initialized. - var streaming = (duration === Infinity && position > 0) // Playing a stream. - var ok = synced && isRunning // Normal operation. - var syncing = !synced && isRunning // Busy syncing. - - if (!ready) { - // Make sure that default values are displayed while the timer is being initialized. - positionNode.nodeValue = '' - durationNode.nodeValue = '' - $('#trackslider').val(0).slider('refresh') - } else { - durationNode.nodeValue = format(duration || Infinity) - if (syncing) { - if (!targetPosition) { - // Waiting for Mopidy to provide a target position. - positionNode.nodeValue = '(wait)' - } else { - // Busy seeking to new target position. - positionNode.nodeValue = '(sync)' - } - } else if (synced || streaming) { - positionNode.nodeValue = format(position) - } - } - - if (ok) { - // Don't update the track slider unless it is synced and running. - // (prevents awkward 'jitter' animation). - $('#trackslider').val(position).slider('refresh') - } -} - -function getHeartbeat () { - if (syncsLeft > 0 && callbackHeartbeats === 0) { - // Step back exponentially while increasing heartbeat. - return Math.round(delay_exponential(5, 2, MAX_SYNCS - syncsLeft)) - } else if (syncsLeft === 0 && callbackHeartbeats === 0) { - // Sync completed, keep checking using maximum number of heartbeats. - return RUN_BEATS - } else { - return START_BEATS - } -} - -function syncTimer (current, target) { - if (target) { - var drift = Math.abs(target - current) - if (drift <= 500) { - syncsLeft-- - // Less than 500ms == in sync. - if (++consecutiveSyncs === 2) { - // Need at least two consecutive syncs to know that Mopidy - // is progressing playback and we are in sync. - synced = true - targetPosition = null - consecutiveSyncs = 0 - } - } else { - // Drift is too large, re-sync with Mopidy. - reset() - targetPosition = target - progressTimer.set(targetPosition) - } - } -} - -function toInt (value) { - return value.match(/^\w*\d+\w*$/) ? parseInt(value) : null -} - -function format (milliseconds) { - if (milliseconds === Infinity) { - return '(n/a)' - } else if (milliseconds === 0) { - return '0:00' - } - - var seconds = Math.floor(milliseconds / 1000) - var minutes = Math.floor(seconds / 60) - seconds = seconds % 60 - - seconds = seconds < 10 ? '0' + seconds : seconds - return minutes + ':' + seconds -} - -function delay_exponential (base, growthFactor, attempts) { - /* Calculate number of beats between syncs based on exponential function. - The format is:: - - base * growthFactor ^ (attempts - 1) - - If ``base`` is set to 'rand' then a random number between - 0 and 1 will be used as the base. - Base must be greater than 0. - */ - if (base === 'rand') { - base = Math.random() - } - beats = base * (Math.pow(growthFactor, (attempts - 1))) - return beats -} - -function reset () { - synced = false - consecutiveSyncs = 0 - syncsLeft = MAX_SYNCS - callbackHeartbeats = START_BEATS - targetPosition = null -} - -function setProgressTimer (pos) { - reset() - targetPosition = pos - progressTimer.set(pos) - if (!play) { - // Set lapsed time and slider position directly as timer callback is not currently - // running. - positionNode.nodeValue = format(pos) - $('#trackslider').val(pos).slider('refresh') - } -} - -function updatePosition (pos) { - positionNode.nodeValue = format(pos) -} - -function startProgressTimer () { - reset() - progressTimer.start() -} - -function resetProgressTimer () { - progressTimer.reset() - reset() - targetPosition = 0 -} diff --git a/mopidy_musicbox_webclient/static/js/synced_timer.js b/mopidy_musicbox_webclient/static/js/synced_timer.js new file mode 100644 index 0000000..25b2eb9 --- /dev/null +++ b/mopidy_musicbox_webclient/static/js/synced_timer.js @@ -0,0 +1,218 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory) + } else if (typeof module === 'object' && module.exports) { + module.exports = factory() + } else { + root.SyncedProgressTimer = factory() + } +}(this, function () { + 'use strict' + + function delay_exponential (base, growthFactor, attempts) { + /* Calculate number of beats between syncs based on exponential function. + The format is:: + + base * growthFactor ^ (attempts - 1) + + If ``base`` is set to 'rand' then a random number between + 0 and 1 will be used as the base. + Base must be greater than 0. + */ + if (base === 'rand') { + base = Math.random() + } + // console.log(base + ' * (Math.pow(' + growthFactor + ', (' + attempts + ' - 1)) = ' + base * (Math.pow(growthFactor, (attempts - 1)))) + return base * (Math.pow(growthFactor, (attempts - 1))) + } + + function SyncedProgressTimer (maxAttempts, mopidy) { + if (!(this instanceof SyncedProgressTimer)) { + return new SyncedProgressTimer(maxAttempts, mopidy) + } + + this.positionNode = document.createTextNode('') + this.durationNode = document.createTextNode('') + + $('#songelapsed').append(this.positionNode) + $('#songlength').append(this.durationNode) + + this._progressTimer = new ProgressTimer({ + // Make sure that the timer object's context is available. + callback: $.proxy(this.timerCallback, this) + }) + + this._maxAttempts = maxAttempts + this._mopidy = mopidy + this._isConnected = false + this._mopidy.on('state:online', $.proxy(function () { this._isConnected = true }), this) + this._mopidy.on('state:offline', $.proxy(function () { this._isConnected = false }), this) + this.init() + } + + SyncedProgressTimer.SYNC_STATE = { + NOT_SYNCED: 0, + SYNCING: 1, + SYNCED: 2 + } + + SyncedProgressTimer.format = function (milliseconds) { + if (milliseconds === Infinity) { + return '(n/a)' + } else if (milliseconds === 0) { + return '0:00' + } + + var seconds = Math.floor(milliseconds / 1000) + var minutes = Math.floor(seconds / 60) + seconds = seconds % 60 + + seconds = seconds < 10 ? '0' + seconds : seconds + return minutes + ':' + seconds + } + + SyncedProgressTimer.prototype.init = function () { + this.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED + this._syncAttemptsRemaining = this._maxAttempts + + this.positionNode.nodeValue = '' + this.durationNode.nodeValue = '' + + this._scheduledSyncTime = null + this._previousSyncPosition = null + this._duration = null + + return this + } + + SyncedProgressTimer.prototype.timerCallback = function (position, duration) { + this._update(position, duration) + + if (this._isConnected && this._isSyncScheduled()) { + this._doSync(position, duration) + } + } + + SyncedProgressTimer.prototype._update = function (position, duration) { + if (!(duration === Infinity && position === 0)) { + // Timer has been properly initialized. + this.durationNode.nodeValue = SyncedProgressTimer.format(duration || Infinity) + switch (this.syncState) { + case SyncedProgressTimer.SYNC_STATE.NOT_SYNCED: + // Waiting for Mopidy to provide a target position. + this.positionNode.nodeValue = '(wait)' + break + case SyncedProgressTimer.SYNC_STATE.SYNCING: + // Busy seeking to new target position. + this.positionNode.nodeValue = '(sync)' + break + case SyncedProgressTimer.SYNC_STATE.SYNCED: + this._previousSyncPosition = position + this.positionNode.nodeValue = SyncedProgressTimer.format(position) + $('#trackslider').val(position).slider('refresh') + break + } + } else { + // Make sure that default values are displayed while the timer is being initialized. + this.positionNode.nodeValue = '' + this.durationNode.nodeValue = '' + $('#trackslider').val(0).slider('refresh') + } + } + + SyncedProgressTimer.prototype._isSyncScheduled = function () { + return this._scheduledSyncTime !== null && this._scheduledSyncTime <= new Date().getTime() + } + + SyncedProgressTimer.prototype._doSync = function (position, duration) { + var ready = !(duration === Infinity && position === 0) // Timer has been properly initialized. + if (!ready) { + // Don't try to sync if progress timer has not been initialized yet. + return + } + var _this = this + _this._mopidy.playback.getTimePosition().then(function (targetPosition) { + if (Math.abs(targetPosition - position) <= 500) { + // Less than 500ms == in sync. + _this.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING + _this._syncAttemptsRemaining = Math.max(_this._syncAttemptsRemaining - 1, 0) + if (_this._syncAttemptsRemaining < _this._maxAttempts - 1 && _this._previousSyncPosition !== targetPosition) { + // Need at least two consecutive syncs to know that Mopidy + // is progressing playback and we are in sync. + _this.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED + } + _this._previousSyncPosition = targetPosition + // Step back exponentially while increasing number of callbacks. + _this._scheduledSyncTime = new Date().getTime() + + delay_exponential(0.25, 2, _this._maxAttempts - _this._syncAttemptsRemaining) * 1000 + } else { + // Drift is too large, re-sync with Mopidy. + _this._syncAttemptsRemaining = _this._maxAttempts + _this._scheduledSyncTime = new Date().getTime() + 100 + _this._previousSyncPosition = null + _this.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING + _this._progressTimer.set(targetPosition) + } + }) + } + + SyncedProgressTimer.prototype.set = function (position, duration) { + if (arguments.length === 0) { + throw new Error('"SyncedProgressTimer.set" requires the "position" argument.') + } + // Workaround for https://github.com/adamcik/media-progress-timer/issues/3 + // This causes the timer to die unexpectedly if the position exceeds + // the duration slightly. + if (this._duration && this._duration < position) { + position = this._duration - 1 + } + this.init() + if (arguments.length === 1) { + this._progressTimer.set(position) + } else { + this._duration = duration + this._progressTimer.set(position, duration) + } + + if (!this._isSyncScheduled()) { + // Set lapsed time and slider position directly as timer callback is not currently + // running. + this.positionNode.nodeValue = SyncedProgressTimer.format(position) + if (arguments.length === 2) { + this.durationNode.nodeValue = SyncedProgressTimer.format(duration) + } + $('#trackslider').val(position).slider('refresh') + } + + return this + } + + SyncedProgressTimer.prototype.start = function () { + this.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED + this._scheduledSyncTime = new Date().getTime() + this._progressTimer.start() + return this + } + + SyncedProgressTimer.prototype.stop = function () { + this._progressTimer.stop() + this._scheduledSyncTime = null + this.updatePosition(this._previousSyncPosition) + return this + } + + SyncedProgressTimer.prototype.reset = function () { + this._progressTimer.reset() + this.stop() + this.init() + this.set(0, Infinity) + + return this + } + + SyncedProgressTimer.prototype.updatePosition = function (position) { + this.positionNode.nodeValue = SyncedProgressTimer.format(position) + } + + return SyncedProgressTimer +})) diff --git a/mopidy_musicbox_webclient/static/mb.appcache b/mopidy_musicbox_webclient/static/mb.appcache index c825720..246f924 100644 --- a/mopidy_musicbox_webclient/static/mb.appcache +++ b/mopidy_musicbox_webclient/static/mb.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# 2016-03-28:v1 +# 2016-04-03:v1 NETWORK: * @@ -26,7 +26,7 @@ js/gui.js js/images.js js/library.js js/process_ws.js -js/progress_timer.js +js/synced_timer.js mb.appcache system.html vendors/font_awesome/css/font-awesome.css diff --git a/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js b/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js index 138605f..4592fc0 100644 --- a/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js +++ b/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js @@ -1,128 +1,159 @@ -/*! timer.js v2.0.2 +/*! timer.js v3.0.0 * https://github.com/adamcik/media-progress-timer - * Copyright (c) 2015 Thomas Adamcik + * Copyright (c) 2015-2016 Thomas Adamcik * Licensed under the Apache License, Version 2.0 */ -(function() { - -'use strict'; - -var now = typeof window.performance !== 'undefined' && - typeof window.performance.now !== 'undefined' && - window.performance.now.bind(window.performance) || Date.now || - function() { return new Date().getTime(); }; - -function ProgressTimer(options) { - if (!(this instanceof ProgressTimer)) { - return new ProgressTimer(options); - } else if (typeof options === 'function') { - options = {'callback': options}; - } else if (typeof options !== 'object') { - throw 'ProgressTimer must be called with a callback or options.'; - } else if (typeof options.callback !== 'function') { - throw 'ProgressTimer needs a callback to operate.'; - } - - this._running = false; - this._updateRate = Math.max(options.updateRate || 100, 10); - this._callback = options.callback; - this._fallback = typeof window.requestAnimationFrame === 'undefined' || - options.disableRequestAnimationFrame|| false; - - if (!this._fallback) { - this._callUpdate = this._scheduleAnimationFrame; - this._scheduleUpdate = this._scheduleAnimationFrame; - } - - this._boundCallUpdate = this._callUpdate.bind(this); - this._boundUpdate = this._update.bind(this); - - this.reset(); -} - -ProgressTimer.prototype.set = function(position, duration) { - if (arguments.length === 0) { - throw 'set requires at least a position argument.'; - } else if (arguments.length === 1) { - duration = this._state.duration; +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(); } else { - duration = Math.floor( - Math.max(duration === null ? Infinity : duration || 0, 0)); + root.ProgressTimer = factory(); } - position = Math.floor(Math.min(Math.max(position || 0, 0), duration)); +}(this, function () { + 'use strict'; - this._state = { - initialTimestamp: null, - initialPosition: position, - previousPosition: position, - duration: duration + // Helper function to provide a reference time in milliseconds. + var now = typeof window.performance !== 'undefined' && + typeof window.performance.now !== 'undefined' && + window.performance.now.bind(window.performance) || Date.now || + function() { return new Date().getTime(); }; + + // Helper to warn library users about deprecated features etc. + function warn(msg) { + window.setTimeout(function() { throw msg; }, 0); + } + + // Creates a new timer object, works with both 'new ProgressTimer(options)' + // and just 'ProgressTimer(options). Optionally the timer can also be + // called with only the callback instead of options. + function ProgressTimer(options) { + if (!(this instanceof ProgressTimer)) { + return new ProgressTimer(options); + } else if (typeof options === 'function') { + options = {'callback': options}; + } else if (typeof options !== 'object') { + throw '"ProgressTimer" must be called with a callback or options.'; + } else if (typeof options['callback'] !== 'function') { + throw '"ProgressTimer" needs a callback to operate.'; + } + + this._userCallback = options['callback']; + this._updateId = null; + this._state = null; // Gets initialized by the set() call. + + var frameDuration = 1000 / (options['fallbackTargetFrameRate'] || 30); + // TODO: Remove this legacy code path at some point. + if (options['updateRate'] && !options['fallbackTargetFrameRate']) { + warn('"ProgressTimer" no longer supports the updateRate option.'); + frameDuration = Math.max(options['updateRate'], 1000 / 60); + } + + var useFallback = ( + typeof window.requestAnimationFrame === 'undefined' || + typeof window.cancelAnimationFrame === 'undefined' || + options['disableRequestAnimationFrame'] || false); + + // Make sure this works in _update. + var update = this._update.bind(this); + + if (useFallback) { + this._schedule = function(timestamp) { + var timeout = Math.max(timestamp + frameDuration - now(), 0); + return window.setTimeout(update, Math.floor(timeout)); + }; + this._cancel = window.clearTimeout.bind(window); + } else { + this._schedule = window.requestAnimationFrame.bind(window, update); + this._cancel = window.cancelAnimationFrame.bind(window); + } + + this.reset(); // Reuse reset code to ensure we start in the same state. + } + + // If called with one argument the previous duration is preserved. Note + // that the position can be changed while the timer is running. + ProgressTimer.prototype.set = function(position, duration) { + if (arguments.length === 0) { + throw '"ProgressTimer.set" requires the "position" arugment.'; + } else if (arguments.length === 1) { + // Fallback to previous duration, whatever that was. + duration = this._state.duration; + } else { + // Round down and make sure zero and null are treated as inf. + duration = Math.floor(Math.max( + duration === null ? Infinity : duration || Infinity, 0)); + } + // Make sure '0 <= position <= duration' always holds. + position = Math.floor(Math.min(Math.max(position || 0, 0), duration)); + + this._state = { + initialTimestamp: null, + initialPosition: position, + position: position, + duration: duration + }; + + // Update right away if we don't have anything running. + if (this._updateId === null) { + // TODO: Consider wrapping this in a try/catch? + this._userCallback(position, duration); + } + return this; }; - this._callback(position, duration, this._running); - return this; -}; - -ProgressTimer.prototype.start = function() { - this._running = true; - this._callUpdate(); - return this; -}; - -ProgressTimer.prototype.stop = function() { - this._running = false; - var state = this._state; - return this.set(state.previousPosition, state.duration); -}; - -ProgressTimer.prototype.reset = function() { - this._running = false; - return this.set(0, Infinity); -}; - -ProgressTimer.prototype._callUpdate = function() { - this._update(now()); -}; - -ProgressTimer.prototype._scheduleUpdate = function(timestamp) { - var adjustedTimeout = timestamp + this._updateRate - now(); - setTimeout(this._boundCallUpdate, adjustedTimeout); -}; - -ProgressTimer.prototype._scheduleAnimationFrame = function() { - window.requestAnimationFrame(this._boundUpdate); -}; - -ProgressTimer.prototype._update = function(timestamp) { - if (!this._running) { - return; - } - - var state = this._state; - state.initialTimestamp = state.initialTimestamp || timestamp; - - var position = state.initialPosition + timestamp - state.initialTimestamp; - var duration = state.duration; - - if (position < duration || duration === null) { - var delta = position - state.previousPosition; - if (delta >= this._updateRate || this._fallback) { - this._callback(Math.floor(position), duration, this._running); - state.previousPosition = position; + // Start the timer if it is not already running. + ProgressTimer.prototype.start = function() { + if (this._updateId === null) { + this._updateId = this._schedule(0); } - } else { - // Workaround for https://github.com/adamcik/media-progress-timer/issues/3 - // This causes the timer to die unexpectedly if the position goes - // over the duration slightly. - // this._running = false; - this._callback(duration, duration, this._running); - } - this._scheduleUpdate(timestamp); -}; + return this; + }; -if(typeof module !== 'undefined') { - module.exports = ProgressTimer; -} else { - window.ProgressTimer = ProgressTimer; -} + // Cancel the timer if it us currently tracking progress. + ProgressTimer.prototype.stop = function() { + if (this._updateId !== null) { + this._cancel(this._updateId); -}()); \ No newline at end of file + // Ensure we correctly reset the initial position and timestamp. + this.set(this._state.position, this._state.duration); + this._updateId = null; // Last step to avoid callback in set() + } + return this; + }; + + // Marks the timer as stopped, sets position to zero and duration to inf. + ProgressTimer.prototype.reset = function() { + return this.stop().set(0, Infinity); + }; + + // Calls the user callback with the current position/duration and then + // schedules the next update run via _schedule if we haven't finished. + ProgressTimer.prototype._update = function(timestamp) { + var state = this._state; // We refer a lot to state, this is shorter. + + // Make sure setTimeout has a timestamp and store first reference time. + timestamp = timestamp || now(); + state.initialTimestamp = state.initialTimestamp || timestamp; + + // Recalculate position according to start location and reference. + state.position = ( + state.initialPosition + timestamp - state.initialTimestamp); + + // Ensure callback gets an integer and that 'position <= duration'. + var userPosisition = Math.min( + Math.floor(state.position), state.duration); + + // TODO: Consider wrapping this in a try/catch? + this._userCallback(userPosisition, state.duration); + + if (state.position < state.duration) { + this._updateId = this._schedule(timestamp); // Schedule update. + } else { + this._updateId = null; // Unset since we didn't reschedule. + } + }; + + return ProgressTimer; +})); \ No newline at end of file diff --git a/tests/test_images.js b/tests/js/test_images.js similarity index 71% rename from tests/test_images.js rename to tests/js/test_images.js index 0d8769f..4cd6ab8 100644 --- a/tests/test_images.js +++ b/tests/js/test_images.js @@ -1,5 +1,4 @@ var chai = require('chai') -var should = chai.should() var expect = chai.expect var assert = chai.assert chai.use(require('chai-string')) @@ -7,24 +6,20 @@ chai.use(require('chai-jquery')) var sinon = require('sinon') -var coverArt = require('../mopidy_musicbox_webclient/static/js/images.js') - -var images - -before(function () { - mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'})) - images = $('') -}) +var coverArt = require('../../mopidy_musicbox_webclient/static/js/images.js') describe('CoverArt', function () { + var mopidy + var images + beforeEach(function () { + mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'})) + images = $('') + $(images).removeAttr('src') + }) describe('#getCover()', function () { - beforeEach(function () { - $(images).removeAttr('src') - }) - it('should use default image if no track URI is provided', function () { - coverArt.getCover('', images, '') - $(images).prop('src').should.endWith('images/default_cover.png') + coverArt.getCover('', images, '', mopidy) + expect($(images).prop('src')).to.endWith('images/default_cover.png') }) it('should get image from Mopidy, if available', function () { @@ -33,10 +28,10 @@ describe('CoverArt', function () { mopidy.library = library var getImagesSpy = sinon.spy(mopidy.library, 'getImages') - coverArt.getCover('mock:track:uri', images, '') + coverArt.getCover('mock:track:uri', images, '', mopidy) - assert(getImagesSpy.calledOnce) - $(images).prop('src').should.endWith('mockImageUri') + assert.isTrue(getImagesSpy.calledOnce) + expect($(images).prop('src')).to.endWith('mockImageUri') }) it('should fall back to retrieving image from deprecated track.album.images', function () { @@ -51,21 +46,17 @@ describe('CoverArt', function () { var getImagesSpy = sinon.spy(mopidy.library, 'getImages') var getCoverFromAlbumSpy = sinon.spy(coverArt, 'getCoverFromAlbum') - coverArt.getCover('mock:track:uri', images, '') + coverArt.getCover('mock:track:uri', images, '', mopidy) - assert(getImagesSpy.calledOnce) - assert(getCoverFromAlbumSpy.calledOnce) + assert.isTrue(getImagesSpy.calledOnce) + assert.isTrue(getCoverFromAlbumSpy.calledOnce) }) }) describe('#getCoverFromAlbum()', function () { - beforeEach(function () { - $(images).removeAttr('src') - }) - it('should use default image if no track URI is provided', function () { - coverArt.getCoverFromAlbum('', images, '') - $(images).prop('src').should.endWith('images/default_cover.png') + coverArt.getCoverFromAlbum('', images, '', mopidy) + expect($(images).prop('src')).to.endWith('images/default_cover.png') }) it('should get image from Mopidy track.album.images, if available', function () { @@ -76,10 +67,10 @@ describe('CoverArt', function () { mopidy.library = library var lookupSpy = sinon.spy(mopidy.library, 'lookup') - coverArt.getCoverFromAlbum('mock:track:uri', images, '') + coverArt.getCoverFromAlbum('mock:track:uri', images, '', mopidy) - assert(lookupSpy.calledOnce) - $(images).prop('src').should.endWith('mockAlbumImageUri') + assert.isTrue(lookupSpy.calledOnce) + expect($(images).prop('src')).to.endWith('mockAlbumImageUri') }) it('should use default image if track.album or track.artist is not available', function () { @@ -90,10 +81,10 @@ describe('CoverArt', function () { mopidy.library = library var lookupSpy = sinon.spy(mopidy.library, 'lookup') - coverArt.getCoverFromAlbum('mock:track:uri', images, '') + coverArt.getCoverFromAlbum('mock:track:uri', images, '', mopidy) - assert(lookupSpy.calledOnce) - $(images).prop('src').should.endWith('images/default_cover.png') + assert.isTrue(lookupSpy.calledOnce) + expect($(images).prop('src')).to.endWith('images/default_cover.png') }) it('should fall back to retrieving image from last.fm if none provided by Mopidy', function () { @@ -103,21 +94,18 @@ describe('CoverArt', function () { } mopidy.library = library - var getCoverFromLastFmSpy = sinon.spy(coverArt, 'getCoverFromLastFm') - coverArt.getCoverFromAlbum('mock:track:uri', images, '') + var getCoverFromLastFmStub = sinon.stub(coverArt, 'getCoverFromLastFm') + coverArt.getCoverFromAlbum('mock:track:uri', images, '', mopidy) - assert(getCoverFromLastFmSpy.calledOnce) + assert.isTrue(getCoverFromLastFmStub.calledOnce) + getCoverFromLastFmStub.restore() }) }) describe('#getCoverFromLastFm()', function () { - beforeEach(function () { - $(images).removeAttr('src') - }) - it('should use default image if no track is provided', function () { coverArt.getCoverFromLastFm(undefined, images, '') - $(images).prop('src').should.endWith('images/default_cover.png') + expect($(images).prop('src')).to.endWith('images/default_cover.png') }) it('should fall back to using track artist if album artist is not available', function () { @@ -129,7 +117,7 @@ describe('CoverArt', function () { coverArt.getCoverFromLastFm(track, images, '') var args = getInfoStub.args - assert(args[0][0].artist === 'artistMock') + assert.equal(args[0][0].artist, 'artistMock') getInfoStub.restore() }) @@ -141,7 +129,7 @@ describe('CoverArt', function () { getInfoStub.yieldsTo('success', getInfoResultMock) coverArt.getCoverFromLastFm(track, images, 'small') - $(images).prop('src').should.endWith('mockAlbumImageUri') + expect($(images).prop('src')).to.endWith('mockAlbumImageUri') getInfoStub.restore() }) @@ -150,23 +138,19 @@ describe('CoverArt', function () { var getInfoStub = sinon.stub(coverArt.lastfm.album, 'getInfo') getInfoStub.yieldsTo('error', 'code', 'message') - var consoleSpy = sinon.spy(console, 'error') + var consoleStub = sinon.stub(console, 'error') coverArt.getCoverFromLastFm(track, images, '') - assert(consoleSpy.calledOnce) + assert.isTrue(consoleStub.calledOnce) getInfoStub.restore() - consoleSpy.restore() + consoleStub.restore() }) }) describe('#getArtistImage()', function () { - beforeEach(function () { - $(images).removeAttr('src') - }) - it('should use default image if no artist is provided', function () { coverArt.getArtistImage('', images, '') - $(images).prop('src').should.endWith('images/user_24x32.png') + expect($(images).prop('src')).to.endWith('images/user_24x32.png') }) it('should get artist info from last.fm', function () { @@ -176,7 +160,7 @@ describe('CoverArt', function () { getInfoStub.yieldsTo('success', getInfoResultMock) coverArt.getArtistImage('mockArtist', images, 'small') - $(images).prop('src').should.endWith('mockArtistImageUri') + expect($(images).prop('src')).to.endWith('mockArtistImageUri') getInfoStub.restore() }) @@ -184,12 +168,12 @@ describe('CoverArt', function () { var getInfoStub = sinon.stub(coverArt.lastfm.artist, 'getInfo') getInfoStub.yieldsTo('error', 'code', 'message') - var consoleSpy = sinon.spy(console, 'error') + var consoleStub = sinon.stub(console, 'error') coverArt.getArtistImage('mockArtist', images, 'small') - assert(consoleSpy.calledOnce) + assert.isTrue(consoleStub.calledOnce) getInfoStub.restore() - consoleSpy.restore() + consoleStub.restore() }) }) }) diff --git a/tests/js/test_library.js b/tests/js/test_library.js new file mode 100644 index 0000000..47c2637 --- /dev/null +++ b/tests/js/test_library.js @@ -0,0 +1,56 @@ +var chai = require('chai') +var expect = chai.expect +var assert = chai.assert +chai.use(require('chai-string')) +chai.use(require('chai-jquery')) + +var sinon = require('sinon') + +var library = require('../../mopidy_musicbox_webclient/static/js/library.js') + +describe('Library', function () { + var selectID = '#selectSearchService' + var schemesArray = ['mockScheme1', 'mockScheme2', 'mockScheme3'] + var mopidy = { getUriSchemes: function () { return $.when(schemesArray) } } + + before(function () { + $(document.body).append('') + $('#selectSearchService').selectmenu() + }) + describe('#getSearchSchemes()', function () { + beforeEach(function () { + $(selectID).empty() + }) + + it('should add human-readable options for backend schemes', function () { + uriHumanList = [['mockScheme2', 'mockUriHuman2']] + + library.getSearchSchemes([], mopidy) + assert.equal($(selectID).children().length, schemesArray.length + 1) + expect($(selectID).children(':eq(2)')).to.have.text('MockUriHuman2') + }) + + it('should get default value from cookie', function () { + $.cookie('searchScheme', 'mockScheme3') + library.getSearchSchemes([], mopidy) + expect($(selectID + ' option:selected')).to.have.value('mockScheme3') + }) + + it('should default to "all" backends if no cookie is available', function () { + $.removeCookie('searchScheme') + library.getSearchSchemes([], mopidy) + expect($(selectID + ' option:selected')).to.have.value('all') + }) + + it('should capitalize first character of backend schema', function () { + library.getSearchSchemes([], mopidy) + expect($(selectID).children(':eq(1)')).to.have.text('MockScheme1') + }) + + it('should blacklist services that should not be searched', function () { + library.getSearchSchemes(['mockScheme2'], mopidy) + assert.equal($(selectID).children().length, schemesArray.length) + expect($(selectID).children()).not.to.contain('mockScheme2') + }) + }) +}) diff --git a/tests/js/test_synced_timer.js b/tests/js/test_synced_timer.js new file mode 100644 index 0000000..458c294 --- /dev/null +++ b/tests/js/test_synced_timer.js @@ -0,0 +1,352 @@ +var chai = require('chai') +var expect = chai.expect +var assert = chai.assert +chai.use(require('chai-string')) +chai.use(require('chai-jquery')) + +var sinon = require('sinon') + +var SyncedProgressTimer = require('../../mopidy_musicbox_webclient/static/js/synced_timer.js') + +describe('SyncedTimer', function () { + var MAX_ATTEMPTS = 8 + var syncedProgressTimer + var mopidy + var playback + var getTimePositionStub + before(function () { + $(document.body).append( + '
              ' + + '' + + '' + + '' + + '' + + '
              ' + ) + $('#trackslider').slider() // Initialize slider + }) + beforeEach(function () { + playback = { + getTimePosition: function () { return $.when(1000) }, + getState: function () { return $.when('stopped') } + } + mopidy = new Mopidy({callingConvention: 'by-position-or-by-name'}) + mopidy.playback = playback + getTimePositionStub = sinon.stub(playback, 'getTimePosition') + // Simulate Mopidy's track position advancing 10ms between each call. + for (var i = 0; i < MAX_ATTEMPTS; i++) { + getTimePositionStub.onCall(i).returns($.when(1000 + i * 10)) + } + mopidy = sinon.stub(mopidy) + syncedProgressTimer = new SyncedProgressTimer(MAX_ATTEMPTS, mopidy) + syncedProgressTimer._isConnected = true + }) + afterEach(function () { + getTimePositionStub.restore() + }) + describe('#SyncedTimer()', function () { + it('should add text nodes to DOM for position and duration indicators', function () { + expect($('#songelapsed')).to.have.text('') + expect($('#songlength')).to.have.text('') + }) + + it('should start out in unsynced state', function () { + assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED) + }) + }) + + describe('#format()', function () { + it('should set value of text node', function () { + assert.equal(SyncedProgressTimer.format(1000), '0:01') + }) + + it('should handle Infinity', function () { + assert.equal(SyncedProgressTimer.format(Infinity), '(n/a)') + }) + + it('should handle zero', function () { + assert.equal(SyncedProgressTimer.format(0), '0:00') + }) + }) + + describe('#timerCallback()', function () { + var clock + beforeEach(function () { + clock = sinon.useFakeTimers() + syncedProgressTimer._progressTimer = new ProgressTimer({ + callback: $.proxy(syncedProgressTimer.timerCallback, syncedProgressTimer), + disableRequestAnimationFrame: true // No window available during testing - use fallback mechanism to schedule updates + }) + }) + afterEach(function () { + clock.restore() + }) + + it('should not try to sync unless connected to mopidy', function () { + syncedProgressTimer._isConnected = false + var _syncScheduledStub = sinon.stub(syncedProgressTimer, '_isSyncScheduled') + assert.isFalse(_syncScheduledStub.called, '_syncScheduledStub called') + _syncScheduledStub.restore() + }) + + it('should update text nodes', function () { + var updateStub = sinon.stub(syncedProgressTimer, '_update') + syncedProgressTimer.set(0, 1000).start() + assert.isTrue(updateStub.called, '_update not called') + updateStub.restore() + }) + + it('should check if a sync is scheduled', function () { + var scheduleStub = sinon.stub(syncedProgressTimer, '_isSyncScheduled').returns(true) + syncedProgressTimer.set(0, 1000).start() + assert.isTrue(scheduleStub.called, '_isSyncScheduled not called') + scheduleStub.restore() + }) + + it('should attempt to perform a sync when scheduled', function () { + var syncStub = sinon.stub(syncedProgressTimer, '_doSync') + syncedProgressTimer.set(0, 1000).start() + clock.tick(250) + assert.isTrue(syncStub.called, '_doSync not called') + syncStub.restore() + }) + + it('should perform sync', function () { + // Simulate runtime on a 5-second track + assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED, 'Timer was initialized in incorrect state') + syncedProgressTimer.set(0, 5000).start() + var wasSyncing = false + for (var i = 0; i < MAX_ATTEMPTS; i++) { + clock.tick(250) // 250ms * MAX_ATTEMPTS is only 2 seconds, but we'll be synced after only two attempts + wasSyncing = wasSyncing || syncedProgressTimer.syncState === SyncedProgressTimer.SYNC_STATE.SYNCING + } + assert.isTrue(wasSyncing, 'Timer never entered the "syncing" state') + assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCED, 'Timer failed to sync') + }) + }) + + describe('#_update()', function () { + it('should clear timers and reset slider to zero while not ready', function () { + syncedProgressTimer.positionNode.nodeValue = '1:00' + syncedProgressTimer.durationNode.nodeValue = '2:00' + $('#trackslider').val(100).slider('refresh') + syncedProgressTimer._update(0, Infinity) + + assert.equal(syncedProgressTimer.positionNode.nodeValue, '') + assert.equal(syncedProgressTimer.durationNode.nodeValue, '') + assert.equal($('#trackslider').val(), 0) + }) + + it('should set duration to "(n/a)" for tracks with infinite duration (e.g. streams)', function () { + syncedProgressTimer._update(1000, Infinity) + assert.equal(syncedProgressTimer.durationNode.nodeValue, '(n/a)') + }) + + it('should show "(wait)" while waiting for Mopidy to supply a position', function () { + syncedProgressTimer._update(1000, 2000) + assert.equal(syncedProgressTimer.positionNode.nodeValue, '(wait)') + }) + + it('should show "(sync)" while trying to sync up with Mopidy', function () { + syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING + syncedProgressTimer._update(1000, 2000) + assert.equal(syncedProgressTimer.positionNode.nodeValue, '(sync)') + }) + + it('should update position text and position track slider when synced', function () { + syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED + syncedProgressTimer._update(1000, 2000) + assert.equal(syncedProgressTimer.positionNode.nodeValue, '0:01') + assert.equal($('#trackslider').val(), 1000) + }) + }) + + describe('#_isSyncScheduled()', function () { + var scheduleSpy + var clock + before(function () { + scheduleSpy = sinon.spy(syncedProgressTimer, '_isSyncScheduled') + clock = sinon.useFakeTimers() + }) + after(function () { + scheduleSpy.restore() + clock.restore() + }) + it('should schedule sync when scheduled time arrives', function () { + syncedProgressTimer._scheduledSyncTime = new Date().getTime() + 1000 + assert.isFalse(syncedProgressTimer._isSyncScheduled()) + clock.tick(1000) + assert.isTrue(syncedProgressTimer._isSyncScheduled()) + }) + }) + + describe('#_doSync', function () { + var clock + beforeEach(function () { + clock = sinon.useFakeTimers() + }) + afterEach(function () { + clock.restore() + }) + it('should not try to sync until timer has been set', function () { + syncedProgressTimer._doSync(0, Infinity) + assert.isFalse(getTimePositionStub.called, 'getTimePosition called even though timer has not been set') + }) + it('should request position from Mopidy', function () { + syncedProgressTimer._doSync(1000, 2000) + assert.isTrue(getTimePositionStub.called, 'getTimePosition not called') + }) + + it('should set state to synced after two consecutive successful syncs (i.e. time drift < 500ms)', function () { + assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED) + clock.tick(10) + syncedProgressTimer._doSync(1010, 2000) + assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING) + clock.tick(10) + syncedProgressTimer._doSync(1020, 2000) + assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCED) + }) + + it('should re-initialize and set state to syncing if time drift is more than 500ms', function () { + syncedProgressTimer._doSync(1, 2000) + assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING) + assert.equal(syncedProgressTimer._syncAttemptsRemaining, syncedProgressTimer._maxAttempts) + }) + + it('should step back exponentially while syncing', function () { + for (var i = 0; i < syncedProgressTimer._maxAttempts; i++) { + syncedProgressTimer._doSync(1000 + (i + 1) * 10, 2000) + // If we don't advance the clock then '_syncAttemptsRemaining' should just contain the step-back in seconds + assert.equal(syncedProgressTimer._syncAttemptsRemaining, syncedProgressTimer._maxAttempts - i - 1, 'Incorrect sync attempts remaining') + assert.equal(syncedProgressTimer._scheduledSyncTime, (0.25 * (Math.pow(2, i)) * 1000), 'Incorrect sync time scheduled') + } + }) + + it('should check sync every 32s once synced', function () { + syncedProgressTimer._syncAttemptsRemaining = 0 + syncedProgressTimer._doSync(1000, 2000) + assert.equal(syncedProgressTimer._scheduledSyncTime, 32000) + }) + + it('should not sync unless track playback is progressing', function () { + getTimePositionStub.restore() + getTimePositionStub = sinon.stub(playback, 'getTimePosition') + getTimePositionStub.returns($.when(1000)) // Simulate playback 'stuck' at 1000ms. + assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED) + clock.tick(10) + syncedProgressTimer._doSync(1010, 2000) + assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING) + clock.tick(10) + syncedProgressTimer._doSync(1010, 2000) + assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING) + }) + }) + + describe('#set()', function () { + it('should throw exception if no arguments are provided', function () { + assert.throw(function () { syncedProgressTimer.set() }, Error) + }) + + it('should set position if only one argument is provided', function () { + syncedProgressTimer.set(1000) + assert.equal(syncedProgressTimer._progressTimer._state.position, 1000) + }) + + it('should update track slider if no sync is scheduled', function () { + syncedProgressTimer.stop() + syncedProgressTimer.set(1000, 2000) + expect($('#songelapsed').text()).to.endWith('0:01') + assert.equal($('#trackslider').val(), 1000) + }) + + it('should implement workaround for https://github.com/adamcik/media-progress-timer/issues/3', function () { + syncedProgressTimer.set(1000, 2000).start() + assert.equal(syncedProgressTimer._duration, 2000) + syncedProgressTimer.set(3000) + assert.equal(syncedProgressTimer._progressTimer._state.position, 1999) + }) + }) + + describe('#start()', function () { + it('should start timer', function () { + var startStub = sinon.stub(syncedProgressTimer._progressTimer, 'start') + syncedProgressTimer.start() + assert(startStub.called) + startStub.restore() + }) + + it('should always start in unsynced state', function () { + syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED + syncedProgressTimer.start() + assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED) + }) + + it('should schedule a sync immediately', function () { + var clock = sinon.useFakeTimers() + syncedProgressTimer._scheduledSyncTime = new Date().getTime() + 5000 + expect(syncedProgressTimer._scheduledSyncTime).to.be.above(new Date().getTime()) + syncedProgressTimer.start() + clock.tick(1000) + expect(syncedProgressTimer._scheduledSyncTime).to.be.below(new Date().getTime()) + clock.restore() + }) + }) + + describe('#stop()', function () { + it('should stop timer', function () { + var stopStub = sinon.stub(syncedProgressTimer._progressTimer, 'stop') + syncedProgressTimer.stop() + assert(stopStub.called) + stopStub.restore() + }) + + it('should show position when stopped', function () { + syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED + syncedProgressTimer._update(1000, 5000) + syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING + syncedProgressTimer._update(2000, 5000) + assert.equal(syncedProgressTimer.positionNode.nodeValue, '(sync)') + syncedProgressTimer.stop() + assert.equal(syncedProgressTimer.positionNode.nodeValue, '0:01') + }) + + it('should cancel any scheduled syncs', function () { + syncedProgressTimer._scheduledSyncTime = 5000 + syncedProgressTimer.stop() + expect(syncedProgressTimer._scheduledSyncTime).to.be.null + }) + }) + + describe('#reset()', function () { + it('should reset timer to 0:00 - (n/a) ', function () { + var resetStub = sinon.stub(syncedProgressTimer._progressTimer, 'reset') + var initStub = sinon.stub(syncedProgressTimer, 'init') + var stopStub = sinon.stub(syncedProgressTimer, 'stop') + + syncedProgressTimer.reset() + + assert(resetStub.called) + assert(initStub.called) + assert(stopStub.called) + + assert.equal(syncedProgressTimer.positionNode.nodeValue, '0:00') + assert.equal(syncedProgressTimer.durationNode.nodeValue, '(n/a)') + + resetStub.restore() + initStub.restore() + stopStub.restore() + }) + }) + + describe('#updatePosition()', function () { + it('should format and set position node', function () { + var formatSpy = sinon.spy(SyncedProgressTimer, 'format') + assert.equal(syncedProgressTimer.positionNode.nodeValue, '') + syncedProgressTimer.updatePosition(1000) + + assert.isTrue(formatSpy.called) + expect(syncedProgressTimer.positionNode.nodeValue).to.endWith('0:01') + formatSpy.restore() + }) + }) +}) diff --git a/tests/test_library.js b/tests/test_library.js deleted file mode 100644 index 1d3b19f..0000000 --- a/tests/test_library.js +++ /dev/null @@ -1,54 +0,0 @@ -var chai = require('chai') -var should = chai.should() -var expect = chai.expect -var assert = chai.assert -chai.use(require('chai-string')) -chai.use(require('chai-jquery')) - -var sinon = require('sinon') - -var library = require('../mopidy_musicbox_webclient/static/js/library.js') - -var selectID = '#selectSearchService' -var schemesArray - -before(function () { - $(document.body).append('') - $('#selectSearchService').selectmenu() -}) -describe('Library', function () { - describe('#getSearchSchemes()', function () { - beforeEach(function () { - schemesArray = ['mockScheme1', 'mockScheme2', 'mockScheme3'] - mopidy = { - getUriSchemes: function () { return $.when(schemesArray) } - } - $(selectID).empty() - }) - - it('should add human-readable options for backend schemes', function () { - uriHumanList = [['mockScheme2', 'mockUriHuman2']] - - library.getSearchSchemes() - assert($(selectID).children().length === schemesArray.length + 1) - $(selectID).children(':eq(2)').should.have.text('MockUriHuman2') - }) - - it('should get default value from cookie', function () { - $.cookie('searchScheme', 'mockScheme3') - library.getSearchSchemes() - $(selectID + ' option:selected').should.have.value('mockScheme3') - }) - - it('should default to "all" backends if no cookie is available', function () { - $.removeCookie('searchScheme') - library.getSearchSchemes() - $(selectID + ' option:selected').should.have.value('all') - }) - - it('should capitalize first character of backend schema', function () { - library.getSearchSchemes() - $(selectID).children(':eq(1)').should.have.text('MockScheme1') - }) - }) -}) From 1345357a5ed55ab660e581df0c013cfe3301b0be Mon Sep 17 00:00:00 2001 From: jcass Date: Sun, 3 Apr 2016 16:06:25 +0200 Subject: [PATCH 17/38] Modularise and test setting of jQuery Mobile config options. --- .../static/js/custom_scripting.js | 29 ++++++++++++++---- mopidy_musicbox_webclient/static/mb.appcache | 2 +- tests/js/test_custom_scripting.js | 30 +++++++++++++++++++ 3 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 tests/js/test_custom_scripting.js diff --git a/mopidy_musicbox_webclient/static/js/custom_scripting.js b/mopidy_musicbox_webclient/static/js/custom_scripting.js index 2b544b6..f8b6ca1 100644 --- a/mopidy_musicbox_webclient/static/js/custom_scripting.js +++ b/mopidy_musicbox_webclient/static/js/custom_scripting.js @@ -1,8 +1,25 @@ // jQuery Mobile configuration options // see: http://api.jquerymobile.com/1.3/global-config/ -$(document).bind('mobileinit', function () { - $.extend($.mobile, { - ajaxEnabled: false, - hashListeningEnabled: false - }) -}) +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory) + } else if (typeof module === 'object' && module.exports) { + module.exports = factory() + } else { + root.configureJQueryMobile = factory() + } +}(this, function () { + 'use strict' + + function configureJQueryMobile () { + $.extend($.mobile, { + ajaxEnabled: false, + hashListeningEnabled: false + }) + } + + $(document).bind('mobileinit', configureJQueryMobile) + + return configureJQueryMobile +})) + diff --git a/mopidy_musicbox_webclient/static/mb.appcache b/mopidy_musicbox_webclient/static/mb.appcache index 246f924..f21d33f 100644 --- a/mopidy_musicbox_webclient/static/mb.appcache +++ b/mopidy_musicbox_webclient/static/mb.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# 2016-04-03:v1 +# 2016-04-03:v2 NETWORK: * diff --git a/tests/js/test_custom_scripting.js b/tests/js/test_custom_scripting.js new file mode 100644 index 0000000..5b3e171 --- /dev/null +++ b/tests/js/test_custom_scripting.js @@ -0,0 +1,30 @@ +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 configureJQueryMobile = require('../../mopidy_musicbox_webclient/static/js/custom_scripting.js') + +describe('jQuery Defaults', function () { + it('should disable ajax and hashListening', function () { + expect($.mobile.ajaxEnabled).to.be.true + expect($.mobile.hashListeningEnabled).to.be.true + + configureJQueryMobile() + expect($.mobile.ajaxEnabled).to.be.false + expect($.mobile.hashListeningEnabled).to.be.false + }) + + it('should bind to "mobileinit"', function () { + var configSpy = sinon.spy(configureJQueryMobile) + + $(document).bind('mobileinit', configSpy) + expect(configSpy.called).to.be.false + $(document).trigger('mobileinit') + expect(configSpy.called).to.be.true + configSpy.reset() + }) +}) From f43a9a7afaffc50e3abe278a3df36203f8ca7308 Mon Sep 17 00:00:00 2001 From: jcass Date: Mon, 4 Apr 2016 05:14:45 +0200 Subject: [PATCH 18/38] Optimise progress timer callback. --- .../static/js/synced_timer.js | 124 +++--- mopidy_musicbox_webclient/static/mb.appcache | 2 +- .../vendors/media_progress_timer/timer.js | 12 +- tests/js/test_synced_timer.js | 363 +++++++++++++----- 4 files changed, 329 insertions(+), 172 deletions(-) diff --git a/mopidy_musicbox_webclient/static/js/synced_timer.js b/mopidy_musicbox_webclient/static/js/synced_timer.js index 25b2eb9..e66b0eb 100644 --- a/mopidy_musicbox_webclient/static/js/synced_timer.js +++ b/mopidy_musicbox_webclient/static/js/synced_timer.js @@ -34,8 +34,8 @@ this.positionNode = document.createTextNode('') this.durationNode = document.createTextNode('') - $('#songelapsed').append(this.positionNode) - $('#songlength').append(this.durationNode) + $('#songelapsed').empty().append(this.positionNode) + $('#songlength').empty().append(this.durationNode) this._progressTimer = new ProgressTimer({ // Make sure that the timer object's context is available. @@ -47,7 +47,12 @@ 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() + this.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED + this._isSyncScheduled = false + this._scheduleID = null + this._syncAttemptsRemaining = this._maxAttempts + this._previousSyncPosition = null + this._duration = null } SyncedProgressTimer.SYNC_STATE = { @@ -58,7 +63,7 @@ SyncedProgressTimer.format = function (milliseconds) { if (milliseconds === Infinity) { - return '(n/a)' + return '' } else if (milliseconds === 0) { return '0:00' } @@ -71,57 +76,37 @@ 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()) { + if (this._isSyncScheduled && this._isConnected) { 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') + 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 } } - SyncedProgressTimer.prototype._isSyncScheduled = function () { - return this._scheduledSyncTime !== null && this._scheduledSyncTime <= new Date().getTime() + SyncedProgressTimer.prototype._scheduleSync = function (milliseconds) { + // Use an anonymous callback to set a boolean value, which should be faster to + // check in the timeout callback than doing another function call. + clearTimeout(this._scheduleID) + this._isSyncScheduled = false + this._scheduleID = setTimeout($.proxy(function () { this._isSyncScheduled = true }, this), milliseconds) } SyncedProgressTimer.prototype._doSync = function (position, duration) { @@ -130,11 +115,14 @@ // 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 (_this.syncState === SyncedProgressTimer.SYNC_STATE.NOT_SYNCED) { + _this.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING + } 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 @@ -143,14 +131,13 @@ } _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 + _this._scheduleSync(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._syncAttemptsRemaining = _this._maxAttempts + _this._previousSyncPosition = null + _this._scheduleSync(1000) _this._progressTimer.set(targetPosition) } }) @@ -160,58 +147,61 @@ if (arguments.length === 0) { throw new Error('"SyncedProgressTimer.set" requires the "position" argument.') } + + this.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED + this._syncAttemptsRemaining = this._maxAttempts // 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) + this.durationNode.nodeValue = SyncedProgressTimer.format(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') - } + this.updatePosition(position, 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._scheduleSync(0) this._progressTimer.start() return this } SyncedProgressTimer.prototype.stop = function () { this._progressTimer.stop() - this._scheduledSyncTime = null - this.updatePosition(this._previousSyncPosition) + clearTimeout(this._scheduleID) + this._isSyncScheduled = false + if (this.syncState !== SyncedProgressTimer.SYNC_STATE.SYNCED && this._previousSyncPosition) { + // Timer was busy trying to sync when it was stopped, fallback to displaying the last synced position on screen. + this.positionNode.nodeValue = SyncedProgressTimer.format(this._previousSyncPosition) + } return this } SyncedProgressTimer.prototype.reset = function () { - this._progressTimer.reset() + // this._progressTimer.reset() this.stop() - this.init() this.set(0, Infinity) return this } SyncedProgressTimer.prototype.updatePosition = function (position) { - this.positionNode.nodeValue = SyncedProgressTimer.format(position) + if (!(this._duration === Infinity && position === 0)) { + this.positionNode.nodeValue = SyncedProgressTimer.format(position) + } else { + this.positionNode.nodeValue = '' + } } return SyncedProgressTimer diff --git a/mopidy_musicbox_webclient/static/mb.appcache b/mopidy_musicbox_webclient/static/mb.appcache index f21d33f..720bf20 100644 --- a/mopidy_musicbox_webclient/static/mb.appcache +++ b/mopidy_musicbox_webclient/static/mb.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# 2016-04-03:v2 +# 2016-04-06:v1 NETWORK: * diff --git a/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js b/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js index 4592fc0..59145ca 100644 --- a/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js +++ b/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js @@ -15,9 +15,15 @@ 'use strict'; // Helper function to provide a reference time in milliseconds. - var now = typeof window.performance !== 'undefined' && - typeof window.performance.now !== 'undefined' && - window.performance.now.bind(window.performance) || Date.now || + var now = /* Sinon does not currently support faking `window.performance` + (see https://github.com/sinonjs/sinon/issues/803). + Changing this to only rely on `new Date().getTime() + in the interim in order to allow testing of the + progress timer from MMW. + + typeof window.performance !== 'undefined' && + typeof window.performance.now !== 'undefined' && + window.performance.now.bind(window.performance) || Date.now ||*/ function() { return new Date().getTime(); }; // Helper to warn library users about deprecated features etc. diff --git a/tests/js/test_synced_timer.js b/tests/js/test_synced_timer.js index 458c294..1ab85f6 100644 --- a/tests/js/test_synced_timer.js +++ b/tests/js/test_synced_timer.js @@ -14,6 +14,20 @@ describe('SyncedTimer', function () { var mopidy var playback var getTimePositionStub + var clock + + function setFakeTimers () { + 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 + }) + } + + function restoreFakeTimers () { + clock.restore() + } + before(function () { $(document.body).append( '
              ' + @@ -24,26 +38,40 @@ describe('SyncedTimer', function () { '
              ' ) $('#trackslider').slider() // Initialize slider - }) - beforeEach(function () { + $('#trackslider').on('slidestart', function () { + syncedProgressTimer.stop() + $('#trackslider').on('change', function () { syncedProgressTimer.updatePosition($(this).val()) }) + }) + + $('#trackslider').on('slidestop', function () { + $('#trackslider').off('change') + syncedProgressTimer.updatePosition($(this).val()) + // Simulate doSeekPos($(this).val()) + syncedProgressTimer.set($(this).val()) + }) + 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)) + // Simulate Mopidy's track position advancing 250ms between each call for 0:01 to 0:10 + for (var i = 0; i < 10000 / 250; i++) { + getTimePositionStub.onCall(i).returns($.when((i + 1) * 250)) } - mopidy = sinon.stub(mopidy) + mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'})) + mopidy.playback = playback + }) + + beforeEach(function () { syncedProgressTimer = new SyncedProgressTimer(MAX_ATTEMPTS, mopidy) syncedProgressTimer._isConnected = true }) + afterEach(function () { - getTimePositionStub.restore() + getTimePositionStub.reset() }) + describe('#SyncedTimer()', function () { it('should add text nodes to DOM for position and duration indicators', function () { expect($('#songelapsed')).to.have.text('') @@ -61,7 +89,7 @@ describe('SyncedTimer', function () { }) it('should handle Infinity', function () { - assert.equal(SyncedProgressTimer.format(Infinity), '(n/a)') + assert.equal(SyncedProgressTimer.format(Infinity), '') }) it('should handle zero', function () { @@ -70,44 +98,57 @@ describe('SyncedTimer', function () { }) 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 - }) + setFakeTimers() }) afterEach(function () { - clock.restore() + restoreFakeTimers() }) it('should not try to sync unless connected to mopidy', function () { + var _doSyncStub = sinon.stub(syncedProgressTimer, '_doSync') + syncedProgressTimer._isConnected = false - var _syncScheduledStub = sinon.stub(syncedProgressTimer, '_isSyncScheduled') - assert.isFalse(_syncScheduledStub.called, '_syncScheduledStub called') - _syncScheduledStub.restore() + syncedProgressTimer.set(0, 1000).start() + clock.tick(1000) + + assert.isFalse(_doSyncStub.called, '_doSync called') + syncedProgressTimer.stop() + _doSyncStub.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') + syncedProgressTimer.stop() 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 as soon as timer is started', function () { + var syncStub = sinon.stub(syncedProgressTimer, '_doSync') + + syncedProgressTimer.set(0, 1000).start() // 'start' will immediately schedule a sync. + clock.tick(250) + + assert.isTrue(syncStub.called, '_doSync not called') + syncedProgressTimer.stop() + syncStub.restore() }) - it('should attempt to perform a sync when scheduled', function () { + it('should not attempt to perform a sync untill scheduled', function () { var syncStub = sinon.stub(syncedProgressTimer, '_doSync') - syncedProgressTimer.set(0, 1000).start() + + syncedProgressTimer.set(0, 5000).start() + syncedProgressTimer._scheduleSync(500) clock.tick(250) - assert.isTrue(syncStub.called, '_doSync not called') + assert.isFalse(syncStub.called, 'next _doSync should only have been called after 500ms') + + syncStub.reset() + clock.tick(500) + assert.isTrue(syncStub.called, 'next _doSync not called after 500ms') + syncedProgressTimer.stop() syncStub.restore() }) @@ -115,34 +156,27 @@ describe('SyncedTimer', 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 + for (var i = 0; i < 4; i++) { + clock.tick(250) wasSyncing = wasSyncing || syncedProgressTimer.syncState === SyncedProgressTimer.SYNC_STATE.SYNCING } + syncedProgressTimer.stop() assert.isTrue(wasSyncing, 'Timer never entered the "syncing" state') assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCED, 'Timer failed to sync') + syncedProgressTimer.stop() }) }) 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 () { + it('should set duration to "" for tracks with infinite duration (e.g. streams)', function () { syncedProgressTimer._update(1000, Infinity) - assert.equal(syncedProgressTimer.durationNode.nodeValue, '(n/a)') + assert.equal(syncedProgressTimer.durationNode.nodeValue, '') }) - it('should show "(wait)" while waiting for Mopidy to supply a position', function () { + it('should show "(wait)" while untill syncing starts', function () { + syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED syncedProgressTimer._update(1000, 2000) assert.equal(syncedProgressTimer.positionNode.nodeValue, '(wait)') }) @@ -161,84 +195,122 @@ describe('SyncedTimer', function () { }) }) - describe('#_isSyncScheduled()', function () { - var scheduleSpy - var clock - before(function () { - scheduleSpy = sinon.spy(syncedProgressTimer, '_isSyncScheduled') - clock = sinon.useFakeTimers() + describe('#scheduleSync', function () { + beforeEach(function () { + setFakeTimers() }) - after(function () { - scheduleSpy.restore() - clock.restore() + afterEach(function () { + restoreFakeTimers() }) + 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()) + clock.tick(0) + syncedProgressTimer._scheduleSync(1000) + assert.isFalse(syncedProgressTimer._isSyncScheduled) + clock.tick(1001) + assert.isTrue(syncedProgressTimer._isSyncScheduled) + }) + + it('should clear schedule on each call', function () { + var clearSpy = sinon.spy(window, 'clearTimeout') + + clock.tick(0) + syncedProgressTimer._isSyncScheduled = true + syncedProgressTimer._scheduleSync(1000) + assert.isFalse(syncedProgressTimer._isSyncScheduled) + + var scheduleID = syncedProgressTimer._scheduleID + clock.tick(1001) + syncedProgressTimer._scheduleSync(1000) + assert(clearSpy.calledWith(scheduleID)) + clearSpy.restore() }) }) describe('#_doSync', function () { - var clock beforeEach(function () { - clock = sinon.useFakeTimers() + setFakeTimers() }) afterEach(function () { - clock.restore() + restoreFakeTimers() }) + 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') + assert.isFalse(getTimePositionStub.called, 'tried to do sync even though the 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 "SYNCING" as soon as the first sync attempt is made', function () { + syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED + syncedProgressTimer._doSync(100, 2000) + assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING) + }) + 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) + clock.tick(250) + syncedProgressTimer._doSync(250, 2000) assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING) - clock.tick(10) - syncedProgressTimer._doSync(1020, 2000) + clock.tick(250) + syncedProgressTimer._doSync(500, 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) + var scheduleStub = sinon.stub(syncedProgressTimer, '_scheduleSync') + + syncedProgressTimer._doSync(1000, 2000) + assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING) assert.equal(syncedProgressTimer._syncAttemptsRemaining, syncedProgressTimer._maxAttempts) + assert.isNull(syncedProgressTimer._previousSyncPosition) + assert(scheduleStub.calledWith(1000), 'Expected next sync to be scheduled 1s from now') + scheduleStub.restore() }) it('should step back exponentially while syncing', function () { + var scheduleStub = sinon.stub(syncedProgressTimer, '_scheduleSync') + 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') + syncedProgressTimer._doSync(i * 250, 2000) + assert.equal(syncedProgressTimer._syncAttemptsRemaining, syncedProgressTimer._maxAttempts - i - 1, 'Incorrect number of sync attempts remaining') + assert(scheduleStub.calledWith(0.25 * (Math.pow(2, i)) * 1000), 'Incorrect sync time scheduled: ' + scheduleStub.getCall(i)) + scheduleStub.reset() } + scheduleStub.restore() }) it('should check sync every 32s once synced', function () { + var scheduleStub = sinon.stub(syncedProgressTimer, '_scheduleSync') + syncedProgressTimer._syncAttemptsRemaining = 0 - syncedProgressTimer._doSync(1000, 2000) - assert.equal(syncedProgressTimer._scheduledSyncTime, 32000) + syncedProgressTimer._doSync(250, 2000) + assert(scheduleStub.calledWith(32000)) + scheduleStub.restore() }) 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) + clock.tick(250) + syncedProgressTimer._doSync(250, 2000) assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING) - clock.tick(10) - syncedProgressTimer._doSync(1010, 2000) + clock.tick(250) + syncedProgressTimer._doSync(250, 2000) assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING) + + // Restore getTimePositionStub to previous state + getTimePositionStub = sinon.stub(playback, 'getTimePosition') + // Simulate Mopidy's track position advancing 250ms between each call for 0:01 to 0:10 + for (var i = 0; i < 10000 / 250; i++) { + getTimePositionStub.onCall(i).returns($.when((i + 1) * 250)) + } }) }) @@ -252,18 +324,21 @@ describe('SyncedTimer', function () { assert.equal(syncedProgressTimer._progressTimer._state.position, 1000) }) - it('should update track slider if no sync is scheduled', function () { + it('should update position and track slider immediately', function () { syncedProgressTimer.stop() syncedProgressTimer.set(1000, 2000) - expect($('#songelapsed').text()).to.endWith('0:01') + + expect($('#songelapsed').text()).to.equal('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) + assert.equal(syncedProgressTimer._progressTimer._state.position, 1999, 'Expected position to be less than duration') + syncedProgressTimer.stop() }) }) @@ -272,6 +347,7 @@ describe('SyncedTimer', function () { var startStub = sinon.stub(syncedProgressTimer._progressTimer, 'start') syncedProgressTimer.start() assert(startStub.called) + syncedProgressTimer.stop() startStub.restore() }) @@ -279,16 +355,19 @@ describe('SyncedTimer', function () { syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED syncedProgressTimer.start() assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED) + syncedProgressTimer.stop() }) 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()) + var scheduleSpy = sinon.spy(syncedProgressTimer, '_scheduleSync') + + syncedProgressTimer.set(0, 1000) + syncedProgressTimer._isSyncScheduled = false syncedProgressTimer.start() - clock.tick(1000) - expect(syncedProgressTimer._scheduledSyncTime).to.be.below(new Date().getTime()) - clock.restore() + + assert(scheduleSpy.calledWith(0)) + syncedProgressTimer.stop() + scheduleSpy.restore() }) }) @@ -296,45 +375,51 @@ describe('SyncedTimer', function () { it('should stop timer', function () { var stopStub = sinon.stub(syncedProgressTimer._progressTimer, 'stop') syncedProgressTimer.stop() + assert(stopStub.called) + syncedProgressTimer.stop() stopStub.restore() }) - it('should show position when stopped', function () { + it('should show last synced position if stopped while busy syncing', function () { + syncedProgressTimer.set(1000, 5000) syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED - syncedProgressTimer._update(1000, 5000) + syncedProgressTimer._previousSyncPosition = 1000 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') + expect($('#songelapsed').text()).to.equal('0:01') }) it('should cancel any scheduled syncs', function () { - syncedProgressTimer._scheduledSyncTime = 5000 + var cancelSpy = sinon.spy(window, 'clearTimeout') + + syncedProgressTimer._isSyncScheduled = true syncedProgressTimer.stop() - expect(syncedProgressTimer._scheduledSyncTime).to.be.null + + assert.isFalse(syncedProgressTimer._isSyncScheduled) + assert(cancelSpy.calledWith(syncedProgressTimer._scheduleID)) + cancelSpy.restore() }) }) 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') + it('should reset timer to "" - "" ', function () { var stopStub = sinon.stub(syncedProgressTimer, 'stop') + var setStub = sinon.stub(syncedProgressTimer, 'set') syncedProgressTimer.reset() - assert(resetStub.called) - assert(initStub.called) assert(stopStub.called) + assert(setStub.called) - assert.equal(syncedProgressTimer.positionNode.nodeValue, '0:00') - assert.equal(syncedProgressTimer.durationNode.nodeValue, '(n/a)') + assert.equal(syncedProgressTimer.positionNode.nodeValue, '', 'Position node was not reset') + assert.equal(syncedProgressTimer.durationNode.nodeValue, '', 'Duration node was not reset') - resetStub.restore() - initStub.restore() stopStub.restore() + setStub.restore() }) }) @@ -345,8 +430,84 @@ describe('SyncedTimer', function () { syncedProgressTimer.updatePosition(1000) assert.isTrue(formatSpy.called) - expect(syncedProgressTimer.positionNode.nodeValue).to.endWith('0:01') + expect(syncedProgressTimer.positionNode.nodeValue).to.equal('0:01') formatSpy.restore() }) + + it('should set position to "" if timer has not been initialized', function () { + syncedProgressTimer.set(1000, 2000) + expect(syncedProgressTimer.positionNode.nodeValue).to.equal('0:01') + + syncedProgressTimer.updatePosition(0) + assert.equal(syncedProgressTimer.positionNode.nodeValue, '0:00', 'Position node was not reset') + + syncedProgressTimer.reset() + syncedProgressTimer.updatePosition(0) + assert.equal(syncedProgressTimer.positionNode.nodeValue, '', 'Position node was not reset') + }) + }) + + describe('integration tests', function () { + beforeEach(function () { + setFakeTimers() + }) + afterEach(function () { + restoreFakeTimers() + }) + + it('simulate 30-second test run, ', function () { + // Initialize + syncedProgressTimer.reset() + expect($('#songelapsed').text()).to.equal('') + expect($('#songlength').text()).to.equal('') + assert.equal($('#trackslider').val(), 0) + + // Set song info + syncedProgressTimer.set(0, 30000) + expect($('#songelapsed').text()).to.equal('0:00') + expect($('#songlength').text()).to.equal('0:30') + assert.equal($('#trackslider').val(), 0) + + // Start + syncedProgressTimer.start() + clock.tick(40) + expect($('#songelapsed').text()).to.equal('(wait)') + expect($('#songlength').text()).to.equal('0:30') + assert.equal($('#trackslider').val(), 0) + + // Syncing + clock.tick(250) + expect($('#songelapsed').text()).to.equal('(sync)') + expect($('#songlength').text()).to.equal('0:30') + assert.equal($('#trackslider').val(), 0) + + // Synced + clock.tick(1000) + expect($('#songelapsed').text()).to.equal('0:01') + expect($('#songlength').text()).to.equal('0:30') + assert.isAtLeast($('#trackslider').val(), 1000) + + // Move slider + $('#trackslider').trigger('slidestart') + clock.tick(250) + $('#trackslider').val(5000).slider('refresh') + $('#trackslider').trigger('change') + clock.tick(250) + $('#trackslider').trigger('slidestop') + + clock.tick(1000) // Position should remain '0:05' as the timer should not be running after a slider change + expect($('#songelapsed').text()).to.equal('0:05') + + // Start -> Sync -> Stop + syncedProgressTimer.start() + clock.tick(40) + expect($('#songelapsed').text()).to.equal('(sync)') + syncedProgressTimer._previousSyncPosition = 1000 + syncedProgressTimer.stop() + expect($('#songelapsed').text()).to.equal('0:01') + expect($('#songlength').text()).to.equal('0:30') + + syncedProgressTimer.stop() + }) }) }) From 9e4793b14db91bc4318ccdf804b616c2370ca339 Mon Sep 17 00:00:00 2001 From: jcass Date: Tue, 12 Apr 2016 08:42:37 +0200 Subject: [PATCH 19/38] Workaround for https://github.com/adamcik/media-progress-timer/issues/3 Ensure that timer.js will keep on running until stopped explicitly by MMW. --- mopidy_musicbox_webclient/static/mb.appcache | 2 +- .../vendors/media_progress_timer/timer.js | 12 ++++--- tests/js/test_synced_timer.js | 34 ++++++++++++++----- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/mopidy_musicbox_webclient/static/mb.appcache b/mopidy_musicbox_webclient/static/mb.appcache index 720bf20..7b69bc6 100644 --- a/mopidy_musicbox_webclient/static/mb.appcache +++ b/mopidy_musicbox_webclient/static/mb.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# 2016-04-06:v1 +# 2016-04-12:v1 NETWORK: * diff --git a/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js b/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js index 59145ca..9ca7e85 100644 --- a/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js +++ b/mopidy_musicbox_webclient/static/vendors/media_progress_timer/timer.js @@ -153,12 +153,14 @@ // TODO: Consider wrapping this in a try/catch? this._userCallback(userPosisition, state.duration); - - if (state.position < state.duration) { + // Workaround for https://github.com/adamcik/media-progress-timer/issues/3 + // Mopidy <= 1.1.2 does not always return the correct track position as + // track changes are being done, which can cause the timer to die unexpectedly. + //if (state.position < state.duration) { this._updateId = this._schedule(timestamp); // Schedule update. - } else { - this._updateId = null; // Unset since we didn't reschedule. - } + //} else { + // this._updateId = null; // Unset since we didn't reschedule. + //} }; return ProgressTimer; diff --git a/tests/js/test_synced_timer.js b/tests/js/test_synced_timer.js index 1ab85f6..d921eb2 100644 --- a/tests/js/test_synced_timer.js +++ b/tests/js/test_synced_timer.js @@ -331,15 +331,6 @@ describe('SyncedTimer', function () { expect($('#songelapsed').text()).to.equal('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, 'Expected position to be less than duration') - syncedProgressTimer.stop() - }) }) describe('#start()', function () { @@ -510,4 +501,29 @@ describe('SyncedTimer', function () { syncedProgressTimer.stop() }) }) + + describe('regression tests for https://github.com/adamcik/media-progress-timer/issues/3', function () { + it('should not be possible to set position > duration', function () { + syncedProgressTimer.set(1000, 2000).start() + + assert.equal(syncedProgressTimer._duration, 2000) + syncedProgressTimer.set(3000) + assert.equal(syncedProgressTimer._progressTimer._state.position, 1999, 'Expected position to be less than duration') + syncedProgressTimer.stop() + }) + + it('should keep timer running even if an update would cause position > duration', function () { + setFakeTimers() + + clock.tick(0) + clock.tick(1000) + syncedProgressTimer.set(0, 1000).start() + clock.tick(2000) + + assert.isNotNull(syncedProgressTimer._progressTimer._updateId) + syncedProgressTimer.stop() + + restoreFakeTimers() + }) + }) }) From 3dfbd76a8359c7ab6c2ed7b856daa1a6c950fb3f Mon Sep 17 00:00:00 2001 From: John Cass Date: Tue, 19 Apr 2016 17:15:05 +0200 Subject: [PATCH 20/38] * Retrieve album cover and artist images using MusicBrianz ID, if available. * Retrieve album cover and artist images using MusicBrianz ID, if available. --- README.rst | 2 + .../static/js/functionsvars.js | 2 +- mopidy_musicbox_webclient/static/js/gui.js | 4 +- mopidy_musicbox_webclient/static/js/images.js | 260 ++++++-- .../static/js/process_ws.js | 8 +- .../static/js/synced_timer.js | 1 - tests/js/test_images.js | 575 +++++++++++++++--- 7 files changed, 705 insertions(+), 147 deletions(-) diff --git a/README.rst b/README.rst index 5d3a973..70de863 100644 --- a/README.rst +++ b/README.rst @@ -78,11 +78,13 @@ v2.3.0 (UNRELEASED) - Now remembers which backend was searched previously, and automatically selects that backend as the default search target. (Addresses: `#130 `_). - Upgrade Media Progress Timer to version 3.0.0. +- Now retrieves album cover and artist images using MusicBrainzID, if available. **Fixes** - Don't create Mopidy models manually. (Fixes: `#172 `_). - Context menu is now available for all tracks in browse pane. (Fixes: `#126 `_). +- last.fm artist image lookups should now always return the correct image for similarly named artists. v2.2.0 (2016-03-01) ------------------- diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index ae1634a..ea4ff05 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -269,7 +269,7 @@ function renderSongLiDivider (track, nextTrack, currentIndex, target) { renderSongLiTrackArtists(track) + '

            • ' ) // Retrieve album covers - coverArt.getCover(track.uri, getjQueryID(target + '-cover', track.uri, true), 'small', mopidy) + images.setAlbumImage(track.uri, getjQueryID(target + '-cover', track.uri, true), mopidy, 'small') } else if (currentIndex > 0) { // Small divider $(target).before('
            •  
            • ') diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index 932ee1a..abe94d5 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -128,12 +128,10 @@ function setSongInfo (data) { } if (data.track.album && data.track.album.name) { $('#modalalbum').html('' + data.track.album.name + '') - coverArt.getCover(data.track.uri, '#infocover, #controlspopupimage', 'extralarge', mopidy) } else { $('#modalalbum').html('') - $('#infocover').attr('src', 'images/default_cover.png') - $('#controlspopupimage').attr('src', 'images/default_cover.png') } + images.setAlbumImage(data.track.uri, '#infocover, #controlspopupimage', mopidy) $('#modalartist').html(arttmp) diff --git a/mopidy_musicbox_webclient/static/js/images.js b/mopidy_musicbox_webclient/static/js/images.js index 8bef0c3..19f0c28 100644 --- a/mopidy_musicbox_webclient/static/js/images.js +++ b/mopidy_musicbox_webclient/static/js/images.js @@ -4,7 +4,7 @@ } else if (typeof module === 'object' && module.exports) { module.exports = factory() } else { - root.coverArt = factory() + root.images = factory() } }(this, function () { 'use strict' @@ -12,97 +12,271 @@ var API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733' var API_SECRET = '2c631802c2285d5d5d1502462fe42a2b' - var coverArt = { - lastfm: new LastFM({ + var images = { + + DEFAULT_ALBUM_URL: 'images/default_cover.png', + DEFAULT_ARTIST_URL: 'images/user_24x32.png', + + lastFM: new LastFM({ apiKey: API_KEY, apiSecret: API_SECRET, cache: new LastFMCache() }), - getCover: function (uri, images, size, mopidy) { - var defUrl = 'images/default_cover.png' - $(images).attr('src', defUrl) + /* Extract artist information from Mopidy track. */ + _getArtistInfo: function (track) { + var artistName = '' + var musicBrainzID = '' + + if (track && track.artists && (track.artists.length > 0)) { + // First look for the artist info in the track + artistName = track.artists[0].name + musicBrainzID = track.artists[0].musicbrainz_id + } + + if ((!artistName || !musicBrainzID) && (track && track.album && track.album.artists && track.album.artists.length > 0)) { + // Fallback to using artist info contained in the track's album + artistName = artistName || track.album.artists[0].name + musicBrainzID = musicBrainzID || track.album.artists[0].musicbrainz_id + } + + return {mbid: musicBrainzID, name: artistName} + }, + + /* Utility function for retrieving artist informaton for the given track from last.fm */ + _getLastFmArtistInfo: function (track) { + var artist = images._getArtistInfo(track) + var artistPromise = $.Deferred() + + if (!(track && (track.musicbrainz_id || (track.name && artist && artist.name)))) { + // Avoid expensive last.fm call if tag information is missing. + return artistPromise.reject('none', 'Not enough tag information available for track to make last.fm call.') + } + + var params = {} + // Only add arguments to parameter object if values are available for them. + if (track.musicbrainz_id) { + params.mbid = track.musicbrainz_id + } + if (track.name && artist.name) { + params.track = track.name + params.artist = artist.name + } + + images.lastFM.track.getInfo(params, {success: function (data) { + artistPromise.resolve(data.track.artist) + }, error: function (code, message) { + artistPromise.reject(code, message) + }}) + + return artistPromise + }, + + /* Utility function for retrieving information for the given track from last.fm. */ + _getLastFmAlbumInfo: function (track) { + var artist = images._getArtistInfo(track) + var albumPromise = $.Deferred() + + if (!(track && track.album && (track.album.musicbrainz_id || (track.album.name && artist && artist.name)))) { + // Avoid expensive last.fm call if tag information is missing. + return albumPromise.reject('none', 'Not enough tag information available for album to make last.fm call.') + } + + var musicBrainzID = track.album.musicbrainz_id + var albumName = track.album.name + var artistName = images._getArtistInfo(track).name + + var params = {} + // Only add arguments to parameter object if values are available for them. + if (musicBrainzID) { + params.mbid = musicBrainzID + } + if (artistName && albumName) { + params.artist = artistName + params.album = albumName + } + + images.lastFM.album.getInfo(params, {success: function (data) { + albumPromise.resolve(data) + }, error: function (code, message) { + albumPromise.reject(code, message) + }}) + + return albumPromise + }, + + /** + * Sets an HTML image element to contain the album cover art of the relevant Mopidy track. + * + * Potential sources for the album image will be interrogated in the following order until + * a suitable image URI is found: + * 1.) mopidy.library.getImages + * 2.) mopidy.models.Track.album.images (DEPRECATED) + * 3.) last.fm using the album MusicBrainz ID + * 4.) last.fm using the album name and track artist name + * 5.) last.fm using the album name and album artist name + * 6.) a default image + * + * @param {string} uri - The URI of the Mopidy track to retrieve the album cover image for. + * @param {string} img_element - The identifier of the HTML image element that will be used + * to render the image. + * @param {object} mopidy - The Mopidy.js object that should be used to communicate with the + * Mopidy server. + * @param {string} size - (Optional) The preferred size of the image. This parameter is only + * used in the last.fm lookups if Mopidy does not provide the image + * directly. Can be one of 'small', 'medium', 'large', + * 'extralarge' (default), or 'mega'. + */ + setAlbumImage: function (uri, img_element, mopidy, size) { + // Set default immediately while we're busy retrieving actual image. + $(img_element).attr('src', images.DEFAULT_ALBUM_URL) if (!uri) { return } + size = size || 'extralarge' + 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) + $(img_element).attr('src', imageResults[uri][0].uri) } else { // Also check deprecated 'album.images' in case backend does not // implement mopidy.library.getImages yet... - coverArt.getCoverFromAlbum(uri, images, size, mopidy) + images._setDeprecatedAlbumImage(uri, img_element, mopidy, 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, mopidy) { - var defUrl = 'images/default_cover.png' - $(images).attr('src', defUrl) + // TODO: Remove when Mopidy no longer supports retrieving images + // from 'album.images'. + /* Set album image using mopidy.album.images. */ + _setDeprecatedAlbumImage: function (uri, img_element, mopidy, size) { if (!uri) { + $(img_element).attr('src', images.DEFAULT_ALBUM_URL) return } + size = size || 'extralarge' + 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) + $(img_element).attr('src', track.album.images[0]) } else { - return + // Fallback to last.fm + images._setLastFmAlbumImage(track, img_element, size) } }) }, - getCoverFromLastFm: function (track, images, size) { - var defUrl = 'images/default_cover.png' - $(images).attr('src', defUrl) + /* Lookup album image on last.fm using the provided Mopidy track. */ + _setLastFmAlbumImage: function (track, img_element, size) { if (!track || !(track.album || track.artists)) { + $(img_element).attr('src', images.DEFAULT_ALBUM_URL) 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 - } + size = size || 'extralarge' - this.lastfm.album.getInfo({artist: artistname, album: albumname}, {success: function (data) { + images._getLastFmAlbumInfo(track).then(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) + $(img_element).attr('src', data.album.image[i]['#text'] || images.DEFAULT_ALBUM_URL) + break } } - }, error: function (code, message) { - console.error('Error retrieving album info from last.fm', code, message) - }}) + }, function (code, message) { + $(img_element).attr('src', images.DEFAULT_ALBUM_URL) + console.error('Error getting album info from last.fm (%s: %s)', code, message) + }) }, - getArtistImage: function (artist, images, size) { - var defUrl = 'images/user_24x32.png' - $(images).attr('src', defUrl) - if (!artist || artist.length === 0) { + /** + * Sets an HTML image element to contain the artist image of the relevant Mopidy track. + * + * Potential sources of the artist image will be interrogated in the following order until + * a suitable image URI is found: + * 1.) mopidy.library.getImages + * 2.) last.fm using the artist MusicBrainz ID. If no artist ID is provided, it will be + * looked up on last.fm first using the track and album details. + * 3.) a default image + * + * @param {string} artist_uri - The URI of the Mopidy artist to retrieve the image for. + * @param {string} track_uri - The URI of the Mopidy track that will be used as a fallback + * if the artist URI does not provide any image results. + * @param {string} img_element - The identifier of the HTML image element that will be used + * to render the image. + * @param {object} mopidy - The Mopidy.js object that should be used to communicate with the + * Mopidy server. + * @param {string} size - (Optional) The preferred size of the image. This parameter is only + * used in the last.fm lookups if Mopidy does not provide the image + * directly. Can be one of 'small', 'medium', 'large', + * 'extralarge' (default), or 'mega'. + */ + setArtistImage: function (artist_uri, track_uri, img_element, mopidy, size) { + // Set default immediately while we're busy retrieving actual image. + $(img_element).attr('src', images.DEFAULT_ARTIST_URL) + if (!artist_uri && !track_uri) { return } - this.lastfm.artist.getInfo({artist: artist}, {success: function (data) { + size = size || 'extralarge' + + if (artist_uri) { + // Use artist as starting point for retrieving image. + mopidy.library.getImages({'uris': [artist_uri]}).then(function (imageResults) { + var uri = Object.keys(imageResults)[0] + if (imageResults[uri].length > 0) { + $(img_element).attr('src', imageResults[uri][0].uri) + } else { + // Fall back to using track as starting point for retrieving image. + images._setArtistImageFromTrack(track_uri, img_element, mopidy, size) + } + }) + } + }, + + /* Set artist image using the supplied Mopidy track URI. */ + _setArtistImageFromTrack: function (uri, img_element, mopidy, size) { + mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) { + var uri = Object.keys(resultDict)[0] + var track = resultDict[uri][0] + var artist = images._getArtistInfo(track) + + if (artist.mbid) { + images._setLastFmArtistImage(artist.mbid, img_element, size) + } else { + // Look up unique MusicBrainz ID for artist first using the available track information + images._getLastFmArtistInfo(track).then(function (artist) { + images._setLastFmArtistImage(artist.mbid, img_element, size) + }, function (code, message) { + $(img_element).attr('src', images.DEFAULT_ARTIST_URL) + console.error('Error retrieving artist info from last.fm. (%s: %s)', code, message) + }) + } + }) + }, + + /* Set artist image using the supplied artist MusicBrainz ID. */ + _setLastFmArtistImage: function (mbid, img_element, size) { + if (!mbid) { + // Avoid expensive last.fm call if tag information is missing. + $(img_element).attr('src', images.DEFAULT_ARTIST_URL) + return + } + size = size || 'extralarge' + + images.lastFM.artist.getInfo({mbid: mbid}, {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) + $(img_element).attr('src', data.artist.image[i]['#text'] || images.DEFAULT_ARTIST_URL) + break } } }, error: function (code, message) { - console.error('Error retrieving artist info from last.fm', code, message) + $(img_element).attr('src', images.DEFAULT_ARTIST_URL) + console.error('Error retrieving artist info from last.fm. (%s: %s)', code, message) }}) } } - return coverArt + return images })) diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js index a3ccba3..0b752c2 100644 --- a/mopidy_musicbox_webclient/static/js/process_ws.js +++ b/mopidy_musicbox_webclient/static/js/process_ws.js @@ -220,7 +220,7 @@ function processCurrentPlaylist (resultArr) { function processArtistResults (resultArr) { if (!resultArr || (resultArr.length === 0)) { $('#h_artistname').text('Artist not found...') - coverArt.getCover('', '#artistviewimage, #artistpopupimage', 'extralarge', mopidy) + images.setAlbumImage('', '#artistviewimage, #artistpopupimage', mopidy) showLoading(false) return } @@ -229,7 +229,7 @@ function processArtistResults (resultArr) { resultsToTables(resultArr, ARTIST_TABLE, resultArr.uri) var artistname = getArtist(resultArr) $('#h_artistname, #artistpopupname').html(artistname) - getArtistImage(artistname, '#artistviewimage, #artistpopupimage', 'extralarge') + images.setArtistImage(resultArr.uri, resultArr[0].uri, '#artistviewimage, #artistpopupimage', mopidy) showLoading(false) } @@ -239,7 +239,7 @@ function processArtistResults (resultArr) { function processAlbumResults (resultArr) { if (!resultArr || (resultArr.length === 0)) { $('#h_albumname').text('Album not found...') - coverArt.getCover('', '#albumviewcover, #coverpopupimage', 'extralarge', mopidy) + images.setAlbumImage('', '#albumviewcover, #coverpopupimage', mopidy) showLoading(false) return } @@ -252,6 +252,6 @@ function processAlbumResults (resultArr) { $('#h_albumartist').html(artistname) $('#coverpopupalbumname').html(albumname) $('#coverpopupartist').html(artistname) - coverArt.getCover(resultArr[0].uri, '#albumviewcover, #coverpopupimage', 'extralarge') + images.setAlbumImage(resultArr[0].uri, '#albumviewcover, #coverpopupimage', mopidy) showLoading(false) } diff --git a/mopidy_musicbox_webclient/static/js/synced_timer.js b/mopidy_musicbox_webclient/static/js/synced_timer.js index e66b0eb..aaa6c04 100644 --- a/mopidy_musicbox_webclient/static/js/synced_timer.js +++ b/mopidy_musicbox_webclient/static/js/synced_timer.js @@ -189,7 +189,6 @@ } SyncedProgressTimer.prototype.reset = function () { - // this._progressTimer.reset() this.stop() this.set(0, Infinity) diff --git a/tests/js/test_images.js b/tests/js/test_images.js index 4cd6ab8..11af74c 100644 --- a/tests/js/test_images.js +++ b/tests/js/test_images.js @@ -6,173 +6,558 @@ chai.use(require('chai-jquery')) var sinon = require('sinon') -var coverArt = require('../../mopidy_musicbox_webclient/static/js/images.js') +var images = require('../../mopidy_musicbox_webclient/static/js/images.js') -describe('CoverArt', function () { +describe('images', function () { var mopidy - var images - beforeEach(function () { + var img_element + before(function () { mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'})) - images = $('') - $(images).removeAttr('src') + img_element = $('') }) - describe('#getCover()', function () { - it('should use default image if no track URI is provided', function () { - coverArt.getCover('', images, '', mopidy) - expect($(images).prop('src')).to.endWith('images/default_cover.png') + beforeEach(function () { + $(img_element).removeAttr('src') + }) + + describe('#_getArtistInfo()', function () { + it('should get artist info from track', function () { + var track = { + artists: [{name: 'trackArtistMock', musicbrainz_id: 'trackArtistIDMock'}], + album: {artists: [{name: 'albumArtistMock', musicbrainz_id: 'albumArtistIDMock'}]} + } + + var artist = images._getArtistInfo(track) + assert.equal(artist.name, 'trackArtistMock') + assert.equal(artist.mbid, 'trackArtistIDMock') }) - it('should get image from Mopidy, if available', function () { - var getImagesResultMock = {'mock:track:uri': [{uri: 'mockImageUri'}]} - var library = { getImages: function () { return $.when(getImagesResultMock) } } - mopidy.library = library + it('should fall back to using album artist if track artist is not available', function () { + var track = { + album: {artists: [{name: 'albumArtistMock', musicbrainz_id: 'albumArtistIDMock'}]} + } - var getImagesSpy = sinon.spy(mopidy.library, 'getImages') - coverArt.getCover('mock:track:uri', images, '', mopidy) - - assert.isTrue(getImagesSpy.calledOnce) - expect($(images).prop('src')).to.endWith('mockImageUri') + var artist = images._getArtistInfo(track) + assert.equal(artist.name, 'albumArtistMock') + assert.equal(artist.mbid, 'albumArtistIDMock') }) - it('should fall back to retrieving image from deprecated track.album.images', function () { - var getImagesResultMock = {'mock:track:uri': []} - var lookupResultMock = {'mock:track:uri': [{album: {images: ['mockAlbumImageUri']}}]} - var library = { + it('should use any combination of artist info from tracks and albums', function () { + var track = { + artists: [{name: 'trackArtistMock'}], + album: {artists: [{name: 'albumArtistMock', musicbrainz_id: 'albumArtistIDMock'}]} + } + + var artist = images._getArtistInfo(track) + assert.equal(artist.name, 'trackArtistMock') + assert.equal(artist.mbid, 'albumArtistIDMock') + }) + }) + + describe('#_getLastFmArtistInfo', function () { + var getInfoStub + + before(function () { + getInfoStub = sinon.stub(images.lastFM.track, 'getInfo') + getInfoStub.yieldsTo('success', {track: {artist: {mbid: 'mockArtistID', name: 'mockArtistName'}}}) + }) + afterEach(function () { + getInfoStub.reset() + }) + after(function () { + getInfoStub.restore() + }) + + it('should retrieve artist info from last.fm using MusicBrainz ID', function () { + var track = { + musicbrainz_id: 'trackIDMock' + } + + images._getLastFmArtistInfo(track).then(function (artist) { + assert.equal(artist.mbid, 'mockArtistID') + assert.equal(artist.name, 'mockArtistName') + }, function (code, message) { + assert.fail(code, '', 'Async method call did not resolve as expected') + }) + assert(getInfoStub.calledWith({mbid: 'trackIDMock'})) + }) + + it('should retrieve artist info from last.fm using track and artist name', function () { + var track = { + name: 'trackNameMock', + artists: [{name: 'trackArtistMock'}] + } + + images._getLastFmArtistInfo(track).then(function (artist) { + assert.equal(artist.mbid, 'mockArtistID') + assert.equal(artist.name, 'mockArtistName') + }, function (code, message) { + assert.fail(code, '', 'Async method call did not resolve as expected') + }) + assert(getInfoStub.calledWith({track: 'trackNameMock', artist: 'trackArtistMock'})) + }) + + it('should raise error if neither track MusicBrainz ID OR track AND album names are available', function () { + var track = { + name: 'trackNameMock' + } + + images._getLastFmArtistInfo(track).then(function (data) { + assert.fail(data, undefined, 'Async method call with just track name did not reject as expected') + }, function (code, message) { + assert.equal(code, 'none') + assert.equal(message, 'Not enough tag information available for track to make last.fm call.') + }) + + track = { + artists: [{name: 'trackArtistMock'}] + } + + images._getLastFmArtistInfo(track).then(function (data) { + assert.fail(data, undefined, 'Async method call with just artist name did not reject as expected') + }, function (code, message) { + assert.equal(code, 'none') + assert.equal(message, 'Not enough tag information available for track to make last.fm call.') + }) + }) + + it('should re-raise last.fm errors', function () { + var track = { + musicbrainz_id: 'trackIDMock' + } + + getInfoStub.yieldsTo('error', 'code', 'message') + + images._getLastFmArtistInfo(track).then(function (data) { + assert.fail(data, undefined, 'Async method call did not re-raise reject as expected') + }, function (code, message) { + assert.equal(code, 'code') + assert.equal(message, 'message') + }) + }) + }) + + describe('#_getLastFmAlbumInfo', function () { + var getInfoStub + + before(function () { + getInfoStub = sinon.stub(images.lastFM.album, 'getInfo') + getInfoStub.yieldsTo('success', {album: {image: [{size: 'extralarge', '#text': 'mockURL'}]}}) + }) + afterEach(function () { + getInfoStub.reset() + }) + after(function () { + getInfoStub.restore() + }) + + it('should retrieve album info from last.fm using MusicBrainz ID', function () { + var track = { + album: {musicbrainz_id: 'albumIDMock'} + } + + images._getLastFmAlbumInfo(track).then(function (data) { + assert.equal(data.album.image[0].size, 'extralarge') + assert.equal(data.album.image[0]['#text'], 'mockURL') + }, function (code, message) { + assert.fail(code, '', 'Async method call did not resolve as expected') + }) + assert(getInfoStub.calledWith({mbid: 'albumIDMock'})) + }) + + it('should retrieve album info from last.fm using album name and either the track or artist name', function () { + var track = { + album: { + name: 'albumNameMock', + artists: [{name: 'albumArtistMock'}] + }, + artists: [{name: 'trackArtistMock'}] + } + + images._getLastFmAlbumInfo(track).then(function (data) { + assert.equal(data.album.image[0].size, 'extralarge') + assert.equal(data.album.image[0]['#text'], 'mockURL') + }, function (code, message) { + assert.fail(code, '', 'Async method call did not resolve as expected') + }) + assert(getInfoStub.calledWith({artist: 'trackArtistMock', album: 'albumNameMock'})) + }) + + it('should raise error if neither album MusicBrainz ID OR album AND artist names are available', function () { + var track = { + album: {} + } + + images._getLastFmAlbumInfo(track).then(function (data) { + assert.fail(data, undefined, 'Async method call with just track name did not reject as expected') + }, function (code, message) { + assert.equal(code, 'none') + assert.equal(message, 'Not enough tag information available for album to make last.fm call.') + }) + + track = { + artists: [{name: 'trackArtistMock'}] + } + + images._getLastFmAlbumInfo(track).then(function (data) { + assert.fail(data, undefined, 'Async method call with just artist name did not reject as expected') + }, function (code, message) { + assert.equal(code, 'none') + assert.equal(message, 'Not enough tag information available for album to make last.fm call.') + }) + }) + + it('should re-raise last.fm errors', function () { + var track = { + album: {musicbrainz_id: 'albumIDMock'} + } + + getInfoStub.yieldsTo('error', 'code', 'message') + + images._getLastFmAlbumInfo(track).then(function (data) { + assert.fail(data, undefined, 'Async method call did not re-raise reject as expected') + }, function (code, message) { + assert.equal(code, 'code') + assert.equal(message, 'message') + }) + }) + }) + + describe('#setAlbumImage()', function () { + var getImagesResultMock + var lookupResultMock + var library + var getImagesSpy + var setDeprecatedAlbumImageSpy + + before(function () { + library = { getImages: function () { return $.when(getImagesResultMock) }, lookup: function () { return $.when(lookupResultMock) } } mopidy.library = library - var getImagesSpy = sinon.spy(mopidy.library, 'getImages') - var getCoverFromAlbumSpy = sinon.spy(coverArt, 'getCoverFromAlbum') + getImagesSpy = sinon.spy(mopidy.library, 'getImages') + setDeprecatedAlbumImageSpy = sinon.spy(images, '_setDeprecatedAlbumImage') + }) + afterEach(function () { + getImagesSpy.reset() + setDeprecatedAlbumImageSpy.reset() + }) - coverArt.getCover('mock:track:uri', images, '', mopidy) + it('should use default image if no track URI is provided', function () { + images.setAlbumImage('', img_element, mopidy) + expect($(img_element).prop('src')).to.endWith(images.DEFAULT_ALBUM_URL) + }) + + it('should get image from Mopidy, if available', function () { + getImagesResultMock = {'mock:track:uri': [{uri: 'mockImageUri'}]} + + images.setAlbumImage('mock:track:uri', img_element, mopidy) assert.isTrue(getImagesSpy.calledOnce) - assert.isTrue(getCoverFromAlbumSpy.calledOnce) + expect($(img_element).prop('src')).to.endWith('mockImageUri') + }) + + it('should fall back to retrieving image from deprecated track.album.images', function () { + getImagesResultMock = {'mock:track:uri': []} + lookupResultMock = {'mock:track:uri': [{album: {images: ['mockAlbumImageUri']}}]} + + images.setAlbumImage('mock:track:uri', img_element, mopidy) + + assert.isTrue(getImagesSpy.calledOnce) + assert.isTrue(setDeprecatedAlbumImageSpy.calledOnce) + }) + + it('should default to retrieving "extralarge" album image', function () { + getImagesResultMock = {'mock:track:uri': []} + lookupResultMock = {'mock:track:uri': [{album: {images: ['mockAlbumImageUri']}}]} + + images.setAlbumImage('mock:track:uri', img_element, mopidy) + + expect(setDeprecatedAlbumImageSpy.args[0]).to.include('extralarge') }) }) - describe('#getCoverFromAlbum()', function () { - it('should use default image if no track URI is provided', function () { - coverArt.getCoverFromAlbum('', images, '', mopidy) - expect($(images).prop('src')).to.endWith('images/default_cover.png') - }) + describe('#_setDeprecatedAlbumImage()', function () { + var lookupResultMock + var library - it('should get image from Mopidy track.album.images, if available', function () { - var lookupResultMock = {'mock:track:uri': [{album: {images: ['mockAlbumImageUri']}}]} - var library = { + before(function () { + library = { lookup: function () { return $.when(lookupResultMock) } } mopidy.library = library + }) + + it('should use default image if no track URI is provided', function () { + images._setDeprecatedAlbumImage('', img_element, mopidy) + expect($(img_element).prop('src')).to.endWith(images.DEFAULT_ALBUM_URL) + }) + + it('should get image from Mopidy track.album.img_element, if available', function () { + lookupResultMock = {'mock:track:uri': [{album: {images: ['mockAlbumImageUri']}}]} var lookupSpy = sinon.spy(mopidy.library, 'lookup') - coverArt.getCoverFromAlbum('mock:track:uri', images, '', mopidy) + images._setDeprecatedAlbumImage('mock:track:uri', img_element, mopidy) assert.isTrue(lookupSpy.calledOnce) - expect($(images).prop('src')).to.endWith('mockAlbumImageUri') + expect($(img_element).prop('src')).to.endWith('mockAlbumImageUri') + lookupSpy.restore() }) it('should use default image if track.album or track.artist is not available', function () { - var lookupResultMock = {'mock:track:uri': []} - var library = { - lookup: function () { return $.when(lookupResultMock) } - } - mopidy.library = library + lookupResultMock = {'mock:track:uri': [{}]} var lookupSpy = sinon.spy(mopidy.library, 'lookup') - coverArt.getCoverFromAlbum('mock:track:uri', images, '', mopidy) + images._setDeprecatedAlbumImage('mock:track:uri', img_element, mopidy) assert.isTrue(lookupSpy.calledOnce) - expect($(images).prop('src')).to.endWith('images/default_cover.png') + expect($(img_element).prop('src')).to.endWith(images.DEFAULT_ALBUM_URL) + lookupSpy.restore() }) it('should fall back to retrieving image from last.fm if none provided by Mopidy', function () { - var lookupResultMock = {'mock:track:uri': [{album: {images: []}}]} - var library = { - lookup: function () { return $.when(lookupResultMock) } - } - mopidy.library = library + lookupResultMock = {'mock:track:uri': [{album: {images: []}}]} - var getCoverFromLastFmStub = sinon.stub(coverArt, 'getCoverFromLastFm') - coverArt.getCoverFromAlbum('mock:track:uri', images, '', mopidy) + var setLastFmAlbumImageStub = sinon.stub(images, '_setLastFmAlbumImage') + images._setDeprecatedAlbumImage('mock:track:uri', img_element, mopidy) - assert.isTrue(getCoverFromLastFmStub.calledOnce) - getCoverFromLastFmStub.restore() + assert.isTrue(setLastFmAlbumImageStub.calledOnce) + setLastFmAlbumImageStub.restore() + }) + + it('should default to retrieving "extralarge" album image', function () { + lookupResultMock = {'mock:track:uri': [{album: {}}]} + + var setLastFmAlbumImageStub = sinon.stub(images, '_setLastFmAlbumImage') + images._setDeprecatedAlbumImage('mock:track:uri', img_element, mopidy) + + expect(setLastFmAlbumImageStub.args[0]).to.include('extralarge') + setLastFmAlbumImageStub.restore() }) }) - describe('#getCoverFromLastFm()', function () { - it('should use default image if no track is provided', function () { - coverArt.getCoverFromLastFm(undefined, images, '') - expect($(images).prop('src')).to.endWith('images/default_cover.png') + describe('#_setLastFmAlbumImage()', function () { + var getInfoResultMock + var getInfoStub + + before(function () { + getInfoStub = sinon.stub(images, '_getLastFmAlbumInfo') + }) + beforeEach(function () { + getInfoStub.reset() + }) + after(function () { + getInfoStub.restore() }) - it('should fall back to using track artist if album artist is not available', function () { - var track = {artists: [{name: 'artistMock'}]} - var getInfoResultMock = {album: {image: []}} + it('should use default image if track album or track artists are not available', function () { + images._setLastFmAlbumImage({}, img_element) + expect($(img_element).prop('src')).to.endWith(images.DEFAULT_ALBUM_URL) + }) + + it('should use correct size for setting album image', function () { + var track = {album: {name: 'albumMock', artists: [{name: 'artistMock'}]}} + getInfoResultMock = {album: {image: [ + {'#text': 'mockAlbumSmallImageUri', size: 'small'}, + {'#text': 'mockAlbumMedImageUri', size: 'medium'}, + {'#text': 'mockAlbumLargeImageUri', size: 'large'} + ]}} - var getInfoStub = sinon.stub(coverArt.lastfm.album, 'getInfo') getInfoStub.returns($.when(getInfoResultMock)) - coverArt.getCoverFromLastFm(track, images, '') - var args = getInfoStub.args - assert.equal(args[0][0].artist, 'artistMock') - getInfoStub.restore() + images._setLastFmAlbumImage(track, img_element, 'medium') + expect($(img_element).prop('src')).to.endWith('mockAlbumMedImageUri') }) - it('should get album info from last.fm', function () { - var track = {album: {artists: [{name: 'albumMock'}]}} - var getInfoResultMock = {album: {image: [{'#text': 'mockAlbumImageUri', size: 'small'}]}} + it('should default to "extralarge" if no image size is specified', function () { + var track = {album: {name: 'albumMock', artists: [{name: 'artistMock'}]}} + getInfoResultMock = {album: {image: [ + {'#text': 'mockAlbumSmallImageUri', size: 'small'}, + {'#text': 'mockAlbumMedImageUri', size: 'medium'}, + {'#text': 'mockAlbumXLargeImageUri', size: 'extralarge'}, + {'#text': 'mockAlbumLargeImageUri', size: 'large'} + ]}} - var getInfoStub = sinon.stub(coverArt.lastfm.album, 'getInfo') - getInfoStub.yieldsTo('success', getInfoResultMock) + getInfoStub.returns($.when(getInfoResultMock)) - coverArt.getCoverFromLastFm(track, images, 'small') - expect($(images).prop('src')).to.endWith('mockAlbumImageUri') - getInfoStub.restore() + images._setLastFmAlbumImage(track, img_element) + expect($(img_element).prop('src')).to.endWith('mockAlbumXLargeImageUri') }) - it('should log errors', function () { - var track = {album: {artists: [{name: 'albumMock'}]}} - var getInfoStub = sinon.stub(coverArt.lastfm.album, 'getInfo') - getInfoStub.yieldsTo('error', 'code', 'message') + it('should log last.fm errors', function () { + var track = {album: {name: 'albumMock', artists: [{name: 'artistMock'}]}} + + getInfoStub.returns($.Deferred().reject('code', 'message')) var consoleStub = sinon.stub(console, 'error') - coverArt.getCoverFromLastFm(track, images, '') + images._setLastFmAlbumImage(track, img_element) - assert.isTrue(consoleStub.calledOnce) - getInfoStub.restore() + assert.isTrue(consoleStub.calledWith('Error getting album info from last.fm (%s: %s)', 'code', 'message')) consoleStub.restore() }) }) - describe('#getArtistImage()', function () { - it('should use default image if no artist is provided', function () { - coverArt.getArtistImage('', images, '') - expect($(images).prop('src')).to.endWith('images/user_24x32.png') + describe('#setArtistImage()', function () { + var getImagesResultMock + var library + var getImagesSpy + + before(function () { + library = { + getImages: function () { return $.when(getImagesResultMock) } + } + mopidy.library = library + getImagesSpy = sinon.spy(mopidy.library, 'getImages') + }) + afterEach(function () { + getImagesSpy.reset() + }) + after(function () { + getImagesSpy.restore() }) - it('should get artist info from last.fm', function () { - var getInfoResultMock = {artist: {image: [{'#text': 'mockArtistImageUri', size: 'small'}]}} + it('should use default image if no artist URI is provided', function () { + images.setArtistImage('', '', img_element, mopidy) + expect($(img_element).prop('src')).to.endWith(images.DEFAULT_ARTIST_URL) + }) - var getInfoStub = sinon.stub(coverArt.lastfm.artist, 'getInfo') - getInfoStub.yieldsTo('success', getInfoResultMock) + it('should get artist image from Mopidy, if available', function () { + getImagesResultMock = {'mock:artist:uri': [{uri: 'mockImageUri'}]} - coverArt.getArtistImage('mockArtist', images, 'small') - expect($(images).prop('src')).to.endWith('mockArtistImageUri') + var setArtistImageFromTrackStub = sinon.stub(images, '_setArtistImageFromTrack') + images.setArtistImage('mock:artist:uri', 'mock:track:uri', img_element, mopidy) + + assert.isTrue(getImagesSpy.calledOnce) + expect($(img_element).prop('src')).to.endWith('mockImageUri') + assert.isFalse(setArtistImageFromTrackStub.called) + + setArtistImageFromTrackStub.restore() + }) + + it('should fall back to retrieving artist image from last.fm', function () { + getImagesResultMock = {'mock:artist:uri': []} + + var setArtistImageFromTrackStub = sinon.stub(images, '_setArtistImageFromTrack') + images.setArtistImage('mock:artist:uri', 'mock:track:uri', img_element, mopidy) + + assert.isTrue(getImagesSpy.calledOnce) + assert.isTrue(setArtistImageFromTrackStub.calledOnce) + + setArtistImageFromTrackStub.restore() + }) + }) + + describe('#_setArtistImageFromTrack()', function () { + var lookupResultMock + var library + var getInfoStub + + before(function () { + library = { + lookup: function () { return $.when(lookupResultMock) } + } + mopidy.library = library + getInfoStub = sinon.stub(images, '_getLastFmArtistInfo') + }) + afterEach(function () { + getInfoStub.reset() + }) + after(function () { getInfoStub.restore() }) - it('should log errors', function () { - var getInfoStub = sinon.stub(coverArt.lastfm.artist, 'getInfo') - getInfoStub.yieldsTo('error', 'code', 'message') + it('should set artist image from last.fm using available Mopidy track information', function () { + lookupResultMock = {'mock:track:uri': [{album: {artists: [{name: 'artistMock'}]}}]} + + var getInfoResultMock = {mbid: 'artistIDLookupMock', name: 'artistNameMock'} + getInfoStub.returns($.when(getInfoResultMock)) + var setLastFmArtistImageStub = sinon.stub(images, '_setLastFmArtistImage') + + images._setArtistImageFromTrack('mock:track:uri', img_element, mopidy, 'small') + + assert(getInfoStub.called) + assert(setLastFmArtistImageStub.calledWithMatch('artistIDLookupMock')) + setLastFmArtistImageStub.restore() + }) + + it('should set artist info from last.fm using MusicBrainz ID, if available', function () { + lookupResultMock = {'mock:track:uri': [{album: {artists: [{musicbrainz_id: 'artistIDMock'}]}}]} + + var setLastFmArtistImageStub = sinon.stub(images, '_setLastFmArtistImage') + + images._setArtistImageFromTrack('mock:track:uri', img_element, mopidy, 'small') + + assert(setLastFmArtistImageStub.calledWithMatch('artistIDMock')) + setLastFmArtistImageStub.restore() + }) + + it('should log last.fm errors', function () { + lookupResultMock = {'mock:track:uri': [{album: {artists: [{name: 'artistMock'}]}}]} + + getInfoStub.returns($.Deferred().reject('code', 'message')) var consoleStub = sinon.stub(console, 'error') - coverArt.getArtistImage('mockArtist', images, 'small') - assert.isTrue(consoleStub.calledOnce) + images._setArtistImageFromTrack('mock:track:uri', img_element, mopidy, 'small') + assert.isTrue(consoleStub.calledWith('Error retrieving artist info from last.fm. (%s: %s)', 'code', 'message')) + consoleStub.restore() + }) + }) + + describe('#_setLastFmArtistImage()', function () { + var getInfoResultMock + var getInfoStub + + before(function () { + getInfoStub = sinon.stub(images.lastFM.artist, 'getInfo') + }) + beforeEach(function () { + getInfoStub.reset() + }) + after(function () { getInfoStub.restore() + }) + + it('should use default image if artist MusicBrainz ID is not available', function () { + images._setLastFmArtistImage('', img_element) + expect($(img_element).prop('src')).to.endWith(images.DEFAULT_ARTIST_URL) + }) + + it('should use correct size for setting album image', function () { + getInfoResultMock = {artist: {image: [ + {'#text': 'mockAlbumSmallImageUri', size: 'small'}, + {'#text': 'mockAlbumMedImageUri', size: 'medium'}, + {'#text': 'mockAlbumLargeImageUri', size: 'large'} + ]}} + + getInfoStub.yieldsTo('success', getInfoResultMock) + + images._setLastFmArtistImage('artistIDMock', img_element, 'medium') + expect($(img_element).prop('src')).to.endWith('mockAlbumMedImageUri') + }) + + it('should default to "extralarge" if no image size is specified', function () { + getInfoResultMock = {artist: {image: [ + {'#text': 'mockAlbumSmallImageUri', size: 'small'}, + {'#text': 'mockAlbumMedImageUri', size: 'medium'}, + {'#text': 'mockAlbumXLargeImageUri', size: 'extralarge'}, + {'#text': 'mockAlbumLargeImageUri', size: 'large'} + ]}} + + getInfoStub.yieldsTo('success', getInfoResultMock) + + images._setLastFmArtistImage('artistIDMock', img_element) + expect($(img_element).prop('src')).to.endWith('mockAlbumXLargeImageUri') + }) + + it('should log last.fm errors', function () { + getInfoStub.yieldsTo('error', 'code', 'message') + + var consoleStub = sinon.stub(console, 'error') + images._setLastFmArtistImage('artistIDMock', img_element) + + assert.isTrue(consoleStub.calledWith('Error retrieving artist info from last.fm. (%s: %s)', 'code', 'message')) consoleStub.restore() }) }) From 7daf5a738315b7228ecd5e95315fe255a5bfc115 Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 16 Apr 2016 17:10:52 +0200 Subject: [PATCH 21/38] Modularise controls.js. Fixes 124 Make default click action user-configurable. Fixes #133. Optimise updating of now-playing icons. Fixes #184. --- .eslintrc | 1 + README.rst | 8 + mopidy_musicbox_webclient/__init__.py | 7 + mopidy_musicbox_webclient/ext.conf | 1 + .../static/css/webclient.css | 69 +- mopidy_musicbox_webclient/static/index.html | 109 +- .../static/js/controls.js | 1239 ++++++++--------- .../static/js/functionsvars.js | 39 +- mopidy_musicbox_webclient/static/js/gui.js | 103 +- .../static/js/library.js | 5 +- .../static/js/process_ws.js | 41 +- mopidy_musicbox_webclient/static/mb.appcache | 2 +- mopidy_musicbox_webclient/static/system.html | 4 +- mopidy_musicbox_webclient/web.py | 1 + mopidy_musicbox_webclient/webclient.py | 3 + tests/js/dummy_tracklist.js | 108 ++ tests/js/test_controls.js | 265 ++++ tests/js/test_images.js | 9 +- tests/js/test_synced_timer.js | 8 +- tests/test_extension.py | 2 + tests/test_webclient.py | 3 + 21 files changed, 1221 insertions(+), 806 deletions(-) create mode 100644 tests/js/dummy_tracklist.js create mode 100644 tests/js/test_controls.js diff --git a/.eslintrc b/.eslintrc index dc14e5b..5b838e3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -7,5 +7,6 @@ "indent": [2, 4, {"SwitchCase": 1}], "no-undef": 0, // TODO: Set this to '2' once Javascript has been modularised. "no-unused-vars": 0, // TODO: Set this to '2' once Javascript has been modularised. + "camelcase": 1, } } diff --git a/README.rst b/README.rst index 70de863..00fa03e 100644 --- a/README.rst +++ b/README.rst @@ -79,12 +79,20 @@ v2.3.0 (UNRELEASED) (Addresses: `#130 `_). - Upgrade Media Progress Timer to version 3.0.0. - Now retrieves album cover and artist images using MusicBrainzID, if available. +- New configuration parameter ``on_track_click`` can be used to customize the action that is performed when the + user clicks on a track in a list. Valid options are: ``PLAY_NOW``, ``PLAY_NEXT``, ``ADD_THIS_BOTTOM``, + ``ADD_ALL_BOTTOM``, ``PLAY_ALL`` (default), and ``DYNAMIC`` (repeats last action). + (Addresses: `#133 `_). +- Optimized updating of 'now playing' icons in tracklists. + (Addresses: `#184 `_). **Fixes** - Don't create Mopidy models manually. (Fixes: `#172 `_). - Context menu is now available for all tracks in browse pane. (Fixes: `#126 `_). - last.fm artist image lookups should now always return the correct image for similarly named artists. +- Ensure that browsed tracks are always added to the queue using the track URI rather than the track's position in the folder. + (Fixes: `#124 `_). v2.2.0 (2016-03-01) ------------------- diff --git a/mopidy_musicbox_webclient/__init__.py b/mopidy_musicbox_webclient/__init__.py index 89f4c5d..2bc628e 100644 --- a/mopidy_musicbox_webclient/__init__.py +++ b/mopidy_musicbox_webclient/__init__.py @@ -22,6 +22,13 @@ class Extension(ext.Extension): schema['musicbox'] = config.Boolean(optional=True) schema['websocket_host'] = config.Hostname(optional=True) schema['websocket_port'] = config.Port(optional=True) + schema['on_track_click'] = config.String(optional=True, + choices=['PLAY_NOW', + 'PLAY_NEXT', + 'ADD_THIS_BOTTOM', + 'ADD_ALL_BOTTOM', + 'PLAY_ALL', + 'DYNAMIC']) return schema def setup(self, registry): diff --git a/mopidy_musicbox_webclient/ext.conf b/mopidy_musicbox_webclient/ext.conf index fa7898e..f5d8a17 100644 --- a/mopidy_musicbox_webclient/ext.conf +++ b/mopidy_musicbox_webclient/ext.conf @@ -3,3 +3,4 @@ enabled = true musicbox = false websocket_host = websocket_port = +on_track_click = PLAY_ALL diff --git a/mopidy_musicbox_webclient/static/css/webclient.css b/mopidy_musicbox_webclient/static/css/webclient.css index 7a2ed05..38637c1 100644 --- a/mopidy_musicbox_webclient/static/css/webclient.css +++ b/mopidy_musicbox_webclient/static/css/webclient.css @@ -244,8 +244,8 @@ } .smalldivider { - font-size: 25% !important; - height: 5px !important; + font-size: 10%; + height: 2px; background-color: #ddd !important; } @@ -342,6 +342,11 @@ text-decoration: none; } +.popupArtistLi, +.popupAlbumLi { + display: none +} + .popupArtistName, .popupTrackName, .popupAlbumName, @@ -361,7 +366,7 @@ margin-top: 10px; } -#controlspopupimage, +#albumCoverImg, #coverpopupimage, #artistpopupimage { display: block; @@ -372,16 +377,56 @@ max-height: 90%; } -#popupTracksLv li, -#popupQueueLv li, -#popupBrowseLv li { - border-bottom: 1px solid #aaa; +/* Override to make buttons more visible in popups.*/ +#popupTracks .ui-btn-up-c, +#popupQueue .ui-btn-up-c { + background: white; } -#popupTracksLv, -#popupQueueLv, -#popupBrowseLv li { - border: 1px solid #aaa; +/* Custom icons for popup listviews - see http://demos.jquerymobile.com/1.3.2/#CustomIcons */ +.ui-icon-playAll:after, +.ui-icon-play:after, +.ui-icon-playNext:after, +.ui-icon-add:after, +.ui-icon-addAll:after, +.ui-icon-remove:after { + color: #34495e; + font-family: 'FontAwesome'; +} + +.ui-icon-playAll:after { + content: '\f050'; +} + +.ui-icon-play:after { + content: '\f04b'; +} + +.ui-icon-playNext:after { + content: '\f149'; +} + +.ui-icon-add:after { + content: '\f196'; +} + +.ui-icon-addAll:after { + content: '\f0fe'; +} + +.ui-icon-remove:after { + content: '\f00d'; +} + +.ui-icon-playAll, +.ui-icon-play, +.ui-icon-playNext, +.ui-icon-add, +.ui-icon-addAll, +.ui-icon-remove { + background-color: unset; + background-image: none; + font-weight: normal; } .popupDialog { @@ -523,7 +568,7 @@ a { overflow: hidden; } - #controlspopupimage { + #albumCoverImg { max-width: 90%; max-height: 90%; margin-bottom: 3px; diff --git a/mopidy_musicbox_webclient/static/index.html b/mopidy_musicbox_webclient/static/index.html index 4c4b832..8620f30 100644 --- a/mopidy_musicbox_webclient/static/index.html +++ b/mopidy_musicbox_webclient/static/index.html @@ -36,7 +36,7 @@ - +
              @@ -87,7 +87,7 @@
            • - + @@ -112,47 +112,28 @@ Album artist
              - - -
              + -
              +
              - -
              @@ -426,18 +407,18 @@

              Play a specific stream/track and optionally save it to your favourites. - - -
              @@ -459,7 +440,7 @@

              - +
            • @@ -467,16 +448,16 @@
              - - - + + +
              - - - - - + + + + +
              diff --git a/mopidy_musicbox_webclient/static/js/controls.js b/mopidy_musicbox_webclient/static/js/controls.js index b1bcc9b..1b2e55e 100644 --- a/mopidy_musicbox_webclient/static/js/controls.js +++ b/mopidy_musicbox_webclient/static/js/controls.js @@ -1,663 +1,644 @@ -/** ********************************* - * play tracks from a browse list * - ***********************************/ -function playBrowsedTracks (action, trackIndex) { - $('#popupBrowse').popup('close') - toast('Loading...') +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory) + } else if (typeof module === 'object' && module.exports) { + module.exports = factory() + } else { + root.controls = factory() + } +}(this, function () { + 'use strict' - if (typeof trackIndex === 'undefined') { - trackIndex = $('#popupBrowse').data('tlid') - } - if (action === PLAY_ALL) { - mopidy.tracklist.clear() - // Default for radio streams is to just add the selected URI. - if (isStreamUri(browseTracks[trackIndex].uri)) { - action = PLAY_NOW - } - } - var trackUris = [] - switch (action) { - case PLAY_NOW: - case PLAY_NEXT: - case ADD_THIS_BOTTOM: - trackUris.push(browseTracks[trackIndex].uri) - break - case PLAY_ALL: - case ADD_ALL_BOTTOM: - trackUris = getUris(browseTracks) - break - default: - break - } - var maybePlay = function (tlTracks) { - if (action === PLAY_NOW || action === PLAY_ALL) { - var playIndex = (action === PLAY_ALL) ? trackIndex : 0 - mopidy.playback.play({'tl_track': tlTracks[playIndex]}) - } - } + var controls = { - switch (action) { - case PLAY_NOW: - case PLAY_NEXT: - mopidy.tracklist.index().then(function (currentIndex) { - mopidy.tracklist.add({'at_position': currentIndex + 1, 'uris': trackUris}).then(maybePlay) - }) - break - case ADD_THIS_BOTTOM: - case ADD_ALL_BOTTOM: - case PLAY_ALL: - mopidy.tracklist.add({'uris': trackUris}).then(maybePlay) - break - default: - break - } - return false -} + /** + * 'onClick' handler for tracks that are rendered in a list. + * + * Adds tracks to current tracklist and starts playback if necessary. + * + * @param {string} action - The action to perform. Valid actions are: + * PLAY_NOW: add the track at 'trackIndex' and start playback. + * PLAY_NEXT: insert track after currently playing track. + * ADD_THIS_BOTTOM: add track to bottom of tracklist. + * ADD_ALL_BOTTOM: add all tracks in in the list to bottom of + * tracklist. + * PLAY_ALL: clear tracklist and start playback of the track + * with URI provided in 'trackUri'. + * @param {object} mopidy - The Mopidy.js object that should be used to communicate with the + * Mopidy server. + * @param {string} trackUri - (Optional) The URI of the specific track that the action should + * be performed on. If no URI is provided then the 'data' attribute + * of the popup DIV is assumed to contain the track URI. + * @param {string} playlistUri - (Optional) The URI of the playlist containing the tracks + * to be played. If no URI is provided then the 'list' attribute + * of the popup DIV is assumed to contain the playlist URI. + */ -/** ******************************* - * play an uri from a tracklist * - *********************************/ -function playTrack (action) { - var hash = document.location.hash.split('?') - var divid = hash[0].substr(1) + playTracks: function (action, mopidy, trackUri, playlistUri) { + $('#popupTracks').popup('close') + toast('Loading...') - // Search page default click behaviour adds and plays selected track only. - if (action === PLAY_NOW && divid === 'search') { - action === PLAY_NOW_SEARCH - } - - $('#popupTracks').popup('close') - $('#controlspopup').popup('close') - toast('Loading...') - - playlisturi = $('#popupTracks').data('list') - uri = $('#popupTracks').data('track') - - var trackUris = getTracksFromUri(playlisturi) - // find track that was selected - for (var selected = 0; selected < trackUris.length; selected++) { - if (trackUris[selected] === uri) { - break - } - } - switch (action) { - case ADD_THIS_BOTTOM: - case PLAY_NEXT: - case PLAY_NOW_SEARCH: - trackUris = [trackUris[selected]] - selected = 0 - } - switch (action) { - case PLAY_NOW: - case PLAY_NOW_SEARCH: - mopidy.tracklist.clear().then( - mopidy.tracklist.add({'uris': trackUris}).then( - function (tlTracks) { - mopidy.playback.play({'tl_track': tlTracks[selected]}) - } - ) - ) - break - case PLAY_NEXT: - mopidy.tracklist.index().then(function (currentIndex) { - mopidy.tracklist.add({'at_position': currentIndex + 1, 'uris': trackUris}) - }) - break - case ADD_THIS_BOTTOM: - case ADD_ALL_BOTTOM: - mopidy.tracklist.add({'uris': trackUris}) - break - } - return false -} - -/** * - * Plays a Track given by an URI from the given playlist URI. - * @param track_uri, playlist_uri - * @returns {boolean} - */ -function playTrackByUri (track_uri, playlist_uri) { - function findAndPlayTrack (tlTracks) { - if (tlTracks.length > 0) { - // Find track that was selected - for (var selected = 0; selected < tlTracks.length; selected++) { - if (tlTracks[selected].track.uri === track_uri) { - mopidy.playback.play({'tl_track': tlTracks[selected]}) - return - } + trackUri = trackUri || $('#popupTracks').data('track') + if (typeof trackUri === 'undefined') { + throw new Error('No track URI provided for playback.') } - } - console.error('Failed to find and play selected track ', track_uri) - return - } - - // Stop directly, for user feedback - mopidy.tracklist.clear() - - // this is deprecated, remove when popuptracks is removed completly - $('#popupTracks').popup('close') - $('#controlspopup').popup('close') - // end of deprecated - - toast('Loading...') - - mopidy.tracklist.add({'uris': [playlist_uri]}).then(function (tlTracks) { - // Can fail for all sorts of reasons. If so, just add individually. - if (tlTracks.length === 0) { - var trackUris = getTracksFromUri(playlist_uri, false) - mopidy.tracklist.add({'uris': trackUris}).then(findAndPlayTrack) - } else { - findAndPlayTrack(tlTracks) - } - }) - return false -} - -/** ****************************************************** - * play an uri from the queue - *********************************************************/ - -/** * - * Plays a Track from a Playlist. - * @param uri - * @param tlid - * @returns {boolean} - */ -function playTrackQueueByTlid (uri, tlid) { - // stop directly, for user feedback - mopidy.playback.stop() - $('#popupQueue').popup('close') - toast('Loading...') - - tlid = parseInt(tlid) - mopidy.tracklist.filter({ - 'tlid': [tlid] - }).then( - function (tlTracks) { - if (tlTracks.length > 0) { - mopidy.playback.play({'tl_track': tlTracks[0]}) - return + playlistUri = playlistUri || $('#popupTracks').data('list') + if (typeof playlistUri === 'undefined') { + throw new Error('No playlist URI provided for playback.') } - console.log('Failed to play selected track ', tlid) - } - ) - return false -} -/** * - * @deprecated - * @returns {boolean} - */ -function playTrackQueue () { - uri = $('#popupQueue').data('track') - tlid = $('#popupQueue').data('tlid') - return playTrackQueueByTlid(uri, tlid) -} + action = controls.getAction(action) -/** ********************************* - * remove a track from the queue * - ***********************************/ -function removeTrack () { - $('#popupQueue').popup('close') - toast('Deleting...') + if (action === PLAY_ALL) { + mopidy.tracklist.clear() + } - tlid = parseInt($('#popupQueue').data('tlid')) - console.log(tlid) - mopidy.tracklist.remove({'tlid': [tlid]}) -} - -function clearQueue () { - mopidy.tracklist.clear().then( - resetSong() - ) - return false -} - -function savePressed (key) { - if (key === 13) { - saveQueue() - return false - } - return true -} - -function showSavePopup () { - mopidy.tracklist.getTracks().then(function (tracks) { - if (tracks.length > 0) { - $('#saveinput').val('') - $('#popupSave').popup('open') - } - }) -} - -function saveQueue () { - mopidy.tracklist.getTracks().then(function (tracks) { - var playlistName = $('#saveinput').val().trim() - if (playlistName !== null && playlistName !== '') { - getPlaylistByName(playlistName, 'm3u', false).then(function (exists) { - if (exists) { - $('#popupSave').popup('close') - $('#popupOverwrite').popup('open') - $('#overwriteConfirmBtn').click(function () { - initSave(playlistName, tracks) + var trackUris = controls._getTrackURIsForAction(action, trackUri, playlistUri) + // Add the tracks and start playback if necessary. + switch (action) { + case PLAY_NOW: + case PLAY_NEXT: + // Find track that is currently playing. + mopidy.tracklist.index().then(function (currentIndex) { + // Add browsed track just below it. + mopidy.tracklist.add({at_position: currentIndex + 1, uris: trackUris}).then(function (tlTracks) { + if (action === PLAY_NOW) { // Start playback immediately. + mopidy.playback.stop().then(function () { + mopidy.playback.play({tlid: tlTracks[0].tlid}) + }) + } + }) }) - } else { - initSave(playlistName, tracks) + break + case ADD_THIS_BOTTOM: + case ADD_ALL_BOTTOM: + case PLAY_ALL: + mopidy.tracklist.add({uris: trackUris}).then(function (tlTracks) { + if (action === PLAY_ALL) { // Start playback of selected track immediately. + mopidy.tracklist.filter({uri: [trackUri]}).then(function (tlTracks) { + mopidy.playback.stop().then(function () { + mopidy.playback.play({tlid: tlTracks[0].tlid}) + }) + }) + } + }) + break + default: + throw new Error('Unexpected tracklist action identifier: ' + action) + } + + if (window[$(document.body).data('on-track-click')] === DYNAMIC) { + // Save last 'action' - will become default for future 'onClick' events + var previousAction = $.cookie('onTrackClick') + if (typeof previousAction === 'undefined' || action !== previousAction) { + $.cookie('onTrackClick', action, { expires: 365 }) + updatePlayIcons('', '', controls.getIconForAction(action)) + } + } + }, + + /* Getter function for 'action' variable. Also checks config settings and cookies if required. */ + getAction: function (action) { + if (typeof action === 'undefined' || action.length === 0) { // Action parameter not provided, use defaults + action = window[$(document.body).data('on-track-click')] + } + if (action === DYNAMIC) { // Re-use last action stored in cookie. + action = $.cookie('onTrackClick') + if (typeof action === 'undefined') { + action = PLAY_ALL // Backwards-compatible default value. + } + } + return action + }, + + /* Retrieves the Font Awesome character for the given action. */ + getIconForAction: function (action) { + action = controls.getAction(action) + + switch (parseInt(action)) { + case PLAY_ALL: + return 'fa fa-fast-forward' + case PLAY_NOW: + return 'fa fa-play' + case PLAY_NEXT: + return 'fa fa-level-down' + case ADD_THIS_BOTTOM: + return 'fa fa-plus-square-o' + case ADD_ALL_BOTTOM: + return 'fa fa-plus-square' + default: + throw new Error('Unkown tracklist action identifier: ' + action) + } + }, + + /* Retrieves the relevant track URIs for the given action. */ + _getTrackURIsForAction: function (action, trackUri, playlistUri) { + var trackUris = [] + // Fill 'trackUris', by determining which tracks should be added. + switch (parseInt(action)) { + case PLAY_NOW: + case PLAY_NEXT: + case ADD_THIS_BOTTOM: + // Process single track + trackUris.push(trackUri) + break + case PLAY_ALL: + case ADD_ALL_BOTTOM: + // Process all tracks in playlist + trackUris = getTracksFromUri(playlistUri, false) + break + default: + throw new Error('Unexpected tracklist action identifier: ' + action) + } + return trackUris + }, + + /** ****************************************************** + * play an uri from the queue + *********************************************************/ + + /** * + * Plays a Track from a Playlist. + * @param tlid + * @returns {boolean} + */ + playQueueTrack: function (tlid) { + // Stop directly, for user feedback + mopidy.playback.stop() + $('#popupQueue').popup('close') + toast('Loading...') + + tlid = tlid || $('#popupQueue').data('tlid') + mopidy.playback.play({'tlid': parseInt(tlid)}) + }, + + /** ********************************* + * remove a track from the queue * + ***********************************/ + removeTrack: function (tlid) { + $('#popupQueue').popup('close') + toast('Deleting...') + + tlid = tlid || $('#popupQueue').data('tlid') + mopidy.tracklist.remove({'tlid': [parseInt(tlid)]}) + }, + + clearQueue: function () { + mopidy.tracklist.clear().then( + resetSong() + ) + return false + }, + + savePressed: function (key) { + if (key === 13) { + controls.saveQueue() + return false + } + return true + }, + + showSavePopup: function () { + mopidy.tracklist.getTracks().then(function (tracks) { + if (tracks.length > 0) { + $('#saveinput').val('') + $('#popupSave').popup('open') } }) - } - }) - return false -} + }, -function initSave (playlistName, tracks) { - $('#popupOverwrite').popup('close') - $('#popupSave').popup('close') - $('#saveinput').val('') - toast('Saving...') - mopidy.playlists.create({'name': playlistName, 'uri_scheme': 'm3u'}).then(function (playlist) { - playlist.tracks = tracks - mopidy.playlists.save({'playlist': playlist}).then() - }) -} + saveQueue: function () { + mopidy.tracklist.getTracks().then(function (tracks) { + var playlistName = $('#saveinput').val().trim() + if (playlistName !== null && playlistName !== '') { + controls.getPlaylistByName(playlistName, 'm3u', false).then(function (exists) { + if (exists) { + $('#popupSave').popup('close') + $('#popupOverwrite').popup('open') + $('#overwriteConfirmBtn').click(function () { + controls.initSave(playlistName, tracks) + }) + } else { + controls.initSave(playlistName, tracks) + } + }) + } + }) + return false + }, -function refreshPlaylists () { - mopidy.playlists.refresh().then(function () { - playlists = {} - $('#playlisttracksdiv').hide() - $('#playlistslistdiv').show() - }) - return false -} + initSave: function (playlistName, tracks) { + $('#popupOverwrite').popup('close') + $('#popupSave').popup('close') + $('#saveinput').val('') + toast('Saving...') + mopidy.playlists.create({'name': playlistName, 'uri_scheme': 'm3u'}).then(function (playlist) { + playlist.tracks = tracks + mopidy.playlists.save({'playlist': playlist}).then() + }) + }, -/** *********** - * Buttons * - *************/ + refreshPlaylists: function () { + mopidy.playlists.refresh().then(function () { + playlists = {} + $('#playlisttracksdiv').hide() + $('#playlistslistdiv').show() + }) + return false + }, -function doShuffle () { - mopidy.playback.stop() - mopidy.tracklist.shuffle() - mopidy.playback.play() -} + /** *********** + * Buttons * + *************/ -/* Toggle state of play button */ -function setPlayState (nwplay) { - if (nwplay) { - $('#btplayNowPlaying >i').removeClass('fa-play').addClass('fa-pause') - $('#btplayNowPlaying').attr('title', 'Pause') - $('#btplay >i').removeClass('fa-play').addClass('fa-pause') - $('#btplay').attr('title', 'Pause') - mopidy.playback.getTimePosition().then(processCurrentposition, console.error) - syncedProgressTimer.start() - } else { - $('#btplayNowPlaying >i').removeClass('fa-pause').addClass('fa-play') - $('#btplayNowPlaying').attr('title', 'Play') - $('#btplay >i').removeClass('fa-pause').addClass('fa-play') - $('#btplay').attr('title', 'Play') - syncedProgressTimer.stop() - } - play = nwplay -} - -// play or pause -function doPlay () { - toast('Please wait...', 250) - if (!play) { - mopidy.playback.play() - } else { - if (isStreamUri(songdata.track.uri)) { + doShuffle: function () { mopidy.playback.stop() - } else { - mopidy.playback.pause() - } - } - setPlayState(!play) -} + mopidy.tracklist.shuffle() + mopidy.playback.play() + }, -function doPrevious () { - toast('Playing previous track...') - mopidy.playback.previous() -} - -function doNext () { - toast('Playing next track...') - mopidy.playback.next() -} - -function backbt () { - history.back() - return false -} - -/** *********** - * Options * - *************/ -function setTracklistOption (name, new_value) { - if (!new_value) { - $('#' + name + 'bt').attr('style', 'color:#2489ce') - } else { - $('#' + name + 'bt').attr('style', 'color:#66DD33') - } - return new_value -} - -function setRepeat (nwrepeat) { - if (repeat !== nwrepeat) { - repeat = setTracklistOption('repeat', nwrepeat) - } -} - -function setRandom (nwrandom) { - if (random !== nwrandom) { - random = setTracklistOption('random', nwrandom) - } -} - -function setConsume (nwconsume) { - if (consume !== nwconsume) { - consume = setTracklistOption('consume', nwconsume) - } -} - -function setSingle (nwsingle) { - if (single !== nwsingle) { - single = setTracklistOption('single', nwsingle) - } -} - -function doRandom () { - mopidy.tracklist.setRandom({'value': !random}).then() -} - -function doRepeat () { - mopidy.tracklist.setRepeat({'value': !repeat}).then() -} - -function doConsume () { - mopidy.tracklist.setConsume({'value': !consume}).then() -} - -function doSingle () { - mopidy.tracklist.setSingle({'value': !single}).then() -} - -/** ********************************************* - * Track Slider * - * Use a timer to prevent looping of commands * - ***********************************************/ -function doSeekPos (value) { - if (!positionChanging) { - positionChanging = value - mopidy.playback.seek({'time_position': Math.round(value)}).then(function () { - positionChanging = null - }) - } -} - -function setPosition (pos) { - if (!positionChanging && $('#trackslider').val() !== pos) { - syncedProgressTimer.set(pos) - } -} - -/** ********************************************* - * Volume slider * - * Use a timer to prevent looping of commands * - ***********************************************/ - -function setVolume (value) { - if (!volumeChanging && !volumeSliding && $('#volumeslider').val() !== value) { - $('#volumeslider').off('change') - $('#volumeslider').val(value).slider('refresh') - $('#volumeslider').on('change', function () { doVolume($(this).val()) }) - } -} - -function doVolume (value) { - if (!volumeChanging) { - volumeChanging = value - mopidy.playback.setVolume({'volume': parseInt(volumeChanging)}).then(function () { - volumeChanging = null - }) - } -} - -function setMute (nwmute) { - if (mute !== nwmute) { - mute = nwmute - if (mute) { - $('#mutebt').attr('class', 'fa fa-volume-off') - } else { - $('#mutebt').attr('class', 'fa fa-volume-up') - } - } -} - -function doMute () { - mopidy.mixer.setMute({'mute': !mute}) -} - -/** ********** - * Stream * - ************/ -function streamPressed (key) { - if (key === 13) { - playStreamUri() - return false - } - return true -} - -function playStreamUri (uri) { - // value of name is based on the passing of an uri as a parameter or not - var nwuri = uri || $('#streamuriinput').val().trim() - var service = $('#selectstreamservice').val() - if (!uri && service) { - nwuri = service + ':' + nwuri - } - if (isServiceUri(nwuri) || isStreamUri(nwuri) || validUri(nwuri)) { - toast('Playing...') - // stop directly, for user feedback - mopidy.playback.stop() - // hide ios/android keyboard - document.activeElement.blur() - clearQueue() - $('input').blur() - mopidy.tracklist.add({'uris': [nwuri]}) - mopidy.playback.play() - } else { - toast('No valid url!') - } - return false -} - -function getCurrentlyPlaying () { - $('#streamuriinput').val(songdata.track.uri) - var name = songdata.track.name - if (songdata.track.artists) { - var artistStr = artistsToString(songdata.track.artists) - if (artistStr) { - name = artistStr + ' - ' + name - } - } - $('#streamnameinput').val(name) - return true -} - -function getUriSchemes () { - uriSchemes = {} - return mopidy.getUriSchemes().then(function (schemes) { - for (var i = 0; i < schemes.length; i++) { - uriSchemes[schemes[i].toLowerCase()] = true - } - }) -} - -function getPlaylistByName (name, scheme, create) { - var uri_scheme = scheme || '' - var uri = '' - if (uri_scheme && !uriSchemes[uri_scheme]) { - return Mopidy.when(false) - } - return mopidy.playlists.asList().catch(console.error.bind(console)).then(function (plists) { - for (var i = 0; i < plists.length; i++) { - if ((plists[i].name === name) && (uri_scheme === '' || getScheme(plists[i].uri) === uri_scheme)) { - return plists[i] - } - } - if (create) { - return mopidy.playlists.create({'name': name, 'uri_scheme': uri_scheme}).done(function (plist) { - console.log("Created playlist '%s'", plist.name) - return plist - }) - } - console.log("Can't find playist '%s", name) - return Mopidy.when(false) - }) -} - -function getPlaylistFull (uri) { - return mopidy.playlists.lookup({'uri': uri}).then(function (pl) { - playlists[uri] = pl - return pl - }) -} - -function getFavourites () { - return getPlaylistByName(STREAMS_PLAYLIST_NAME, - STREAMS_PLAYLIST_SCHEME, - true).then(function (playlist) { - if (playlist) { - return getPlaylistFull(playlist.uri) - } - return Mopidy.when(false) - }) -} - -function addToFavourites (newTracks) { - getFavourites().catch(console.error.bind(console)).then(function (favourites) { - if (favourites) { - if (favourites.tracks) { - Array.prototype.push.apply(favourites.tracks, newTracks) + /* Toggle state of play button */ + setPlayState: function (nwplay) { + if (nwplay) { + $('#btplayNowPlaying >i').removeClass('fa-play').addClass('fa-pause') + $('#btplayNowPlaying').attr('title', 'Pause') + $('#btplay >i').removeClass('fa-play').addClass('fa-pause') + $('#btplay').attr('title', 'Pause') + mopidy.playback.getTimePosition().then(processCurrentposition, console.error) + syncedProgressTimer.start() } else { - favourites.tracks = newTracks + $('#btplayNowPlaying >i').removeClass('fa-pause').addClass('fa-play') + $('#btplayNowPlaying').attr('title', 'Play') + $('#btplay >i').removeClass('fa-pause').addClass('fa-play') + $('#btplay').attr('title', 'Play') + syncedProgressTimer.stop() } - mopidy.playlists.save({'playlist': favourites}).then(function (s) { - showFavourites() - }) - } - }) -} + play = nwplay + }, -function addFavourite (uri, name) { - uri = uri || $('#streamuriinput').val().trim() - name = name || $('#streamnameinput').val().trim() - mopidy.library.lookup({'uris': [uri]}).then(function (results) { - var newTracks = results[uri] - if (newTracks.length === 1) { - // TODO: Supporting adding an entire playlist? - if (name) { - newTracks[0].name = name // User overrides name. - } - addToFavourites(newTracks) - } else { - if (newTracks.length === 0) { - console.log('No tracks to add') + // play or pause + doPlay: function () { + toast('Please wait...', 250) + if (!play) { + mopidy.playback.play() } else { - console.log('Too many tracks (%d) to add', tracks.length) + if (isStreamUri(songdata.track.uri)) { + mopidy.playback.stop() + } else { + mopidy.playback.pause() + } } - } - }) -} + controls.setPlayState(!play) + }, -function deleteFavourite (index) { - getFavourites().then(function (favourites) { - if (favourites && favourites.tracks && index < favourites.tracks.length) { - var name = favourites.tracks[index].name - if (confirm("Are you sure you want to remove '" + name + "'?")) { - favourites.tracks.splice(index, 1) - mopidy.playlists.save({'playlist': favourites}).then(function (s) { - showFavourites() + doPrevious: function () { + toast('Playing previous track...') + mopidy.playback.previous() + }, + + doNext: function () { + toast('Playing next track...') + mopidy.playback.next() + }, + + backbt: function () { + history.back() + return false + }, + + /** *********** + * Options * + *************/ + setTracklistOption: function (name, new_value) { + if (!new_value) { + $('#' + name + 'bt').attr('style', 'color:#2489ce') + } else { + $('#' + name + 'bt').attr('style', 'color:#66DD33') + } + return new_value + }, + + setRepeat: function (nwrepeat) { + if (repeat !== nwrepeat) { + repeat = controls.setTracklistOption('repeat', nwrepeat) + } + }, + + setRandom: function (nwrandom) { + if (random !== nwrandom) { + random = controls.setTracklistOption('random', nwrandom) + } + }, + + setConsume: function (nwconsume) { + if (consume !== nwconsume) { + consume = controls.setTracklistOption('consume', nwconsume) + } + }, + + setSingle: function (nwsingle) { + if (single !== nwsingle) { + single = controls.setTracklistOption('single', nwsingle) + } + }, + + doRandom: function () { + mopidy.tracklist.setRandom({'value': !random}).then() + }, + + doRepeat: function () { + mopidy.tracklist.setRepeat({'value': !repeat}).then() + }, + + doConsume: function () { + mopidy.tracklist.setConsume({'value': !consume}).then() + }, + + doSingle: function () { + mopidy.tracklist.setSingle({'value': !single}).then() + }, + + /** ********************************************* + * Track Slider * + * Use a timer to prevent looping of commands * + ***********************************************/ + doSeekPos: function (value) { + if (!positionChanging) { + positionChanging = value + mopidy.playback.seek({'time_position': Math.round(value)}).then(function () { + positionChanging = null }) } - } - }) -} + }, -function showFavourites () { - $('#streamuristable').empty() - getFavourites().then(function (favourites) { - if (!favourites) { - return - } - var tmp = '' - - $.cookie.json = true - if ($.cookie('streamUris')) { - tmp = '' - } - if (favourites.tracks) { - var child = '' - for (var i = 0; i < favourites.tracks.length; i++) { - child = '
            •  ' + - '' + - ' ' - child += '

              ' + favourites.tracks[i].name + '

            • ' - tmp += child + setPosition: function (pos) { + if (!positionChanging && $('#trackslider').val() !== pos) { + syncedProgressTimer.set(pos) } - } - $('#streamuristable').html(tmp) - }) -} + }, -// TODO: Remove this upgrade path in next major release. -function upgradeStreamUrisToFavourites () { - toast('Converting streamUris...') - $.cookie.json = true - var streamUris = $.cookie('streamUris') // Read the cookie. - if (streamUris) { - var uris = [] // Prepare a list of uris to lookup. - for (var key in streamUris) { - var rs = streamUris[key] - if (rs) { - uris.push(rs[1]) + /** ********************************************* + * Volume slider * + * Use a timer to prevent looping of commands * + ***********************************************/ + + setVolume: function (value) { + if (!volumeChanging && !volumeSliding && $('#volumeslider').val() !== value) { + $('#volumeslider').off('change') + $('#volumeslider').val(value).slider('refresh') + $('#volumeslider').on('change', function () { controls.doVolume($(this).val()) }) } - } - mopidy.library.lookup({'uris': uris}).then(function (results) { - var tracks = [] // Prepare a list of tracks to add. - for (var key in streamUris) { - var rs = streamUris[key] - if (rs) { - var track = results[rs[1]][0] - if (track) { - track.name = rs[0] || track.name // Use custom name if provided. - tracks.push(track) - } else { - console.log('Skipping unplayable streamUri ' + rs[1]) - } + }, + + doVolume: function (value) { + if (!volumeChanging) { + volumeChanging = value + mopidy.playback.setVolume({'volume': parseInt(volumeChanging)}).then(function () { + volumeChanging = null + }) + } + }, + + setMute: function (nwmute) { + if (mute !== nwmute) { + mute = nwmute + if (mute) { + $('#mutebt').attr('class', 'fa fa-volume-off') + } else { + $('#mutebt').attr('class', 'fa fa-volume-up') } } - addToFavourites(tracks) - $.cookie('streamUris', null) // Delete the cookie now we're done. - console.log(tracks.length + ' streamUris added to favourites') - }) - } else { - console.log('No streamUris cookie found') + }, + + doMute: function () { + mopidy.mixer.setMute({'mute': !mute}) + }, + + /** ********** + * Stream * + ************/ + streamPressed: function (key) { + if (key === 13) { + controls.playStreamUri() + return false + } + return true + }, + + playStreamUri: function (uri) { + // value of name is based on the passing of an uri as a parameter or not + var nwuri = uri || $('#streamuriinput').val().trim() + var service = $('#selectstreamservice').val() + if (!uri && service) { + nwuri = service + ':' + nwuri + } + if (isServiceUri(nwuri) || isStreamUri(nwuri) || validUri(nwuri)) { + toast('Playing...') + // stop directly, for user feedback + mopidy.playback.stop() + // hide ios/android keyboard + document.activeElement.blur() + controls.clearQueue() + $('input').blur() + mopidy.tracklist.add({'uris': [nwuri]}) + mopidy.playback.play() + } else { + toast('No valid url!') + } + return false + }, + + getCurrentlyPlaying: function () { + $('#streamuriinput').val(songdata.track.uri) + var name = songdata.track.name + if (songdata.track.artists) { + var artistStr = artistsToString(songdata.track.artists) + if (artistStr) { + name = artistStr + ' - ' + name + } + } + $('#streamnameinput').val(name) + return true + }, + + getUriSchemes: function () { + uriSchemes = {} + return mopidy.getUriSchemes().then(function (schemes) { + for (var i = 0; i < schemes.length; i++) { + uriSchemes[schemes[i].toLowerCase()] = true + } + }) + }, + + getPlaylistByName: function (name, scheme, create) { + var uri_scheme = scheme || '' + var uri = '' + if (uri_scheme && !uriSchemes[uri_scheme]) { + return Mopidy.when(false) + } + return mopidy.playlists.asList().catch(console.error.bind(console)).then(function (plists) { + for (var i = 0; i < plists.length; i++) { + if ((plists[i].name === name) && (uri_scheme === '' || getScheme(plists[i].uri) === uri_scheme)) { + return plists[i] + } + } + if (create) { + return mopidy.playlists.create({'name': name, 'uri_scheme': uri_scheme}).done(function (plist) { + console.log("Created playlist '%s'", plist.name) + return plist + }) + } + console.log("Can't find playist '%s", name) + return Mopidy.when(false) + }) + }, + + getPlaylistFull: function (uri) { + return mopidy.playlists.lookup({'uri': uri}).then(function (pl) { + playlists[uri] = pl + return pl + }) + }, + + getFavourites: function () { + return controls.getPlaylistByName(STREAMS_PLAYLIST_NAME, + STREAMS_PLAYLIST_SCHEME, + true).then(function (playlist) { + if (playlist) { + return controls.getPlaylistFull(playlist.uri) + } + return Mopidy.when(false) + }) + }, + + addToFavourites: function (newTracks) { + controls.getFavourites().catch(console.error.bind(console)).then(function (favourites) { + if (favourites) { + if (favourites.tracks) { + Array.prototype.push.apply(favourites.tracks, newTracks) + } else { + favourites.tracks = newTracks + } + mopidy.playlists.save({'playlist': favourites}).then(function (s) { + controls.showFavourites() + }) + } + }) + }, + + addFavourite: function (uri, name) { + uri = uri || $('#streamuriinput').val().trim() + name = name || $('#streamnameinput').val().trim() + mopidy.library.lookup({'uris': [uri]}).then(function (results) { + var newTracks = results[uri] + if (newTracks.length === 1) { + // TODO: Supporting adding an entire playlist? + if (name) { + newTracks[0].name = name // User overrides name. + } + controls.addToFavourites(newTracks) + } else { + if (newTracks.length === 0) { + console.log('No tracks to add') + } else { + console.log('Too many tracks (%d) to add', tracks.length) + } + } + }) + }, + + deleteFavourite: function (index) { + controls.getFavourites().then(function (favourites) { + if (favourites && favourites.tracks && index < favourites.tracks.length) { + var name = favourites.tracks[index].name + if (confirm("Are you sure you want to remove '" + name + "'?")) { + favourites.tracks.splice(index, 1) + mopidy.playlists.save({'playlist': favourites}).then(function (s) { + controls.showFavourites() + }) + } + } + }) + }, + + showFavourites: function () { + $('#streamuristable').empty() + controls.getFavourites().then(function (favourites) { + if (!favourites) { + return + } + var tmp = '' + + $.cookie.json = true + if ($.cookie('streamUris')) { + tmp = '' + } + if (favourites.tracks) { + var child = '' + for (var i = 0; i < favourites.tracks.length; i++) { + child = '
            •  ' + + '' + + ' ' + child += '

              ' + favourites.tracks[i].name + '

            • ' + tmp += child + } + } + $('#streamuristable').html(tmp) + }) + }, + + // TODO: Remove this upgrade path in next major release. + upgradeStreamUrisToFavourites: function () { + toast('Converting streamUris...') + $.cookie.json = true + var streamUris = $.cookie('streamUris') // Read the cookie. + if (streamUris) { + var uris = [] // Prepare a list of uris to lookup. + for (var key in streamUris) { + var rs = streamUris[key] + if (rs) { + uris.push(rs[1]) + } + } + mopidy.library.lookup({'uris': uris}).then(function (results) { + var tracks = [] // Prepare a list of tracks to add. + for (var key in streamUris) { + var rs = streamUris[key] + if (rs) { + var track = results[rs[1]][0] + if (track) { + track.name = rs[0] || track.name // Use custom name if provided. + tracks.push(track) + } else { + console.log('Skipping unplayable streamUri ' + rs[1]) + } + } + } + controls.addToFavourites(tracks) + $.cookie('streamUris', null) // Delete the cookie now we're done. + console.log(tracks.length + ' streamUris added to favourites') + }) + } else { + console.log('No streamUris cookie found') + } + }, + + haltSystem: function () { + $.post('/settings/shutdown') + toast('Stopping system...', 10000) + setTimeout(function () { + window.history.back() + }, 10000) + }, + + rebootSystem: function () { + $.post('/settings/reboot') + toast('Rebooting...', 10000) + setTimeout(function () { + window.history.back() + }, 10000) + } + } -} - -function haltSystem () { - $.post('/settings/shutdown') - toast('Stopping system...', 10000) - setTimeout(function () { - window.history.back() - }, 10000) -} - -function rebootSystem () { - $.post('/settings/reboot') - toast('Rebooting...', 10000) - setTimeout(function () { - window.history.back() - }, 10000) -} + return controls +})) diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index ea4ff05..83bb401 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -20,7 +20,7 @@ var volumeSliding = false var positionChanging var initgui = true -var popupData = {} +var popupData = {} // TODO: Refactor into one shared cache var songlength = 0 var artistshtml = '' @@ -36,12 +36,11 @@ var STREAMS_PLAYLIST_SCHEME = 'm3u' var uriSchemes = {} // array of cached playlists (not only user-playlists, also search, artist, album-playlists) -var playlists = {} +var playlists = {} // TODO: Refactor into one shared cache var currentplaylist -var customTracklists = [] +var customTracklists = [] // TODO: Refactor into one shared cache var browseStack = [] -var browseTracks = [] var ua = navigator.userAgent var isMobileSafari = /Mac/.test(ua) && /Mobile/.test(ua) @@ -68,9 +67,7 @@ PLAY_NEXT = 1 ADD_THIS_BOTTOM = 2 ADD_ALL_BOTTOM = 3 PLAY_ALL = 4 -PLAY_NOW_SEARCH = 5 - -MAX_TABLEROWS = 50 +DYNAMIC = 5 // the first part of Mopidy extensions which serve radio streams var radioExtensionsList = ['somafm', 'tunein', 'dirble', 'audioaddict'] @@ -185,9 +182,9 @@ function albumTracksToTable (pl, target, uri) { nextTrack = i < pl.length - 1 ? pl[i + 1] : undefined track = pl[i] popupData[track.uri] = track - renderSongLi(previousTrack, track, nextTrack, uri, '', ALBUM_TABLE, i, pl.length) + renderSongLi(previousTrack, track, nextTrack, uri, '', target, i, pl.length) } - updatePlayIcons(songdata.track.uri, songdata.tlid) + updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction()) } function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, currentIndex, listLength) { @@ -204,16 +201,14 @@ function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, curre $(target).append('
            • ' + track.name + ' [Stream]

            • ') return } - // Play by tlid if available. - // TODO: Need to consolidate all of the 'play...' functions - if (tlid && target === BROWSE_TABLE) { - onClick = 'return playBrowsedTracks(PLAY_ALL, ' + tlid + ');' - } else if (tlid) { + + if (target === CURRENT_PLAYLIST_TABLE && typeof tlid === 'number' && tlid >= 0) { // Current queue: Show popup menu icon. onClick plays track. tlidParameter = '\',\'' + tlid - onClick = 'return playTrackQueueByTlid(\'' + track.uri + '\',\'' + tlid + '\');' - } else { - onClick = 'return playTrackByUri(\'' + track.uri + '\',\'' + uri + '\');' + onClick = 'return controls.playQueueTrack(' + tlid + ');' + } else { // All other tracklist: Show default action icon. onClick performs default action + onClick = 'return controls.playTracks(\'\', mopidy, \'' + track.uri + '\', \'' + uri + '\');' } + $(target).append( '
            • ' + '' + @@ -223,8 +218,8 @@ function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, curre if (listLength === 1 || !hasSameAlbum(previousTrack, track) && !hasSameAlbum(track, nextTrack)) { renderSongLiAlbumInfo(track, target) } - // TODO: remove this hard-coded condition for 'ALBUM_TABLE' - if (target !== ALBUM_TABLE && !hasSameAlbum(previousTrack, track)) { + // TODO: remove this hard-coded conditions for 'ALBUM_TABLE' and 'BROWSE_TABLE' + if (target !== ALBUM_TABLE && target !== BROWSE_TABLE && !hasSameAlbum(previousTrack, track)) { // Starting to render a new album in the list. renderSongLiDivider(track, nextTrack, currentIndex, target) } @@ -339,7 +334,7 @@ function resultsToTables (results, target, uri, onClickBack, backIsOptional) { renderSongLi(previousTrack, track, nextTrack, uri, tlid, target, i, results.length) } } - updatePlayIcons(songdata.track.uri, songdata.tlid) + updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction()) } // process updated playlist to gui @@ -397,7 +392,7 @@ function getUris (tracks) { function getTracksFromUri (uri, full_track_data) { var returnTracksOrUris = function (tracks) { - return (full_track_data || false) ? tracks : getUris(tracks) + return full_track_data ? tracks : getUris(tracks) } if (customTracklists[uri]) { return returnTracksOrUris(customTracklists[uri]) @@ -552,7 +547,7 @@ function getjQueryID (identifier, uri, includePrefix) { } else if (identifier.charAt(0) !== '#' && includePrefix) { identifier = '#' + identifier } - return identifier + '-' + fixedEncodeURIComponent(uri).replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '') + return identifier + '-' + fixedEncodeURIComponent(uri).replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '') // eslint-disable-line no-useless-escape } // Strict URI encoding as per https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index abe94d5..41f1dca 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -7,8 +7,8 @@ * Song Info Sreen * ********************/ function resetSong () { - setPlayState(false) - setPosition(0) + controls.setPlayState(false) + controls.setPosition(0) var data = {} data.tlid = -1 data.track = {} @@ -84,7 +84,7 @@ function setSongInfo (data) { data.track.name = decodeURI(name[name.length - 1]) } - updatePlayIcons(data.track.uri, data.tlid) + updatePlayIcons(data.track.uri, data.tlid, controls.getIconForAction()) artistshtml = '' artiststext = '' @@ -131,7 +131,7 @@ function setSongInfo (data) { } else { $('#modalalbum').html('') } - images.setAlbumImage(data.track.uri, '#infocover, #controlspopupimage', mopidy) + images.setAlbumImage(data.track.uri, '#infocover, #albumCoverImg', mopidy) $('#modalartist').html(arttmp) @@ -153,7 +153,6 @@ function closePopups () { $('#artistpopup').popup('close') $('#coverpopup').popup('close') $('#popupQueue').popup('close') - $('#controlspopup').popup('close') } function popupTracks (e, listuri, trackuri, tlid) { @@ -163,6 +162,9 @@ function popupTracks (e, listuri, trackuri, tlid) { $('.popupTrackName').html(popupData[trackuri].name) if (popupData[trackuri].album && popupData[trackuri].album.name) { $('.popupAlbumName').html(popupData[trackuri].album.name) + $('.popupAlbumLi').show() + } else { + $('.popupAlbumLi').hide() } var child = '' @@ -194,9 +196,6 @@ function popupTracks (e, listuri, trackuri, tlid) { if (divid === 'current') { $('.addqueue').hide() popupName = '#popupQueue' - } else if (divid === 'browse') { - $('.addqueue').show() - popupName = '#popupBrowse' } else { $('.addqueue').show() popupName = '#popupTracks' @@ -232,8 +231,8 @@ function initSocketevents () { library.getCurrentPlaylist() updateStatusOfAll() library.getPlaylists() - getUriSchemes().then(function () { - showFavourites() + controls.getUriSchemes().then(function () { + controls.showFavourites() }) library.getBrowseDir() library.getSearchSchemes(searchBlacklist, mopidy) @@ -250,7 +249,7 @@ function initSocketevents () { mopidy.on('event:trackPlaybackStarted', function (data) { setSongInfo(data.tl_track) - setPlayState(true) + controls.setPlayState(true) }) mopidy.on('event:playlistsLoaded', function (data) { @@ -273,21 +272,21 @@ function initSocketevents () { }) mopidy.on('event:volumeChanged', function (data) { - setVolume(data.volume) + controls.setVolume(data.volume) }) mopidy.on('event:muteChanged', function (data) { - setMute(data.mute) + controls.setMute(data.mute) }) mopidy.on('event:playbackStateChanged', function (data) { switch (data.new_state) { case 'paused': case 'stopped': - setPlayState(false) + controls.setPlayState(false) break case 'playing': - setPlayState(true) + controls.setPlayState(true) break } }) @@ -297,7 +296,7 @@ function initSocketevents () { }) mopidy.on('event:seeked', function (data) { - setPosition(parseInt(data.time_position)) + controls.setPosition(parseInt(data.time_position)) if (play) { syncedProgressTimer.start() } @@ -420,9 +419,7 @@ function locationHashChanged () { case 'search': $('#navsearch a').addClass($.mobile.activeBtnClass) $('#searchinput').focus() - if (customTracklists['mbw:allresultscache'] === '') { - library.initSearch($('#searchinput').val()) - } + library.initSearch($('#searchinput').val()) break case 'stream': $('#navstream a').addClass('ui-state-active ui-state-persist ui-btn-active') @@ -519,7 +516,7 @@ $(document).ready(function (event) { $('#songinfo').click(function () { return switchContent('nowPlaying') }) - $('#controlspopupimage').click(function () { + $('#albumCoverImg').click(function () { return switchContent('current') }) $('#navToggleFullscreen').click(function () { @@ -559,15 +556,15 @@ $(document).ready(function (event) { var actualkey = String.fromCharCode(unicode) switch (actualkey) { case ' ': - doPlay() + controls.doPlay() event.preventDefault() break case '>': - doNext() + controls.doNext() event.preventDefault() break case '<': - doPrevious() + controls.doPrevious() event.preventDefault() break } @@ -586,8 +583,8 @@ $(document).ready(function (event) { $.event.special.swipe.durationThreshold = 500 // swipe songinfo and panel - $('#normalFooter, #nowPlayingFooter').on('swiperight', doPrevious) - $('#normalFooter, #nowPlayingFooter').on('swipeleft', doNext) + $('#normalFooter, #nowPlayingFooter').on('swiperight', controls.doPrevious) + $('#normalFooter, #nowPlayingFooter').on('swipeleft', controls.doNext) $('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swiperight', function (event) { if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) { $('#panel').panel('open') @@ -609,37 +606,55 @@ $(document).ready(function (event) { $('#trackslider').on('slidestop', function () { $('#trackslider').off('change') syncedProgressTimer.updatePosition($(this).val()) - doSeekPos($(this).val()) + controls.doSeekPos($(this).val()) }) $('#volumeslider').on('slidestart', function () { volumeSliding = true }) $('#volumeslider').on('slidestop', function () { volumeSliding = false }) - $('#volumeslider').on('change', function () { doVolume($(this).val()) }) + $('#volumeslider').on('change', function () { controls.doVolume($(this).val()) }) }) -function updatePlayIcons (uri, tlid) { +function updatePlayIcons (uri, tlid, popupMenuIcon) { // Update styles of listviews + if (arguments.length < 3) { + throw new Error('Missing parameters for "updatePlayIcons" function call.') + } var listviews = [PLAYLIST_TABLE, SEARCH_TRACK_TABLE, ARTIST_TABLE, ALBUM_TABLE, BROWSE_TABLE] var target = CURRENT_PLAYLIST_TABLE.substr(1) - $(CURRENT_PLAYLIST_TABLE).children('li').each(function () { - var eachTlid = $(this).attr('tlid') - if (typeof eachTlid !== 'undefined') { - eachTlid = parseInt(eachTlid) - } - if (this.id === getjQueryID(target, uri) && eachTlid === tlid) { - $(this).addClass('currenttrack') - } else { - $(this).removeClass('currenttrack') - } - }) + if (uri && typeof tlid === 'number' && tlid >= 0) { + $(CURRENT_PLAYLIST_TABLE).children('li.song.albumli').each(function () { + var eachTlid = $(this).attr('tlid') + if (typeof eachTlid !== 'undefined') { + eachTlid = parseInt(eachTlid) + } + if (this.id === getjQueryID(target, uri) && eachTlid === tlid) { + if (!$(this).hasClass('currenttrack')) { + $(this).addClass('currenttrack') + } + } else if ($(this).hasClass('currenttrack')) { + $(this).removeClass('currenttrack') + } + }) + } + + var popupElement for (var i = 0; i < listviews.length; i++) { target = listviews[i].substr(1) - $(listviews[i]).children('li').each(function () { - if (this.id === getjQueryID(target, uri)) { - $(this).addClass('currenttrack2') - } else { - $(this).removeClass('currenttrack2') + $(listviews[i]).children('li.song.albumli').each(function () { + if (uri) { + if (this.id === getjQueryID(target, uri)) { + $(this).addClass('currenttrack2') + } else { + $(this).removeClass('currenttrack2') + } + } + if (popupMenuIcon) { + popupElement = $(this).children('a.moreBtn').children('i').first() + if (!popupElement.hasClass(popupMenuIcon)) { + popupElement.removeClass() + popupElement.addClass(popupMenuIcon) + } } }) } diff --git a/mopidy_musicbox_webclient/static/js/library.js b/mopidy_musicbox_webclient/static/js/library.js index dad0606..13e63e8 100644 --- a/mopidy_musicbox_webclient/static/js/library.js +++ b/mopidy_musicbox_webclient/static/js/library.js @@ -37,9 +37,6 @@ document.activeElement.blur() $('input').blur() - delete customTracklists[URI_SCHEME + ':allresultscache'] - delete customTracklists[URI_SCHEME + ':artistresultscache'] - delete customTracklists[URI_SCHEME + ':albumresultscache'] delete customTracklists[URI_SCHEME + ':trackresultscache'] $('#searchartists').hide() $('#searchalbums').hide() @@ -258,7 +255,7 @@ resultsToTables(tracks, PLAYLIST_TABLE, uri, 'return library.togglePlaylists();', true) showLoading(false) }) - updatePlayIcons(uri) + updatePlayIcons(uri, '', controls.getIconForAction()) $('#playlistslist li a').each(function () { $(this).removeClass('playlistactive') if (this.id === uri) { diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js index 0b752c2..b6464d4 100644 --- a/mopidy_musicbox_webclient/static/js/process_ws.js +++ b/mopidy_musicbox_webclient/static/js/process_ws.js @@ -16,49 +16,49 @@ function processCurrenttrack (data) { * process results of volume *********************************************************/ function processVolume (data) { - setVolume(data) + controls.setVolume(data) } /** ****************************************************** * process results of mute *********************************************************/ function processMute (data) { - setMute(data) + controls.setMute(data) } /** ****************************************************** * process results of a repeat *********************************************************/ function processRepeat (data) { - setRepeat(data) + controls.setRepeat(data) } /** ****************************************************** * process results of random *********************************************************/ function processRandom (data) { - setRandom(data) + controls.setRandom(data) } /** ****************************************************** * process results of consume *********************************************************/ function processConsume (data) { - setConsume(data) + controls.setConsume(data) } /** ****************************************************** * process results of single *********************************************************/ function processSingle (data) { - setSingle(data) + controls.setSingle(data) } /** ****************************************************** * process results of current position *********************************************************/ function processCurrentposition (data) { - setPosition(parseInt(data)) + controls.setPosition(parseInt(data)) } /** ****************************************************** @@ -66,9 +66,9 @@ function processCurrentposition (data) { *********************************************************/ function processPlaystate (data) { if (data === 'playing') { - setPlayState(true) + controls.setPlayState(true) } else { - setPlayState(false) + controls.setPlayState(false) } } @@ -84,26 +84,24 @@ function processBrowseDir (resultArr) { showLoading(false) return } - browseTracks = [] uris = [] - var ref, track, previousTrack, nextTrack + var ref, previousRef, nextRef var uri = resultArr[0].uri var length = 0 || resultArr.length + customTracklists[BROWSE_TABLE] = [] for (var i = 0, index = 0; i < resultArr.length; i++) { if (resultArr[i].type === 'track') { + previousRef = ref || undefined + nextRef = i < resultArr.length - 1 ? resultArr[i + 1] : undefined ref = resultArr[i] + // TODO: consolidate usage of various arrays for caching URIs, Refs, and Tracks popupData[ref.uri] = ref - browseTracks.push(ref) + customTracklists[BROWSE_TABLE].push(ref) uris.push(ref.uri) - $(BROWSE_TABLE).append( - '
            • ' + - '' + - '' + - '' + - '

              ' + ref.name + '

            • ' - ) + renderSongLi(previousRef, ref, nextRef, BROWSE_TABLE, '', BROWSE_TABLE, index, resultArr.length) + index++ } else { var iconClass = '' @@ -119,7 +117,7 @@ function processBrowseDir (resultArr) { } } - updatePlayIcons(songdata.track.uri, songdata.tlid) + updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction()) if (uris.length > 0) { mopidy.library.lookup({'uris': uris}).then(function (resultDict) { @@ -134,6 +132,7 @@ function processBrowseDir (resultArr) { nextTrack = undefined } track = resultDict[ref.uri][0] + popupData[track.uri] = track // Need full track info in popups in order to display albums and artists. if (uris.length === 1 || (previousTrack && !hasSameAlbum(previousTrack, track) && !hasSameAlbum(track, nextTrack))) { renderSongLiAlbumInfo(track, BROWSE_TABLE) } @@ -211,7 +210,7 @@ function processCurrentPlaylist (resultArr) { currentplaylist = resultArr resultsToTables(currentplaylist, CURRENT_PLAYLIST_TABLE) mopidy.playback.getCurrentTlTrack().then(processCurrenttrack, console.error) - updatePlayIcons(songdata.track.uri, songdata.tlid) + updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction()) } /** ****************************************************** diff --git a/mopidy_musicbox_webclient/static/mb.appcache b/mopidy_musicbox_webclient/static/mb.appcache index 7b69bc6..6e558db 100644 --- a/mopidy_musicbox_webclient/static/mb.appcache +++ b/mopidy_musicbox_webclient/static/mb.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# 2016-04-12:v1 +# 2016-04-26:v2 NETWORK: * diff --git a/mopidy_musicbox_webclient/static/system.html b/mopidy_musicbox_webclient/static/system.html index a252a6e..1c4a929 100644 --- a/mopidy_musicbox_webclient/static/system.html +++ b/mopidy_musicbox_webclient/static/system.html @@ -23,8 +23,8 @@
              diff --git a/mopidy_musicbox_webclient/web.py b/mopidy_musicbox_webclient/web.py index e562fde..3ee687b 100644 --- a/mopidy_musicbox_webclient/web.py +++ b/mopidy_musicbox_webclient/web.py @@ -37,6 +37,7 @@ class IndexHandler(tornado.web.RequestHandler): 'isMusicBox': json.dumps(webclient.is_music_box()), 'websocketUrl': webclient.get_websocket_url(self.request), 'hasAlarmClock': json.dumps(webclient.has_alarm_clock()), + 'onTrackClick': webclient.get_default_click_action() } self.__path = path self.__title = string.Template('MusicBox on $hostname') diff --git a/mopidy_musicbox_webclient/webclient.py b/mopidy_musicbox_webclient/webclient.py index 6db7903..e4c6e79 100644 --- a/mopidy_musicbox_webclient/webclient.py +++ b/mopidy_musicbox_webclient/webclient.py @@ -44,3 +44,6 @@ class Webclient(object): def is_music_box(self): return self.ext_config.get('musicbox', False) + + def get_default_click_action(self): + return self.ext_config.get('on_track_click', 'PLAY_ALL') diff --git a/tests/js/dummy_tracklist.js b/tests/js/dummy_tracklist.js new file mode 100644 index 0000000..0401dba --- /dev/null +++ b/tests/js/dummy_tracklist.js @@ -0,0 +1,108 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory) + } else if (typeof module === 'object' && module.exports) { + module.exports = factory() + } else { + root.DummyTracklist = factory() + } +}(this, function () { + 'use strict' + + /* A dummy tracklist with partial support for mocking mopidy.core.TracklistController. + * + * Returns resolved promises to simulate functionality of Mopidy.js. + */ + function DummyTracklist () { + if (!(this instanceof DummyTracklist)) { + return new DummyTracklist() + } + this._tlTracks = [] + this._nextTlid = 1 + return this + } + + /* Add tracks to the tracklist. params.uris should contain an array of strings for the URIs to be added. */ + DummyTracklist.prototype.add = function (params) { + if (!params || !params.uris) { + throw new Error('No tracks provided to add.') + } + if (params.tracks || params.uri) { + throw new Error('DummyTracklist.add does not support deprecated "tracks" and "uri" parameters.') + } + + // Add tracks to end of tracklist if no position is provided + params.at_position = params.at_position || this._tlTracks.length + var tlTrack + var tlTracks = [] + for (var i = 0; i < params.uris.length; i++) { + tlTrack = { + tlid: this._nextTlid++, + track: { + uri: params.uris[i] + } + } + tlTracks.push(tlTrack) + this._tlTracks.splice(params.at_position + i, 0, tlTrack) + } + + return $.when(tlTracks) + } + + /* Clears the tracklist */ + DummyTracklist.prototype.clear = function () { + this._tlTracks = [] + } + + /** + * Retuns a list containing tlTracks that contain the provided + * criteria.uri or has ID criteria.tlid. + * + */ + DummyTracklist.prototype.filter = function (criteria) { + if (!criteria || (!criteria.uri && !criteria.tlid)) { + throw new Error('No URI or tracklist ID provided to filter on.') + } + + var matches = [] + if (criteria.uri) { // Look for matching URIs + for (var i = 0; i < criteria.uri.length; i++) { + for (var j = 0; j < this._tlTracks.length; j++) { + if (this._tlTracks[j].track.uri === criteria.uri[i]) { + matches.push(this._tlTracks[j]) + } + } + } + } + if (criteria.tlid) { // Look for matching tracklist IDs + for (i = 0; i < criteria.tlid.length; i++) { + for (j = 0; j < this._tlTracks.length; j++) { + if (this._tlTracks[j].tlid === criteria.tlid[i]) { + matches.push(this._tlTracks[j]) + } + } + } + } + return $.when(matches) + } + + /* Retuns index of the currently 'playing' track. */ + DummyTracklist.prototype.index = function (params) { + if (!params) { + if (this._tlTracks.length > 1) { + // Always just assume that the second track is playing + return $.when(1) + } else { + return $.when(0) + } + } + for (var i = 0; i < this._tlTracks.length; i++) { + if (this._tlTracks[i].tlid === params.tlid || (params.tl_track && params.tl_track.tlid === this._tlTracks[i].tlid)) { + return $.when(i) + } + } + return $.when(0) + } + + return DummyTracklist +})) diff --git a/tests/js/test_controls.js b/tests/js/test_controls.js new file mode 100644 index 0000000..6bc5b6f --- /dev/null +++ b/tests/js/test_controls.js @@ -0,0 +1,265 @@ +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 controls = require('../../mopidy_musicbox_webclient/static/js/controls.js') +var DummyTracklist = require('./dummy_tracklist.js') + +describe('controls', function () { + var mopidy + var div_element + var QUEUE_TRACKS = [ // Simulate an existing queue with three tracks loaded. + {uri: 'track:tlTrackMock1'}, + {uri: 'track:tlTrackMock2'}, + {uri: 'track:tlTrackMock3'} + ] + var NEW_TRACKS = [ // Simulate the user browsing to a folder with three tracks inside it. + {uri: 'track:trackMock1'}, + {uri: 'tunein:track:trackMock2'}, // Stream + {uri: 'track:trackMock3'} + ] + var addSpy + + before(function () { + $(document.body).append('
              ') + $('#popupTracks').popup() // Initialize popup + $(document.body).data('on-track-click', 'PLAY_ALL') // Set default click action + + mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'})) + + var playback = { + play: sinon.stub(), + stop: sinon.stub() + + } + mopidy.playback = playback + mopidy.playback.stop.returns($.when()) + // Mock the Mopidy tracklist so that we have a predictable state to test against. + mopidy.tracklist = new DummyTracklist() + addSpy = sinon.spy(mopidy.tracklist, 'add') + clearSpy = sinon.spy(mopidy.tracklist, 'clear') + }) + + beforeEach(function () { + mopidy.tracklist.clear() + clearSpy.reset() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + }) + + afterEach(function () { + mopidy.playback.play.reset() + addSpy.reset() + }) + + after(function () { + mopidy.tracklist.add.restore() + mopidy.tracklist.clear.restore() + }) + + describe('#playTracks()', function () { + it('PLAY_ALL should clear tracklist first before populating with tracks', function () { + customTracklists[BROWSE_TABLE] = NEW_TRACKS + controls.playTracks(PLAY_ALL, mopidy, NEW_TRACKS[0].uri, BROWSE_TABLE) + assert(clearSpy.called) + }) + + it('should not clear tracklist for events other than PLAY_ALL', function () { + customTracklists[BROWSE_TABLE] = NEW_TRACKS + controls.playTracks(PLAY_NOW, mopidy, NEW_TRACKS[0].uri, BROWSE_TABLE) + assert(clearSpy.notCalled) + }) + + it('should raise exception if trackUri parameter is not provided and "track" data attribute is empty', function () { + assert.throw(function () { controls.playTracks('', mopidy) }, Error) + + controls.playTracks(PLAY_ALL, mopidy, NEW_TRACKS[0].uri, BROWSE_TABLE) + assert(mopidy.playback.play.calledWithMatch({tlid: mopidy.tracklist._tlTracks[0].tlid})) + }) + + it('should raise exception if playListUri parameter is not provided and "track" data attribute is empty', function () { + assert.throw(function () { controls.playTracks('', mopidy, NEW_TRACKS[0].uri) }, Error) + + controls.playTracks(PLAY_ALL, mopidy, NEW_TRACKS[0].uri, BROWSE_TABLE) + assert(mopidy.playback.play.calledWithMatch({tlid: mopidy.tracklist._tlTracks[0].tlid})) + }) + + it('should raise exception if unknown tracklist action is provided', function () { + var getTrackURIsForActionStub = sinon.stub(controls, '_getTrackURIsForAction') // Stub to bypass earlier exception + assert.throw(function () { controls.playTracks('99', mopidy, NEW_TRACKS[0].uri, BROWSE_TABLE) }, Error) + getTrackURIsForActionStub.restore() + }) + + it('should use "track" and "list" data attributes as fallback if parameters are not provided', function () { + $('#popupTracks').data('track', 'track:trackMock1') // Simulate 'track:trackMock1' being clicked. + $('#popupTracks').data('list', BROWSE_TABLE) + customTracklists[BROWSE_TABLE] = NEW_TRACKS + + controls.playTracks(PLAY_ALL, mopidy) + assert(mopidy.playback.play.calledWithMatch({tlid: mopidy.tracklist._tlTracks[0].tlid})) + }) + + it('PLAY_NOW, PLAY_NEXT, and ADD_THIS_BOTTOM should only add one track to the tracklist', function () { + controls.playTracks(PLAY_NOW, mopidy, NEW_TRACKS[0].uri) + assert(addSpy.calledWithMatch({at_position: 2, uris: [NEW_TRACKS[0].uri]}), 'PLAY_NOW did not add correct track') + addSpy.reset() + + mopidy.tracklist.clear() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + + controls.playTracks(PLAY_NEXT, mopidy, NEW_TRACKS[0].uri) + assert(addSpy.calledWithMatch({at_position: 2, uris: [NEW_TRACKS[0].uri]}), 'PLAY_NEXT did not add correct track') + addSpy.reset() + + mopidy.tracklist.clear() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + + controls.playTracks(ADD_THIS_BOTTOM, mopidy, NEW_TRACKS[0].uri) + assert(addSpy.calledWithMatch({uris: [NEW_TRACKS[0].uri]}), 'ADD_THIS_BOTTOM did not add correct track') + }) + + it('PLAY_ALL and ADD_ALL_BOTTOM should add all tracks to tracklist', function () { + controls.playTracks(PLAY_ALL, mopidy, NEW_TRACKS[0].uri) + assert(addSpy.calledWithMatch({uris: getUris(NEW_TRACKS)}), 'PLAY_ALL did not add correct tracks') + addSpy.reset() + + mopidy.tracklist.clear() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + + controls.playTracks(ADD_ALL_BOTTOM, mopidy, NEW_TRACKS[0].uri) + assert(addSpy.calledWithMatch({uris: getUris(NEW_TRACKS)}), 'ADD_ALL_BOTTOM did not add correct tracks') + }) + + it('PLAY_NOW and PLAY_NEXT should insert track after currently playing track', function () { + controls.playTracks(PLAY_NOW, mopidy, NEW_TRACKS[0].uri) + assert(addSpy.calledWithMatch({at_position: 2, uris: [NEW_TRACKS[0].uri]}), 'PLAY_NOW did not insert track at correct position') + addSpy.reset() + + mopidy.tracklist.clear() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + + controls.playTracks(PLAY_NEXT, mopidy, NEW_TRACKS[0].uri) + assert(addSpy.calledWithMatch({at_position: 2, uris: [NEW_TRACKS[0].uri]}), 'PLAY_NEXT did not insert track at correct position') + }) + + it('only PLAY_NOW and PLAY_ALL should trigger playback', function () { + controls.playTracks(PLAY_NOW, mopidy, 2) + assert(mopidy.playback.play.calledWithMatch({tlid: mopidy.tracklist._tlTracks[2].tlid}), 'PLAY_NOW did not start playback of correct track') + mopidy.playback.play.reset() + + mopidy.tracklist.clear() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + + controls.playTracks(PLAY_NEXT, mopidy, NEW_TRACKS[0].uri) + assert.isFalse(mopidy.playback.play.called, 'PLAY_NEXT should not have triggered playback to start') + mopidy.playback.play.reset() + + mopidy.tracklist.clear() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + + controls.playTracks(ADD_THIS_BOTTOM, mopidy, NEW_TRACKS[0].uri) + assert.isFalse(mopidy.playback.play.called, 'ADD_THIS_BOTTOM should not have triggered playback to start') + mopidy.playback.play.reset() + + mopidy.tracklist.clear() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + + controls.playTracks(PLAY_ALL, mopidy, NEW_TRACKS[2].uri) + assert(mopidy.playback.play.calledWithMatch({tlid: mopidy.tracklist._tlTracks[2].tlid}), 'PLAY_ALL did not start playback of correct track') + mopidy.playback.play.reset() + + mopidy.tracklist.clear() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + + controls.playTracks(ADD_ALL_BOTTOM, mopidy, NEW_TRACKS[0].uri) + assert.isFalse(mopidy.playback.play.called, 'ADD_ALL_BOTTOM should not have triggered playback to start') + mopidy.playback.play.reset() + }) + + it('should store last action in cookie if on-track-click mode is set to "DYNAMIC"', function () { + $(document.body).data('on-track-click', 'DYNAMIC') + var cookieStub = sinon.stub($, 'cookie') + controls.playTracks(PLAY_NOW, mopidy, 2) + assert(cookieStub.calledWithMatch('onTrackClick', PLAY_NOW, {expires: 365})) + cookieStub.reset() + + $(document.body).data('on-track-click', 'PLAY_NOW') + controls.playTracks(PLAY_NOW, mopidy, 2) + assert(cookieStub.notCalled) + cookieStub.restore() + }) + }) + + describe('#getAction()', function () { + it('should use default action if none is specified', function () { + window.MOCK_DEFAULT = 99 // Define global variable to test against. + $(document.body).data('on-track-click', 'MOCK_DEFAULT') + assert.equal(controls.getAction(), 99) + }) + + it('should get action from cookie if action is set to "DYNAMIC"', function () { + $(document.body).data('on-track-click', 'DYNAMIC') + var cookieStub = sinon.stub($, 'cookie') + controls.getAction() + assert(cookieStub.called) + cookieStub.restore() + }) + + it('should default to "PLAY_ALL" if no cookie is available for "DYNAMIC"', function () { + $(document.body).data('on-track-click', 'DYNAMIC') + $.removeCookie('onTrackClick') + assert.equal(controls.getAction(), PLAY_ALL) + }) + }) + + describe('#getIconForAction()', function () { + it('should return correct FontAwesome class for each tracklist action', function () { + assert.equal(controls.getIconForAction(PLAY_ALL), 'fa fa-fast-forward') + assert.equal(controls.getIconForAction(PLAY_NOW), 'fa fa-play') + assert.equal(controls.getIconForAction(PLAY_NEXT), 'fa fa-level-down') + assert.equal(controls.getIconForAction(ADD_THIS_BOTTOM), 'fa fa-plus-square-o') + assert.equal(controls.getIconForAction(ADD_ALL_BOTTOM), 'fa fa-plus-square') + }) + + it('should raise error if unknown tracklist action is provided', function () { + assert.throw(function () { controls.getIconForAction(99) }, Error) + }) + + it('should handle action identifier strings in addition to integers', function () { + assert.equal(controls.getIconForAction('0'), 'fa fa-play') + }) + + it('should use default tracklist action if no parameter is provided', function () { + assert.equal(controls.getIconForAction(), 'fa fa-fast-forward') + }) + }) + + describe('#_getTrackURIsForAction()', function () { + it('should return just "trackUri" for PLAY_NOW, PLAY_NEXT, and ADD_THIS_BOTTOM', function () { + assert.equal(controls._getTrackURIsForAction(PLAY_NOW, 'mockUri')[0], 'mockUri') + assert.equal(controls._getTrackURIsForAction(PLAY_NEXT, 'mockUri')[0], 'mockUri') + assert.equal(controls._getTrackURIsForAction(ADD_THIS_BOTTOM, 'mockUri')[0], 'mockUri') + }) + + it('should get tracks from "playlistUri" for PLAY_ALL, and ADD_ALL_BOTTOM', function () { + customTracklists[BROWSE_TABLE] = NEW_TRACKS + + var tracks = controls._getTrackURIsForAction(PLAY_ALL, NEW_TRACKS[0], BROWSE_TABLE) + assert.equal(tracks.length, NEW_TRACKS.length) + for (var i = 0; i < tracks.length; i++) { + assert.equal(tracks[i], NEW_TRACKS[i].uri) + } + }) + + it('should raise error if unknown tracklist action is provided', function () { + assert.throw(function () { controls._getTrackURIsForAction(99) }, Error) + }) + + it('should handle action identifier strings in addition to integers', function () { + assert.equal(controls._getTrackURIsForAction('0', 'mockUri')[0], 'mockUri') + }) + }) +}) diff --git a/tests/js/test_images.js b/tests/js/test_images.js index 11af74c..68004b9 100644 --- a/tests/js/test_images.js +++ b/tests/js/test_images.js @@ -243,6 +243,9 @@ describe('images', function () { getImagesSpy.reset() setDeprecatedAlbumImageSpy.reset() }) + after(function () { + mopidy.library.getImages.restore() + }) it('should use default image if no track URI is provided', function () { images.setAlbumImage('', img_element, mopidy) @@ -302,7 +305,7 @@ describe('images', function () { assert.isTrue(lookupSpy.calledOnce) expect($(img_element).prop('src')).to.endWith('mockAlbumImageUri') - lookupSpy.restore() + mopidy.library.lookup.restore() }) it('should use default image if track.album or track.artist is not available', function () { @@ -313,7 +316,7 @@ describe('images', function () { assert.isTrue(lookupSpy.calledOnce) expect($(img_element).prop('src')).to.endWith(images.DEFAULT_ALBUM_URL) - lookupSpy.restore() + mopidy.library.lookup.restore() }) it('should fall back to retrieving image from last.fm if none provided by Mopidy', function () { @@ -414,7 +417,7 @@ describe('images', function () { getImagesSpy.reset() }) after(function () { - getImagesSpy.restore() + mopidy.library.getImages.restore() }) it('should use default image if no artist URI is provided', function () { diff --git a/tests/js/test_synced_timer.js b/tests/js/test_synced_timer.js index d921eb2..3e296d8 100644 --- a/tests/js/test_synced_timer.js +++ b/tests/js/test_synced_timer.js @@ -223,7 +223,7 @@ describe('SyncedTimer', function () { clock.tick(1001) syncedProgressTimer._scheduleSync(1000) assert(clearSpy.calledWith(scheduleID)) - clearSpy.restore() + window.clearTimeout.restore() }) }) @@ -358,7 +358,7 @@ describe('SyncedTimer', function () { assert(scheduleSpy.calledWith(0)) syncedProgressTimer.stop() - scheduleSpy.restore() + syncedProgressTimer._scheduleSync.restore() }) }) @@ -392,7 +392,7 @@ describe('SyncedTimer', function () { assert.isFalse(syncedProgressTimer._isSyncScheduled) assert(cancelSpy.calledWith(syncedProgressTimer._scheduleID)) - cancelSpy.restore() + window.clearTimeout.restore() }) }) @@ -422,7 +422,7 @@ describe('SyncedTimer', function () { assert.isTrue(formatSpy.called) expect(syncedProgressTimer.positionNode.nodeValue).to.equal('0:01') - formatSpy.restore() + SyncedProgressTimer.format.restore() }) it('should set position to "" if timer has not been initialized', function () { diff --git a/tests/test_extension.py b/tests/test_extension.py index cfaa39d..5ce0d43 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -18,6 +18,7 @@ class ExtensionTests(unittest.TestCase): assert 'enabled = true' in config assert 'websocket_host =' in config assert 'websocket_port =' in config + assert 'on_track_click = PLAY_ALL' in config def test_get_config_schema(self): ext = Extension() @@ -27,6 +28,7 @@ class ExtensionTests(unittest.TestCase): assert 'musicbox' in schema assert 'websocket_host' in schema assert 'websocket_port' in schema + assert 'on_track_click' in schema def test_setup(self): registry = mock.Mock() diff --git a/tests/test_webclient.py b/tests/test_webclient.py index 1ac2657..e15acaa 100644 --- a/tests/test_webclient.py +++ b/tests/test_webclient.py @@ -76,3 +76,6 @@ class WebclientTests(unittest.TestCase): def test_is_musicbox(self): assert not self.mmw.is_music_box() + + def test_default_click_action(self): + assert self.mmw.get_default_click_action() == 'PLAY_ALL' From 4474658d39abe377d8980fef7c42b1c1755d3597 Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 16 Apr 2016 17:10:52 +0200 Subject: [PATCH 22/38] Reduce number of DOM manipulations to improve performance. --- .gitignore | 2 + README.rst | 3 + .../static/js/functionsvars.js | 67 +++++++++++-------- mopidy_musicbox_webclient/static/js/gui.js | 1 - .../static/js/process_ws.js | 13 ++-- mopidy_musicbox_webclient/static/mb.appcache | 2 +- 6 files changed, 52 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index e66dbe5..8c6a739 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ npm-debug.log .project *.pbxproj *.egg-info +.cache +.eggs diff --git a/README.rst b/README.rst index 00fa03e..6076f85 100644 --- a/README.rst +++ b/README.rst @@ -85,6 +85,7 @@ v2.3.0 (UNRELEASED) (Addresses: `#133 `_). - Optimized updating of 'now playing' icons in tracklists. (Addresses: `#184 `_). +- Optimized rendering of large lists of tracks to make UI more responsive. **Fixes** @@ -93,6 +94,8 @@ v2.3.0 (UNRELEASED) - last.fm artist image lookups should now always return the correct image for similarly named artists. - Ensure that browsed tracks are always added to the queue using the track URI rather than the track's position in the folder. (Fixes: `#124 `_). +- Fixed an issue where searches would be performed as soon as the user switches to the 'Search' pane, + instead of waiting for the 'Search!' button to be clicked. v2.2.0 (2016-03-01) ------------------- diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index 83bb401..7079d62 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -175,6 +175,7 @@ function artistsToString (artists, max) { *********************************************************/ function albumTracksToTable (pl, target, uri) { var track, previousTrack, nextTrack + var html = '' $(target).empty() $(target).attr('data', uri) for (var i = 0; i < pl.length; i++) { @@ -182,8 +183,9 @@ function albumTracksToTable (pl, target, uri) { nextTrack = i < pl.length - 1 ? pl[i + 1] : undefined track = pl[i] popupData[track.uri] = track - renderSongLi(previousTrack, track, nextTrack, uri, '', target, i, pl.length) + html += renderSongLi(previousTrack, track, nextTrack, uri, '', target, i, pl.length) } + $(target).append(html) updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction()) } @@ -191,15 +193,16 @@ function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, curre var name var tlidParameter = '' var onClick = '' + var html = '' track.name = validateTrackName(track, currentIndex) // Leave out unplayable items if (track.name.substring(0, 12) === '[unplayable]') { - return + return html } // Streams if (track.length === -1) { - $(target).append('
            • ' + track.name + ' [Stream]

            • ') - return + html += '
            • ' + track.name + ' [Stream]

            • ' + return html } if (target === CURRENT_PLAYLIST_TABLE && typeof tlid === 'number' && tlid >= 0) { // Current queue: Show popup menu icon. onClick plays track. @@ -209,33 +212,33 @@ function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, curre onClick = 'return controls.playTracks(\'\', mopidy, \'' + track.uri + '\', \'' + uri + '\');' } - $(target).append( + html += '
            • ' + '' + '' + - '

              ' + track.name + '

            • ' - ) + '

              ' + track.name + '

              ' + if (listLength === 1 || !hasSameAlbum(previousTrack, track) && !hasSameAlbum(track, nextTrack)) { - renderSongLiAlbumInfo(track, target) - } - // TODO: remove this hard-coded conditions for 'ALBUM_TABLE' and 'BROWSE_TABLE' - if (target !== ALBUM_TABLE && target !== BROWSE_TABLE && !hasSameAlbum(previousTrack, track)) { - // Starting to render a new album in the list. - renderSongLiDivider(track, nextTrack, currentIndex, target) + html += renderSongLiAlbumInfo(track) } + html += '
              ' + return html } +/* Tracklist renderer for track artist and album name. */ function renderSongLiAlbumInfo (track, target) { - var html = '

              ' - html += renderSongLiTrackArtists(track) + var html = renderSongLiTrackArtists(track) if (track.album && track.album.name) { html += ' - ' + track.album.name + '

              ' } - target = getjQueryID(target, track.uri, true) - $(target).children('a').eq(1).append(html) - $(target + ' a h1 i').addClass(getMediaClass(track.uri)) + if (typeof target !== 'undefined' && target.length > 0) { + target = getjQueryID(target, track.uri, true) + $(target).children('a').eq(1).append(html) + } + return html } +/* Tracklist renderer for track artist information. */ function renderSongLiTrackArtists (track) { var html = '' if (track.artists) { @@ -252,23 +255,28 @@ function renderSongLiTrackArtists (track) { return html } -function renderSongLiDivider (track, nextTrack, currentIndex, target) { - target = getjQueryID(target, track.uri, true) - // Render differently if part of an album - if (hasSameAlbum(track, nextTrack)) { - // Large divider with album cover - $(target).before( +/* Tracklist renderer to insert dividers between albums. */ +function renderSongLiDivider (previousTrack, track, nextTrack, currentIndex, target) { + var html = '' + // Render differently if part of an album. + if (!hasSameAlbum(previousTrack, track) && hasSameAlbum(track, nextTrack)) { + // Large divider with album cover. + html += '
            • ' + '' + '

              ' + track.album.name + '

              ' + renderSongLiTrackArtists(track) + '

            • ' - ) // Retrieve album covers images.setAlbumImage(track.uri, getjQueryID(target + '-cover', track.uri, true), mopidy, 'small') - } else if (currentIndex > 0) { + } else if (!hasSameAlbum(track, nextTrack) && currentIndex > 0) { // Small divider - $(target).before('
            •  
            • ') + html += '
            •  
            • ' } + if (typeof target !== 'undefined' && target.length > 0) { + target = getjQueryID(target, track.uri, true) + $(target).before(html) + } + return html } function renderSongLiBackButton (results, target, onClick, optional) { @@ -317,6 +325,7 @@ function resultsToTables (results, target, uri, onClickBack, backIsOptional) { $(target).attr('data', uri) var track, previousTrack, nextTrack, tlid + var html = '' // Break into albums and put in tables for (i = 0; i < results.length; i++) { @@ -331,9 +340,11 @@ function resultsToTables (results, target, uri, onClickBack, backIsOptional) { nextTrack = nextTrack ? nextTrack.track : undefined } popupData[track.uri] = track - renderSongLi(previousTrack, track, nextTrack, uri, tlid, target, i, results.length) + html += renderSongLiDivider(previousTrack, track, nextTrack, i, target) + html += renderSongLi(previousTrack, track, nextTrack, uri, tlid, target, i, results.length) } } + $(target).append(html) updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction()) } diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index 41f1dca..a175b52 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -419,7 +419,6 @@ function locationHashChanged () { case 'search': $('#navsearch a').addClass($.mobile.activeBtnClass) $('#searchinput').focus() - library.initSearch($('#searchinput').val()) break case 'stream': $('#navstream a').addClass('ui-state-active ui-state-persist ui-btn-active') diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js index b6464d4..794c17e 100644 --- a/mopidy_musicbox_webclient/static/js/process_ws.js +++ b/mopidy_musicbox_webclient/static/js/process_ws.js @@ -89,6 +89,7 @@ function processBrowseDir (resultArr) { var uri = resultArr[0].uri var length = 0 || resultArr.length customTracklists[BROWSE_TABLE] = [] + var html = '' for (var i = 0, index = 0; i < resultArr.length; i++) { if (resultArr[i].type === 'track') { @@ -100,7 +101,7 @@ function processBrowseDir (resultArr) { customTracklists[BROWSE_TABLE].push(ref) uris.push(ref.uri) - renderSongLi(previousRef, ref, nextRef, BROWSE_TABLE, '', BROWSE_TABLE, index, resultArr.length) + html += renderSongLi(previousRef, ref, nextRef, BROWSE_TABLE, '', BROWSE_TABLE, index, resultArr.length) index++ } else { @@ -110,13 +111,13 @@ function processBrowseDir (resultArr) { } else { iconClass = getMediaClass(resultArr[i].uri) } - $(BROWSE_TABLE).append( - '
            • ' + - '

              ' + resultArr[i].name + '

            • ' - ) + html += '
            • ' + + '

              ' + resultArr[i].name + '

            • ' } } + $(BROWSE_TABLE).append(html) + updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction()) if (uris.length > 0) { @@ -138,7 +139,7 @@ function processBrowseDir (resultArr) { } if (!hasSameAlbum(previousTrack, track)) { // Starting to render a new album in the list. - renderSongLiDivider(track, nextTrack, i, BROWSE_TABLE) + renderSongLiDivider(previousTrack, track, nextTrack, i, BROWSE_TABLE) } } }) diff --git a/mopidy_musicbox_webclient/static/mb.appcache b/mopidy_musicbox_webclient/static/mb.appcache index 6e558db..eca4c70 100644 --- a/mopidy_musicbox_webclient/static/mb.appcache +++ b/mopidy_musicbox_webclient/static/mb.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# 2016-04-26:v2 +# 2016-04-27:v1 NETWORK: * From 7de3476b5a4bb5bad8384837389a96a80f7e90ec Mon Sep 17 00:00:00 2001 From: jcass Date: Thu, 28 Apr 2016 14:45:40 +0200 Subject: [PATCH 23/38] Add 'Folder' FontAwesome icon on the Browse pane. --- README.rst | 1 + mopidy_musicbox_webclient/static/js/functionsvars.js | 1 + 2 files changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 6076f85..3c074c7 100644 --- a/README.rst +++ b/README.rst @@ -86,6 +86,7 @@ v2.3.0 (UNRELEASED) - Optimized updating of 'now playing' icons in tracklists. (Addresses: `#184 `_). - Optimized rendering of large lists of tracks to make UI more responsive. +- Added 'Folder' FontAwesome icon on the Browse pane for browsing the filesystem. **Fixes** diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index 7079d62..c4cc492 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -76,6 +76,7 @@ var uriClassList = [ ['spotify', 'fa-spotify'], ['spotifytunigo', 'fa-spotify'], ['local', 'fa-file-sound-o'], + ['file', 'fa-folder-o'], ['m3u', 'fa-file-sound-o'], ['podcast', 'fa-rss-square'], ['dirble', 'fa-microphone'], From 7239d4dd4ba49bcc0bf3ffdb6f4fbc828d4399c8 Mon Sep 17 00:00:00 2001 From: jcass Date: Wed, 4 May 2016 18:10:38 +0200 Subject: [PATCH 24/38] fix:Small divider should not be rendered before last track in an album. Fixes #196. --- mopidy_musicbox_webclient/static/js/functionsvars.js | 8 ++++---- mopidy_musicbox_webclient/static/js/process_ws.js | 5 +---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index c4cc492..e8b5fd0 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -219,7 +219,7 @@ function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, curre '' + '

              ' + track.name + '

              ' - if (listLength === 1 || !hasSameAlbum(previousTrack, track) && !hasSameAlbum(track, nextTrack)) { + if (listLength === 1 || (!hasSameAlbum(previousTrack, track) && !hasSameAlbum(track, nextTrack))) { html += renderSongLiAlbumInfo(track) } html += '
              ' @@ -257,7 +257,7 @@ function renderSongLiTrackArtists (track) { } /* Tracklist renderer to insert dividers between albums. */ -function renderSongLiDivider (previousTrack, track, nextTrack, currentIndex, target) { +function renderSongLiDivider (previousTrack, track, nextTrack, target) { var html = '' // Render differently if part of an album. if (!hasSameAlbum(previousTrack, track) && hasSameAlbum(track, nextTrack)) { @@ -269,7 +269,7 @@ function renderSongLiDivider (previousTrack, track, nextTrack, currentIndex, tar renderSongLiTrackArtists(track) + '

              ' // Retrieve album covers images.setAlbumImage(track.uri, getjQueryID(target + '-cover', track.uri, true), mopidy, 'small') - } else if (!hasSameAlbum(track, nextTrack) && currentIndex > 0) { + } else if (previousTrack && !hasSameAlbum(previousTrack, track)) { // Small divider html += '
            •  
            • ' } @@ -341,7 +341,7 @@ function resultsToTables (results, target, uri, onClickBack, backIsOptional) { nextTrack = nextTrack ? nextTrack.track : undefined } popupData[track.uri] = track - html += renderSongLiDivider(previousTrack, track, nextTrack, i, target) + html += renderSongLiDivider(previousTrack, track, nextTrack, target) html += renderSongLi(previousTrack, track, nextTrack, uri, tlid, target, i, results.length) } } diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js index 794c17e..a6bfe40 100644 --- a/mopidy_musicbox_webclient/static/js/process_ws.js +++ b/mopidy_musicbox_webclient/static/js/process_ws.js @@ -137,10 +137,7 @@ function processBrowseDir (resultArr) { if (uris.length === 1 || (previousTrack && !hasSameAlbum(previousTrack, track) && !hasSameAlbum(track, nextTrack))) { renderSongLiAlbumInfo(track, BROWSE_TABLE) } - if (!hasSameAlbum(previousTrack, track)) { - // Starting to render a new album in the list. - renderSongLiDivider(previousTrack, track, nextTrack, i, BROWSE_TABLE) - } + renderSongLiDivider(previousTrack, track, nextTrack, BROWSE_TABLE) } }) showLoading(false) From 30ac2a506567243b1a49ccc7ccc33cac1653d0fa Mon Sep 17 00:00:00 2001 From: jcass Date: Thu, 5 May 2016 07:04:40 +0200 Subject: [PATCH 25/38] New icons for 'PLAY' and 'PLAY_ALL' action. --- README.rst | 2 ++ mopidy_musicbox_webclient/static/css/webclient.css | 9 +++++++-- mopidy_musicbox_webclient/static/index.html | 6 +++--- mopidy_musicbox_webclient/static/js/controls.js | 4 ++-- mopidy_musicbox_webclient/static/js/functionsvars.js | 2 +- mopidy_musicbox_webclient/static/mb.appcache | 2 +- tests/js/test_controls.js | 8 ++++---- 7 files changed, 20 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 3c074c7..1695a4c 100644 --- a/README.rst +++ b/README.rst @@ -87,6 +87,8 @@ v2.3.0 (UNRELEASED) (Addresses: `#184 `_). - Optimized rendering of large lists of tracks to make UI more responsive. - Added 'Folder' FontAwesome icon on the Browse pane for browsing the filesystem. +- New icons for 'PLAY' and 'PLAY_ALL' actions. In general, icons with an empty background will perform an action only + on the selected track, while icons with a filled background will apply the action to all tracks in the list. **Fixes** diff --git a/mopidy_musicbox_webclient/static/css/webclient.css b/mopidy_musicbox_webclient/static/css/webclient.css index 38637c1..d792822 100644 --- a/mopidy_musicbox_webclient/static/css/webclient.css +++ b/mopidy_musicbox_webclient/static/css/webclient.css @@ -300,6 +300,11 @@ line-height: 100%; } +.moreBtn i { + color: #ddd; + text-size: initial; +} + .backnav { background-color: #ccc !important; } @@ -395,11 +400,11 @@ } .ui-icon-playAll:after { - content: '\f050'; + content: '\f144'; } .ui-icon-play:after { - content: '\f04b'; + content: '\f01d'; } .ui-icon-playNext:after { diff --git a/mopidy_musicbox_webclient/static/index.html b/mopidy_musicbox_webclient/static/index.html index 8620f30..842ad8e 100644 --- a/mopidy_musicbox_webclient/static/index.html +++ b/mopidy_musicbox_webclient/static/index.html @@ -115,12 +115,12 @@