Release v2.3.0
This commit is contained in:
commit
53ff635f6a
1
.csslintrc
Normal file
1
.csslintrc
Normal file
@ -0,0 +1 @@
|
||||
--format=compact
|
||||
12
.eslintrc
Normal file
12
.eslintrc
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"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.
|
||||
"camelcase": 1,
|
||||
}
|
||||
}
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -8,12 +8,16 @@
|
||||
MANIFEST
|
||||
build/
|
||||
cover/
|
||||
.karma_coverage/
|
||||
coverage.xml
|
||||
dist/
|
||||
docs/_build/
|
||||
mopidy.log*
|
||||
node_modules/
|
||||
nosetests.xml
|
||||
npm-debug.log
|
||||
.project
|
||||
*.pbxproj
|
||||
*.egg-info
|
||||
.cache
|
||||
.eggs
|
||||
|
||||
23
.travis.yml
23
.travis.yml
@ -1,13 +1,29 @@
|
||||
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
|
||||
# Need to run 'py27' and 'test' together so that we can share coverage reports.
|
||||
- TOX_ENV=py27,test
|
||||
- TOX_ENV=flake8
|
||||
- TOX_ENV=eslint
|
||||
- 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"
|
||||
|
||||
install:
|
||||
- "pip install tox"
|
||||
@ -16,4 +32,5 @@ script:
|
||||
- "tox -e $TOX_ENV"
|
||||
|
||||
after_success:
|
||||
- "if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; 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"
|
||||
|
||||
114
README.rst
114
README.rst
@ -10,27 +10,52 @@ 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 <http://www.raspberrypi.org/>`_)
|
||||
and remotely control it using your computer, tablet or phone.
|
||||
.. image:: https://img.shields.io/travis/pimusicbox/mopidy-musicbox-webclient/develop.svg?style=flat
|
||||
:target: https://travis-ci.org/pimusicbox/mopidy-musicbox-webclient
|
||||
:alt: Travis CI build status
|
||||
|
||||
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.
|
||||
.. image:: https://img.shields.io/coveralls/pimusicbox/mopidy-musicbox-webclient/develop.svg?style=flat
|
||||
:target: https://coveralls.io/r/pimusicbox/mopidy-musicbox-webclient?branch=develop
|
||||
:alt: Test coverage
|
||||
|
||||
`Mopidy <http://www.mopidy.com/>`_ is a music server which can play music from Spotify, Google Music, SoundCloud, etc.
|
||||
or from your hard drive.
|
||||
.. image:: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat
|
||||
:target: http://standardjs.com/
|
||||
:alt: JavaScript Standard Style
|
||||
|
||||
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 <http://www.pimusicbox.com/>`_.
|
||||
Mopidy MusicBox Webclient (MMW) is a frontend extension and JavaScript-based web client especially written for
|
||||
`Mopidy <http://www.mopidy.com/>`_.
|
||||
|
||||
.. image:: https://github.com/pimusicbox/mopidy-musicbox-webclient/raw/master/screenshots/playlists_desktop.png
|
||||
Features
|
||||
========
|
||||
|
||||
- Responsive design that works equally well on desktop and mobile browsers.
|
||||
- Browse content provided by any Mopidy backend extension.
|
||||
- Add one or more tracks or entire albums to the queue.
|
||||
- Save the current queue to an easily accessible playlist.
|
||||
- Search for tracks, albums, or artists from specific backends or all of Mopidy.
|
||||
- Shows detailed track and album information during playback, with album cover retrieval from Last.fm.
|
||||
- Seek tracks during playback.
|
||||
- Support for all of the Mopidy playback controls (consume mode, repeat, shuffle, etc.)
|
||||
- Deep integration with, and additional features for, the `Pi MusicBox <http://www.pimusicbox.com/>`_.
|
||||
- Fullscreen mode.
|
||||
|
||||
.. image:: https://github.com/pimusicbox/mopidy-musicbox-webclient/raw/develop/screenshots/queue_desktop.png
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
- MMW has been tested on the major browsers (Chrome, IE, Firefox, Safari, iOS). It *may* also work on other browsers
|
||||
that support websockets, cookies, and JavaScript.
|
||||
|
||||
- ``Mopidy`` >= 1.1.0. An extensible music server that plays music from local disk, Spotify, SoundCloud, Google
|
||||
Play Music, and more.
|
||||
|
||||
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. ::
|
||||
@ -40,11 +65,38 @@ Alternatively, clone the repository and run ``sudo python setup.py install`` fro
|
||||
$ sudo python setup.py install
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
MMW is shipped with default settings that should work straight out of the box for most users::
|
||||
|
||||
[musicbox_webclient]
|
||||
enabled = true
|
||||
musicbox = false
|
||||
websocket_host =
|
||||
websocket_port =
|
||||
on_track_click = PLAY_ALL
|
||||
|
||||
The following configuration values are available should you wish to customize your installation further:
|
||||
|
||||
- ``musicbox_webclient/enabled``: If the MMW extension should be enabled or not. Defaults to ``true``.
|
||||
|
||||
- ``musicbox_webclient/musicbox``: Set this to ``true`` if you are connecting to a Mopidy instance running on a
|
||||
Pi Musicbox. Expands the MMW user interface to include features for rebooting the Pi, changing configuration values
|
||||
via a web interface, etc.
|
||||
|
||||
- ``musicbox_webclient/websocket_host``: Optional setting that can be used to specify the target host for Mopidy websocket connections.
|
||||
|
||||
- ``musicbox_webclient/websocket_port``: Optional setting that can be used to specify the target port for Mopidy websocket connections.
|
||||
|
||||
- ``musicbox_webclient/on_track_click``: Specifies the default action that should be performed when the user clicks on
|
||||
a track that is displayed in the Browse pane or as part of Search results. Valid options are: ``PLAY_NOW``,
|
||||
``PLAY_NEXT``, ``ADD_THIS_BOTTOM``, ``ADD_ALL_BOTTOM``, ``PLAY_ALL`` (default), and ``DYNAMIC`` (repeats last action).
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Point your (modern) browser at Mopidy-MusicBox-Webclient running on your Mopidy server e.g.
|
||||
http://localhost:6680/musicbox_webclient.
|
||||
Enter the address of the Mopidy server that you are connecting to in your browser (e.g. http://localhost:6680/musicbox_webclient)
|
||||
|
||||
|
||||
Project resources
|
||||
@ -58,6 +110,44 @@ Project resources
|
||||
Changelog
|
||||
=========
|
||||
|
||||
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 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/99>`_).
|
||||
- Now remembers which backend was searched previously, and automatically selects that backend as the default search target.
|
||||
(Addresses: `#130 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/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 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/133>`_).
|
||||
- Optimized updating of 'now playing' icons in tracklists.
|
||||
(Addresses: `#184 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/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.
|
||||
- Standardize popup dialog layout convention: Sentence fragments have no punctuation, buttons that confirm a
|
||||
destructive action go on the left.
|
||||
|
||||
**Fixes**
|
||||
|
||||
- Don't create Mopidy models manually. (Fixes: `#172 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/172>`_).
|
||||
- Context menu is now available for all tracks in browse pane. (Fixes: `#126 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/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 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/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.
|
||||
- Fixed an issue where the last track in an album was not grouped properly with the rest of the results, and would have
|
||||
a small divider rendered above it. (Fixes: `#196 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/196>`_).
|
||||
- Replaced JavaScript confirmation prompt on 'Streams' pane with jQuery equivalent.
|
||||
(Fixes: `#191 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/191>`_).
|
||||
- Clearing the queue should no longer trigger an album cover image lookup.
|
||||
(Fixes: `#201 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/201>`_).
|
||||
|
||||
v2.2.0 (2016-03-01)
|
||||
-------------------
|
||||
|
||||
|
||||
80
karma.conf.js
Normal file
80
karma.conf.js
Normal file
@ -0,0 +1,80 @@
|
||||
// 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',
|
||||
'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: '.karma_coverage/',
|
||||
reporters: [
|
||||
{ type: 'lcov', subdir: '.' },
|
||||
{ type: 'text' }
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -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,10 +18,17 @@ 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)
|
||||
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):
|
||||
|
||||
@ -3,3 +3,4 @@ enabled = true
|
||||
musicbox = false
|
||||
websocket_host =
|
||||
websocket_port =
|
||||
on_track_click = PLAY_ALL
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#playlisttracksback {
|
||||
.backnav-optional {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -74,7 +74,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#playlisttracksback {
|
||||
.backnav-optional {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@ -244,8 +244,8 @@
|
||||
}
|
||||
|
||||
.smalldivider {
|
||||
font-size: 25% !important;
|
||||
height: 5px !important;
|
||||
font-size: 10%;
|
||||
height: 2px;
|
||||
background-color: #ddd !important;
|
||||
}
|
||||
|
||||
@ -293,13 +293,22 @@
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.song .moreBtn{
|
||||
.song .moreBtn {
|
||||
float: right;
|
||||
padding: 15px 18px 12px 22px;
|
||||
display: inline-block;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.moreBtn i {
|
||||
color: #ddd;
|
||||
font-size: initial;
|
||||
}
|
||||
|
||||
.backnav {
|
||||
background-color: #ccc !important;
|
||||
}
|
||||
|
||||
|
||||
/**********************
|
||||
* Now Playing area *
|
||||
@ -338,6 +347,11 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.popupArtistLi,
|
||||
.popupAlbumLi {
|
||||
display: none
|
||||
}
|
||||
|
||||
.popupArtistName,
|
||||
.popupTrackName,
|
||||
.popupAlbumName,
|
||||
@ -357,7 +371,7 @@
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#controlspopupimage,
|
||||
#albumCoverImg,
|
||||
#coverpopupimage,
|
||||
#artistpopupimage {
|
||||
display: block;
|
||||
@ -368,18 +382,61 @@
|
||||
max-height: 90%;
|
||||
}
|
||||
|
||||
#popupTracksLv li,
|
||||
#popupQueueLv 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 {
|
||||
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: '\f144';
|
||||
}
|
||||
|
||||
.ui-icon-play:after {
|
||||
content: '\f01d';
|
||||
}
|
||||
|
||||
.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 {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*dont hide clear buttons in text input */
|
||||
@ -517,7 +574,7 @@ a {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#controlspopupimage {
|
||||
#albumCoverImg {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
margin-bottom: 3px;
|
||||
|
||||
@ -1,30 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html manifest="mb.appcache">
|
||||
<head>
|
||||
<title>Musicbox</title>
|
||||
<title>{{ title }}</title>
|
||||
<meta charset="utf-8">
|
||||
<script type="text/javascript" src="vendors/jquery/jquery-1.12.0.min.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="vendors/jquery_mobile_flat_ui_theme/jquery.mobile.flatui.min.css"/>
|
||||
<script>
|
||||
//configuration
|
||||
var isMusicBox = '{{musicbox}}' == 'True'; // Remove MusicBox only content (e.g. settings, system pages)
|
||||
var websocketUrl = ('{{useWebsocketUrl}}' == 'True') ? '{{websocket_url}}' : ''
|
||||
var hasAlarmClock = '{{alarmclock}}' == 'True'; // Add Alarm Clock icons
|
||||
|
||||
$(document).bind("mobileinit", function () {
|
||||
$.extend($.mobile, {
|
||||
ajaxEnabled: false,
|
||||
hashListeningEnabled: false
|
||||
// linkBindingEnabled: false
|
||||
// buttonMarkup.hoverDelay: 100,
|
||||
// buttonMarkup.corners: false
|
||||
});
|
||||
});
|
||||
/* window.addEventListener('load', function () {
|
||||
new FastClick(document.body);
|
||||
}, false);
|
||||
*/
|
||||
</script>
|
||||
<script type="text/javascript" src="vendors/jquery/jquery-1.12.0.min.js"></script>
|
||||
<script type="text/javascript" src="vendors/jquery_cookie/jquery.cookie.js"></script>
|
||||
<script type="text/javascript" src="js/custom_scripting.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="vendors/jquery_mobile_flat_ui_theme/jquery.mobile.flatui.min.css"/>
|
||||
<link rel="icon" type="image/gif" href="images/icons/musicbox32.gif" />
|
||||
<link rel="apple-touch-icon" href="images/icons/musicbox57.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="images/icons/musicbox72.png" />
|
||||
@ -52,11 +36,10 @@
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body data-websocket-url="{{websocketUrl}}" data-is-musicbox="{{isMusicBox}}" data-has-alarmclock="{{hasAlarmClock}}" data-on-track-click="{{onTrackClick}}">
|
||||
<div data-role="page" id="page" class="ui-responsive-panel" data-theme="c">
|
||||
<div data-role="panel" id="panel" data-position="left" data-theme="a" data-display="reveal" data-position-fixed="true">
|
||||
|
||||
|
||||
<ul class="ui-listview mainNav" data-role="listview" data-theme="a">
|
||||
<li id="navhome" data-icon="false">
|
||||
<a href="#home" onclick="switchContent('home' ); return false;">
|
||||
@ -76,11 +59,11 @@
|
||||
</li>
|
||||
<li id="navbrowse" data-icon="false">
|
||||
<a href="#browse" onclick="switchContent('browse' ); return false;">
|
||||
<span class="navtxt"> Browse </span><i class="fa fa-folder"></i></a>
|
||||
<span class="navtxt"> Browse </span><i class="fa fa-folder"></i></a>
|
||||
</li>
|
||||
<li id="navstream" data-icon="false">
|
||||
<a href="#stream" onclick="switchContent('stream' ); return false;">
|
||||
<span class="navtxt">Streams </span><i class="fa fa-rss"></i></a>
|
||||
<span class="navtxt">Streams </span><i class="fa fa-rss"></i></a>
|
||||
</li>
|
||||
<li id="navsearch" data-icon="false">
|
||||
<a href="#search" onclick="switchContent('search' ); return false;">
|
||||
@ -104,7 +87,7 @@
|
||||
</li>
|
||||
<li data-icon="false">
|
||||
<div><!-- slider for volume -->
|
||||
<a href="#" onclick="doMute(); return false;"><span title="Toggle mute"><i id="mutebt" class="fa fa-volume-up"></i></span></a>
|
||||
<a href="#" onclick="controls.doMute(); return false;"><span title="Toggle mute"><i id="mutebt" class="fa fa-volume-up"></i></span></a>
|
||||
<label for="volumeslider" class="ui-hidden-accessible">Volume</label>
|
||||
<input id="volumeslider" data-highlight="true" name="volumeslider" data-mini="true" type="range" min="0"
|
||||
value="0" max="100"/>
|
||||
@ -129,48 +112,28 @@
|
||||
<a href="#" onclick="closePopups();"><img id="artistpopupimage" src="" alt="Album artist"/></a>
|
||||
</div>
|
||||
|
||||
<div data-role="popup" data-transition="none" data-theme="c" id="popupBrowse">
|
||||
<div data-role="collapsible-set">
|
||||
<ul data-role="listview" data-icon="false" id="popupBrowseLv">
|
||||
<li>
|
||||
<a href="#" onclick="return playBrowsedTracks(PLAY_ALL);">Play All</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" onclick="return playBrowsedTracks(PLAY_NOW);">Play <span class="popupTrackName"></span></a>
|
||||
</li>
|
||||
<li class="addqueue">
|
||||
<a href="#" onclick="return playBrowsedTracks(PLAY_NEXT);">Play Track Next</a>
|
||||
</li>
|
||||
<li class="addqueue">
|
||||
<a href="#" onclick="return playBrowsedTracks(ADD_THIS_BOTTOM);">Add Track to Bottom of Queue</a>
|
||||
</li>
|
||||
<li class="addqueue">
|
||||
<a href="#" onclick="return playBrowsedTracks(ADD_ALL_BOTTOM);">Add All to Bottom of Queue</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-role="popup" data-transition="none" data-theme="c" id="popupTracks">
|
||||
<div data-role="popup" data-transition="none" data-theme="b" id="popupTracks">
|
||||
<div data-role="collapsible-set">
|
||||
<ul data-role="listview" data-icon="false" id="popupTracksLv">
|
||||
<li>
|
||||
<a href="#" onclick="return playTrack(PLAY_NOW);">Play <span class="popupTrackName"></span></a>
|
||||
<li data-icon="play">
|
||||
<a href="#" onclick="return controls.playTracks(PLAY_NOW, mopidy);">Play <span class="popupTrackName"></span></a>
|
||||
</li>
|
||||
<li class="addqueue">
|
||||
<a href="#" onclick="return playTrack(PLAY_NEXT);">Play Track Next</a>
|
||||
<li data-icon="playAll" data-iconshadow="false">
|
||||
<a href="#" onclick="return controls.playTracks(PLAY_ALL, mopidy);">Play All</a>
|
||||
</li>
|
||||
<li class="addqueue">
|
||||
<a href="#" onclick="return playTrack(ADD_THIS_BOTTOM);">Add Track to Bottom of Queue</a>
|
||||
<li data-icon="playNext" class="addqueue">
|
||||
<a href="#" onclick="return controls.playTracks(PLAY_NEXT, mopidy);">Play Track Next</a>
|
||||
</li>
|
||||
<li class="addqueue">
|
||||
<a href="#" onclick="return playTrack(ADD_ALL_BOTTOM);" id="liaddtobottom">Add all to Bottom of
|
||||
Queue</a>
|
||||
<li data-icon="add" class="addqueue">
|
||||
<a href="#" onclick="return controls.playTracks(ADD_THIS_BOTTOM, mopidy);">Add Track to Bottom of Queue</a>
|
||||
</li>
|
||||
<li>
|
||||
<li data-icon="addAll" class="addqueue">
|
||||
<a href="#" onclick="return controls.playTracks(ADD_ALL_BOTTOM, mopidy);">Add All to Bottom of Queue</a>
|
||||
</li>
|
||||
<li class="popupAlbumLi">
|
||||
<a href="#" onclick="showAlbumPopup('#popupTracks')">Show Album <span class="popupAlbumName"></span></a>
|
||||
</li>
|
||||
<li id="popupArtistsLi">
|
||||
<li class="popupArtistsLi">
|
||||
<a href="#" onclick="showArtist()" class="popupArtistHref">Show Artist <span class="popupArtistName"></span>
|
||||
</a>
|
||||
</li>
|
||||
@ -182,19 +145,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-role="popup" data-transition="none" data-theme="c" id="popupQueue">
|
||||
<div data-role="popup" data-transition="none" data-theme="b" id="popupQueue">
|
||||
<div data-role="collapsible-set">
|
||||
<ul data-role="listview" data-icon="false" id="popupQueueLv">
|
||||
<li>
|
||||
<a href="#" onclick="return playTrackQueue();">Play <span class="popupTrackName"></span></a>
|
||||
<li data-icon="play">
|
||||
<a href="#" onclick="return controls.playQueueTrack();">Play <span class="popupTrackName"></span></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" onclick="return removeTrack();">Remove from Queue</a>
|
||||
<li data-icon="remove">
|
||||
<a href="#" onclick="return controls.removeTrack();">Remove from Queue</a>
|
||||
</li>
|
||||
<li>
|
||||
<li class="popupAlbumLi">
|
||||
<a href="#" onclick="showAlbumPopup('#popupQueue')">Show Album <span class="popupAlbumName"></span></a>
|
||||
</li>
|
||||
<li id="popupArtistsLi">
|
||||
<li class="popupArtistsLi">
|
||||
<a href="#" onclick="showArtist()" class="popupArtistHref">Show Artist <span class="popupArtistName"></span>
|
||||
</a>
|
||||
</li>
|
||||
@ -206,31 +169,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div data-role="popup" data-theme="b" id="popupSave" class="popupDialog">
|
||||
<form>
|
||||
<p>Save current queue to a playlist.
|
||||
<p>Save Current Queue to a Playlist
|
||||
<input id="saveinput" placeholder="Playlist name" class="span2" data-clear-btn="true"
|
||||
onkeypress="return savePressed(event.keyCode);" type="text"/>
|
||||
onkeypress="return controls.savePressed(event.keyCode);" type="text"/>
|
||||
<div data-role="controlgroup" data-type="horizontal" align="center">
|
||||
<button class="btn" type="button" onclick="return saveQueue();">
|
||||
Ok
|
||||
</button>
|
||||
<button class="btn" type="button" onclick="return $('#popupSave').popup('close');">
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn" type="button" onclick="return controls.saveQueue();">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!--/save queue to playlist-->
|
||||
|
||||
|
||||
<div data-role="popup" data-theme="b" id="popupOverwrite" class="popupDialog">
|
||||
<form>
|
||||
<p>Overwrite existing playlist with same name?
|
||||
<p>A playlist with the same name already exists. Overwrite?
|
||||
<div data-role="controlgroup" data-type="horizontal" align="center">
|
||||
<button class="btn" type="button" id="overwriteConfirmBtn">
|
||||
Ok
|
||||
Overwrite
|
||||
</button>
|
||||
<button class="btn" type="button" onclick="$('#popupOverwrite').popup('close'); return $('#popupSave').popup('open');">
|
||||
Cancel
|
||||
@ -239,6 +200,20 @@
|
||||
</form>
|
||||
</div><!--/overwrite existing playlist-->
|
||||
|
||||
<div data-role="popup" data-theme="b" id="popupConfirmDelete" class="popupDialog">
|
||||
<form>
|
||||
<p>Are you sure you want to remove <span class="popupStreamName"></span>?
|
||||
<div data-role="controlgroup" data-type="horizontal" align="center">
|
||||
<button class="btn" type="button" onclick="return controls.deleteFavourite();">
|
||||
Remove
|
||||
</button>
|
||||
<button class="btn" type="button" onclick="$('#popupConfirmDelete').popup('close');">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div><!--/confirm delete stream-->
|
||||
|
||||
<div data-role="header" data-tap-toggle="false" id="header" data-position="fixed" class="header-breakpoint headerbtn">
|
||||
<a id="headermenubtn" href="#panel"><i class="fa fa-align-justify"></i></a>
|
||||
<h1 id="contentHeadline">Musicbox</h1>
|
||||
@ -307,11 +282,9 @@
|
||||
</div>
|
||||
<!--/homepane-->
|
||||
|
||||
|
||||
<div id="nowPlayingpane" data-role="content" class="pane">
|
||||
|
||||
<img id="controlspopupimage" src="images/default_cover.png" alt="Album cover"/>
|
||||
|
||||
<img id="albumCoverImg" src="images/default_cover.png" alt="Album cover"/>
|
||||
|
||||
<div class="nowPlaying-artistInfo">
|
||||
<h3 id="modalname"></h3>
|
||||
@ -333,7 +306,7 @@
|
||||
<h4>Playlists</h4>
|
||||
</div>
|
||||
<div align="right" class="ui-block-b" data-role="controlgroup" data-type="horizontal">
|
||||
<button class="btn" type="button" title="Refresh playlists" onclick="return refreshPlaylists();">
|
||||
<button class="btn" type="button" title="Refresh playlists" onclick="return controls.refreshPlaylists();">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
</div>
|
||||
@ -343,9 +316,6 @@
|
||||
<ul id="playlistslist" class="table"></ul>
|
||||
</div>
|
||||
<div class="ui-block-b scroll" id="playlisttracksdiv">
|
||||
<div id="playlisttracksback" style="height: 30px; margin: 2px; padding-top: 2px; background-color: #aaa;">
|
||||
<a style="display:block; padding: 5px;" href="#" onclick="return togglePlaylists();"><i class="fa fa-arrow-circle-left"></i> Back</a>
|
||||
</div>
|
||||
<div>
|
||||
<ul class="table" id="playlisttracks"></ul>
|
||||
</div>
|
||||
@ -356,8 +326,9 @@
|
||||
|
||||
<div data-role="content" id="browsepane" class="pane">
|
||||
<h4>Browse</h4>
|
||||
<h5 id="browsepath"></h5>
|
||||
<ul id="browselist" class="table"></ul>
|
||||
<div class="ui-grid">
|
||||
<ul id="browsetable" class="table"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<!--/browsepane-->
|
||||
|
||||
@ -367,11 +338,11 @@
|
||||
<h4>Play Queue</h4>
|
||||
</div>
|
||||
<div align="right" class="ui-block-b" data-role="controlgroup" data-type="horizontal">
|
||||
<button class="btn" type="button" title="Save queue to playlist" onclick="return showSavePopup();">
|
||||
<i class="fa fa-floppy-o"></i>
|
||||
<button class="btn" type="button" title="Save queue to playlist" onclick="return controls.showSavePopup();">
|
||||
<i class="fa fa-bookmark-o"></i>
|
||||
</button>
|
||||
<button class="btn" type="button" title="Clear queue" onclick="return clearQueue();">
|
||||
<i class="fa fa-trash-o"></i>
|
||||
<button class="btn" type="button" title="Clear queue" onclick="return controls.clearQueue();">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -385,7 +356,7 @@
|
||||
|
||||
<h3 id="h_albumname"></h3>
|
||||
<h5 id="h_albumartist"></h5>
|
||||
<div id="albumstable"></div>
|
||||
<ul class="table" id="albumstable"></ul>
|
||||
|
||||
</div>
|
||||
|
||||
@ -403,14 +374,10 @@
|
||||
<div class="ui-block">
|
||||
<form>
|
||||
<p>Search for artists, albums, or specific tracks.
|
||||
<select id="selectSearchService">
|
||||
<!-- data-native-menu="false">
|
||||
multiple="multiple" data-native-menu="false">
|
||||
<option data-placeholder="true">Choose services</option> -->
|
||||
</select>
|
||||
<select id="selectSearchService"></select>
|
||||
<input id="searchinput" placeholder="Search term" class="span2" data-clear-btn="true"
|
||||
onkeypress="return searchPressed(event.keyCode);" type="text"/>
|
||||
<button class="btn" type="button" onclick="return initSearch(event.value);">
|
||||
onkeypress="return library.searchPressed(event.keyCode);" type="text"/>
|
||||
<button class="btn" type="button" onclick="return library.initSearch(event.value);">
|
||||
Search!
|
||||
</button>
|
||||
</form>
|
||||
@ -454,18 +421,18 @@
|
||||
<div class="ui-block-a" style="padding: 5px">
|
||||
<form>
|
||||
<p>Play a specific stream/track and optionally save it to your favourites.
|
||||
<button class="btn" type="button" onclick="return getCurrentlyPlaying();">
|
||||
<button class="btn" type="button" onclick="return controls.getCurrentlyPlaying();">
|
||||
Get currently playing
|
||||
</button>
|
||||
<input id="streamuriinput" placeholder="URI" class="span2" data-clear-btn="true"
|
||||
onkeypress="return streamPressed(event.keyCode);" type="text"/>
|
||||
<button class="btn" type="button" onclick="return playStreamUri();">
|
||||
onkeypress="return controls.streamPressed(event.keyCode);" type="text"/>
|
||||
<button class="btn" type="button" onclick="return controls.playStreamUri();">
|
||||
Play
|
||||
</button>
|
||||
|
||||
<input id="streamnameinput" placeholder="Name" class="span2" data-clear-btn="true"
|
||||
onkeypress="return streamPressed(event.keyCode);" type="text"/>
|
||||
<button class="btn" type="button" onclick="return addFavourite();">
|
||||
onkeypress="return controls.streamPressed(event.keyCode);" type="text"/>
|
||||
<button class="btn" type="button" onclick="return controls.addFavourite();">
|
||||
Save
|
||||
</button>
|
||||
<br/>
|
||||
@ -477,7 +444,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div data-role="footer" data-tap-toggle="false" data-position="fixed" id="normalFooter">
|
||||
<div class="footerControls">
|
||||
<div class="songinfo" id="songinfo">
|
||||
@ -488,36 +454,34 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="playicon">
|
||||
<a href="#" onclick="doPlay(); return false"><span id="btplay" title="Play"><i class="fa fa-play"></i></span></a>
|
||||
<a href="#" onclick="controls.doPlay(); return false"><span id="btplay" title="Play"><i class="fa fa-play"></i></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-role="footer" data-tap-toggle="false" data-position="fixed" id="nowPlayingFooter">
|
||||
<div class="footerControls" style="padding-left: 10px;">
|
||||
<div style="float: left;">
|
||||
<a href="#" onclick="doPrevious(); return false"><span id="btprev" title="Previous"><i class="fa fa-fast-backward"></i></span></a>
|
||||
<a href="#" onclick="doPlay(); return false"><span id="btplayNowPlaying" title="Play"><i class="fa fa-play"></i></span></a>
|
||||
<a href="#" onclick="doNext(); return false"><span id="btnext" title="Next"><i class="fa fa-fast-forward"></i></span></a>
|
||||
<a href="#" onclick="controls.doPrevious(); return false"><span id="btprev" title="Previous"><i class="fa fa-fast-backward"></i></span></a>
|
||||
<a href="#" onclick="controls.doPlay(); return false"><span id="btplayNowPlaying" title="Play"><i class="fa fa-play"></i></span></a>
|
||||
<a href="#" onclick="controls.doNext(); return false"><span id="btnext" title="Next"><i class="fa fa-fast-forward"></i></span></a>
|
||||
</div>
|
||||
<div style="float: right; margin-right: 10px;">
|
||||
<a href="#" onclick="doRandom(); return false"><span id="randombt" title="Random"><i class="fa fa-random"></i></span></a>
|
||||
<a href="#" onclick="doRepeat(); return false"><span id="repeatbt" title="Repeat"><i class="fa fa-repeat"></i></span></a>
|
||||
<a href="#" onclick="doConsume(); return false"><span id="consumebt" title="Consume"><i class="fa fa-cutlery"></i></span></a>
|
||||
<a href="#" onclick="doSingle(); return false"><span id="singlebt" title="Single"><i class="fa fa-dot-circle-o"></i></span></a>
|
||||
<a href="#" onclick="doShuffle(); return false"><span id="shufflebt" title="Shuffle"><i class="fa fa-arrows-v"></i></span></a>
|
||||
<a href="#" onclick="controls.doRandom(); return false"><span id="randombt" title="Random"><i class="fa fa-random"></i></span></a>
|
||||
<a href="#" onclick="controls.doRepeat(); return false"><span id="repeatbt" title="Repeat"><i class="fa fa-repeat"></i></span></a>
|
||||
<a href="#" onclick="controls.doConsume(); return false"><span id="consumebt" title="Consume"><i class="fa fa-cutlery"></i></span></a>
|
||||
<a href="#" onclick="controls.doSingle(); return false"><span id="singlebt" title="Single"><i class="fa fa-dot-circle-o"></i></span></a>
|
||||
<a href="#" onclick="controls.doShuffle(); return false"><span id="shufflebt" title="Shuffle"><i class="fa fa-arrows-v"></i></span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- /footer -->
|
||||
|
||||
|
||||
</div>
|
||||
<!-- /page one -->
|
||||
<script type="text/javascript" src="vendors/mopidy/mopidy.min.js"></script>
|
||||
<script type="text/javascript" src="vendors/jquery_cookie/jquery.cookie.js"></script>
|
||||
<script type="text/javascript" src="vendors/media_progress_timer/timer.js"></script>
|
||||
<script type="text/javascript" src="js/progress_timer.js"></script>
|
||||
<script type="text/javascript" src="js/synced_timer.js"></script>
|
||||
<script type="text/javascript" src="js/controls.js"></script>
|
||||
<script type="text/javascript" src="js/library.js"></script>
|
||||
<script type="text/javascript" src="js/functionsvars.js"></script>
|
||||
|
||||
1261
mopidy_musicbox_webclient/static/js/controls.js
vendored
1261
mopidy_musicbox_webclient/static/js/controls.js
vendored
File diff suppressed because it is too large
Load Diff
25
mopidy_musicbox_webclient/static/js/custom_scripting.js
Normal file
25
mopidy_musicbox_webclient/static/js/custom_scripting.js
Normal file
@ -0,0 +1,25 @@
|
||||
// jQuery Mobile configuration options
|
||||
// see: http://api.jquerymobile.com/1.3/global-config/
|
||||
(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
|
||||
}))
|
||||
|
||||
@ -4,79 +4,79 @@
|
||||
* all kinds functions and vars
|
||||
*/
|
||||
|
||||
var mopidy;
|
||||
var mopidy
|
||||
var syncedProgressTimer
|
||||
|
||||
//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 = {} // TODO: Refactor into one shared cache
|
||||
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 = {} // TODO: Refactor into one shared cache
|
||||
var currentplaylist
|
||||
var customTracklists = [] // TODO: Refactor into one shared cache
|
||||
|
||||
var browseStack = [];
|
||||
var browseTracks = [];
|
||||
var browseStack = []
|
||||
|
||||
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';
|
||||
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'
|
||||
BROWSE_TABLE = '#browsetable'
|
||||
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;
|
||||
|
||||
MAX_TABLEROWS = 50;
|
||||
PLAY_NOW = 0
|
||||
PLAY_NEXT = 1
|
||||
ADD_THIS_BOTTOM = 2
|
||||
ADD_ALL_BOTTOM = 3
|
||||
PLAY_ALL = 4
|
||||
DYNAMIC = 5
|
||||
|
||||
// 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'],
|
||||
['spotifytunigo', 'fa-spotify'],
|
||||
['local', 'fa-file-sound-o'],
|
||||
['file', 'fa-folder-o'],
|
||||
['m3u', 'fa-file-sound-o'],
|
||||
['podcast', 'fa-rss-square'],
|
||||
['dirble', 'fa-microphone'],
|
||||
@ -90,13 +90,13 @@ var uriClassList = [
|
||||
['yt', 'fa-youtube'],
|
||||
['audioaddict', 'fa-bullhorn'],
|
||||
['subsonic', 'fa-folder-open']
|
||||
];
|
||||
]
|
||||
|
||||
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'],
|
||||
@ -107,441 +107,464 @@ var uriHumanList = [
|
||||
['youtube', 'YouTube'],
|
||||
['audioaddict', 'AudioAddict'],
|
||||
['subsonic', 'Subsonic']
|
||||
];
|
||||
]
|
||||
|
||||
function scrollToTop() {
|
||||
var divtop = 0;
|
||||
// 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({
|
||||
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 = '<ul class="songsOfAlbum table" >';
|
||||
var liId = '';
|
||||
var targetmin = target.substr(1);
|
||||
$(target).empty();
|
||||
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++) {
|
||||
popupData[pl[i].uri] = pl[i];
|
||||
liID = targetmin + '-' + pl[i].uri;
|
||||
tmp += renderSongLi(pl[i], liID, uri);
|
||||
previousTrack = track || undefined
|
||||
nextTrack = i < pl.length - 1 ? pl[i + 1] : undefined
|
||||
track = pl[i]
|
||||
popupData[track.uri] = track
|
||||
html += renderSongLi(previousTrack, track, nextTrack, uri, '', target, i, pl.length)
|
||||
}
|
||||
tmp += '</ul>';
|
||||
$(target).html(tmp);
|
||||
$(target).attr('data', uri);
|
||||
$(target).append(html)
|
||||
updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction())
|
||||
}
|
||||
|
||||
function renderSongLi(song, liID, uri) {
|
||||
var name;
|
||||
if (!song.name || song.name === '') {
|
||||
name = uri.split('/');
|
||||
name = decodeURI(name[name.length - 1]);
|
||||
function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, currentIndex, listLength) {
|
||||
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 html
|
||||
}
|
||||
// Streams
|
||||
if (track.length === -1) {
|
||||
html += '<li class="albumli"><a href="#"><h1><i class="' + getMediaClass(track.uri) + '"></i> ' + track.name + ' [Stream]</h1></a></li>'
|
||||
return html
|
||||
}
|
||||
|
||||
if (target === CURRENT_PLAYLIST_TABLE && typeof tlid === 'number' && tlid >= 0) { // Current queue: Show popup menu icon. onClick plays track.
|
||||
tlidParameter = '\',\'' + tlid
|
||||
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 + '\');'
|
||||
}
|
||||
|
||||
html +=
|
||||
'<li class="song albumli" id="' + getjQueryID(target, track.uri) + '" tlid="' + tlid + '">' +
|
||||
'<a href="#" class="moreBtn" onclick="return popupTracks(event, \'' + uri + '\',\'' + track.uri + tlidParameter + '\');">' +
|
||||
'<i class="fa fa-play-circle-o"></i></a>' +
|
||||
'<a href="#" onclick="' + onClick + '"><h1><i class="' + getMediaClass(track.uri) + '"></i> ' + track.name + '</h1>'
|
||||
|
||||
if (listLength === 1 || (!hasSameAlbum(previousTrack, track) && !hasSameAlbum(track, nextTrack))) {
|
||||
html += renderSongLiAlbumInfo(track)
|
||||
}
|
||||
html += '</a></li>'
|
||||
return html
|
||||
}
|
||||
|
||||
/* Tracklist renderer for track artist and album name. */
|
||||
function renderSongLiAlbumInfo (track, target) {
|
||||
var html = renderSongLiTrackArtists(track)
|
||||
if (track.album && track.album.name) {
|
||||
html += ' - <em>' + track.album.name + '</em></p>'
|
||||
}
|
||||
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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
/* Tracklist renderer to insert dividers between albums. */
|
||||
function renderSongLiDivider (previousTrack, track, nextTrack, target) {
|
||||
var html = ''
|
||||
// Render differently if part of an album.
|
||||
if (!hasSameAlbum(previousTrack, track) && hasSameAlbum(track, nextTrack)) {
|
||||
// Large divider with album cover.
|
||||
html +=
|
||||
'<li class="albumdivider"><a href="#" onclick="return library.showAlbum(\'' + track.album.uri + '\');">' +
|
||||
'<img id="' + getjQueryID(target + '-cover', track.uri) + '" class="artistcover" width="30" height="30"/>' +
|
||||
'<h1><i class="' + getMediaClass(track.uri) + '"></i> ' + track.album.name + '</h1><p>' +
|
||||
renderSongLiTrackArtists(track) + '</p></a></li>'
|
||||
// Retrieve album covers
|
||||
images.setAlbumImage(track.uri, getjQueryID(target + '-cover', track.uri, true), mopidy, 'small')
|
||||
} else if (previousTrack && !hasSameAlbum(previousTrack, track)) {
|
||||
// Small divider
|
||||
html += '<li class="smalldivider"> </li>'
|
||||
}
|
||||
if (typeof target !== 'undefined' && target.length > 0) {
|
||||
target = getjQueryID(target, track.uri, true)
|
||||
$(target).before(html)
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
function renderSongLiBackButton (results, target, onClick, optional) {
|
||||
if (onClick && onClick.length > 0) {
|
||||
if (!results || results.length === 0) {
|
||||
$(target).empty()
|
||||
$(target).append(
|
||||
'<li class="song albumli"><a href="#" onclick="' + onClick + '"><h1><i></i>No tracks found...</h1></a></li>'
|
||||
)
|
||||
}
|
||||
var opt = ''
|
||||
if (optional) {
|
||||
opt = ' backnav-optional'
|
||||
}
|
||||
$(target).prepend(
|
||||
'<li class="backnav' + opt + '"><a href="#" onclick="' + onClick + '"><h1><i class="fa fa-arrow-circle-left"></i> Back</h1></a></li>'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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 = song.name;
|
||||
name = track.name
|
||||
}
|
||||
songLi = '<li class="song albumli" id="' + liID + '">' +
|
||||
'<a href="#" class="moreBtn" onclick="return popupTracks(event, \'' + uri + '\',\'' + song.uri + '\');">' +
|
||||
'<i class="fa fa-ellipsis-v"></i></a>' +
|
||||
'<a href="#" onclick="return playTrackByUri(\'' + song.uri + '\',\'' + uri + '\');">' +
|
||||
'<h1 class="trackname">' + name + '</h1></a>' +
|
||||
'</li>';
|
||||
return songLi;
|
||||
return name
|
||||
}
|
||||
|
||||
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 resultsToTables (results, target, uri, onClickBack, backIsOptional) {
|
||||
$(target).empty()
|
||||
renderSongLiBackButton(results, target, onClickBack, backIsOptional)
|
||||
if (!results || results.length === 0) {
|
||||
return
|
||||
}
|
||||
songLi = '<li class="song albumli" id="' + liID + '" tlid="' + tlid + '">' +
|
||||
'<a href="#" class="moreBtn" onclick="return popupTracks(event, \'' + uri + '\',\'' + song.uri + '\',\'' + tlid + '\');">' +
|
||||
'<i class="fa fa-ellipsis-v"></i></a>' +
|
||||
'<a href="#" onclick="return playTrackQueueByTlid(\'' + song.uri + '\',\'' + tlid + '\');">' +
|
||||
'<h1 class="trackname">' + name + '</h1></a>' +
|
||||
'</li>';
|
||||
return songLi;
|
||||
$(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++) {
|
||||
previousTrack = track || undefined
|
||||
nextTrack = i < results.length - 1 ? results[i + 1] : undefined
|
||||
track = results[i]
|
||||
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
|
||||
html += renderSongLiDivider(previousTrack, track, nextTrack, target)
|
||||
html += renderSongLi(previousTrack, track, nextTrack, uri, tlid, target, i, results.length)
|
||||
}
|
||||
}
|
||||
$(target).append(html)
|
||||
updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction())
|
||||
}
|
||||
|
||||
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 length = 0 || results.length;
|
||||
for (i = 0; i < length; i++) {
|
||||
//create album if none extists
|
||||
if (!results[i].album) {
|
||||
results[i].album = {"__model__": "Album"};
|
||||
}
|
||||
//create album uri if there is none
|
||||
if (!results[i].album.uri) {
|
||||
results[i].album.uri = 'x';
|
||||
}
|
||||
if (!results[i].album.name) {
|
||||
results[i].album.name = '';
|
||||
}
|
||||
//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);
|
||||
}
|
||||
|
||||
//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 += '<li class="albumli"><a href="#"><h1><i class="' + iconClass + '"></i> ' + results[i].name + ' [Stream]</h1></a></li>';
|
||||
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) {
|
||||
if (i !== 0) {
|
||||
html += '<li class="smalldivider"> </li>';
|
||||
}
|
||||
iconClass = getMediaClass(newalbum[0].uri);
|
||||
var liID = targetmin + '-' + newalbum[0].uri;
|
||||
if (target == CURRENT_PLAYLIST_TABLE) {
|
||||
html += '<li class="song albumli" id="' + liID + '" tlid="' + newtlids[0] + '">' +
|
||||
'<a href="#" class="moreBtn" onclick="return popupTracks(event, \'' + uri + '\',\'' + newalbum[0].uri + '\',\'' + newtlids[0] + '\');">' +
|
||||
'<i class="fa fa-ellipsis-v"></i></a>' +
|
||||
'<a href="#" onclick="return playTrackQueueByTlid(\'' + newalbum[0].uri + '\',\'' + newtlids[0] + '\');">' +
|
||||
'<h1><i class="' + iconClass + '"></i> ' + newalbum[0].name + "</h1><p>";
|
||||
} else {
|
||||
html += '<li class="song albumli" id="' + liID + '">' +
|
||||
'<a href="#" class="moreBtn" onclick="return popupTracks(event, \'' + uri + '\',\'' + newalbum[0].uri + '\');">' +
|
||||
'<i class="fa fa-ellipsis-v"></i></a>' +
|
||||
'<a href="#" onclick="return playTrackByUri(\'' + newalbum[0].uri + '\',\'' + uri + '\');">' +
|
||||
'<h1><i class="' + iconClass + '"></i> ' + newalbum[0].name + "</h1><p>";
|
||||
}
|
||||
|
||||
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 += '<em>' + newalbum[0].album.name + '</em></p>';
|
||||
html += '</a></li>';
|
||||
|
||||
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 += '<li class="albumdivider">';
|
||||
html += '<a href="#" onclick="return showAlbum(\'' + results[i].album.uri + '\');"><img id="' +
|
||||
targetmin + '-cover-' + i + '" class="artistcover" width="30" height="30" /><h1><i class="' + iconClass + '"></i> ' + results[i].album.name + '</h1><p>';
|
||||
}
|
||||
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 += '</p></a></li>';
|
||||
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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
//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 = '<li id="' + targetmin + '-' + pl[i].uri + '"><a href="#" onclick="return popupTracks(event, \'' + uri + '\',\'' + pl[i].uri + '\');">';
|
||||
child += '<h1>' + pl[i].name + "</h1>";
|
||||
child += '<p>';
|
||||
child += '<span style="float: right;">' + timeFromSeconds(pl[i].length / 1000) + '</span>';
|
||||
popupData[pl[i].uri] = pl[i]
|
||||
child = '<li id="' + targetmin + '-' + pl[i].uri + '"><a href="#" onclick="return popupTracks(event, \'' + uri + '\',\'' + pl[i].uri + '\');">'
|
||||
child += '<h1>' + pl[i].name + 'h1>'
|
||||
child += '<p>'
|
||||
child += '<span style="float: right;">' + timeFromSeconds(pl[i].length / 1000) + '</span>'
|
||||
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 += ' / <em>' + pl[i].album.name + '</em></p>';
|
||||
child += '</a></li>';
|
||||
tmp += child;
|
||||
child += ' / <em>' + pl[i].album.name + '</em></p>'
|
||||
child += '</a></li>'
|
||||
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 ? 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];
|
||||
if (scheme === uriClassList[i][0]) {
|
||||
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];
|
||||
if (scheme.toLowerCase() === uriHumanList[i][0].toLowerCase()) {
|
||||
return uriHumanList[i][1]
|
||||
}
|
||||
}
|
||||
return '';
|
||||
return uri
|
||||
}
|
||||
|
||||
function isServiceUri(uri) {
|
||||
var scheme = getScheme(uri);
|
||||
for (var i = 0; i < uriClassList.length; i++) {
|
||||
if (scheme == uriClassList[i][0]) {
|
||||
return true;
|
||||
function isServiceUri (uri) {
|
||||
var scheme = getScheme(uri)
|
||||
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]) {
|
||||
return true;
|
||||
for (i = 0; i < radioExtensionsList.length; i++) {
|
||||
if (scheme === radioExtensionsList[i]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
function isFavouritesPlaylist(playlist) {
|
||||
return (playlist.name == STREAMS_PLAYLIST_NAME &&
|
||||
getScheme(playlist.uri) == STREAMS_PLAYLIST_SCHEME);
|
||||
function isFavouritesPlaylist (playlist) {
|
||||
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');
|
||||
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. Also accepts jQuery identifiers starting with '#'.
|
||||
*
|
||||
* @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) {
|
||||
if (identifier.charAt(0) === '#' && !includePrefix) {
|
||||
identifier = identifier.substr(1)
|
||||
} else if (identifier.charAt(0) !== '#' && includePrefix) {
|
||||
identifier = '#' + identifier
|
||||
}
|
||||
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
|
||||
function fixedEncodeURIComponent (str) {
|
||||
return encodeURIComponent(str).replace(/[!'()*]/g, function (c) {
|
||||
return '%' + c.charCodeAt(0).toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,92 +1,282 @@
|
||||
/**
|
||||
* @author Wouter van Wijk
|
||||
*/
|
||||
|
||||
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({
|
||||
apiKey : API_KEY,
|
||||
apiSecret : API_SECRET,
|
||||
cache : fmcache
|
||||
});
|
||||
});
|
||||
|
||||
function getCover(uri, images, size) {
|
||||
var defUrl = 'images/default_cover.png';
|
||||
$(images).attr('src', defUrl);
|
||||
if (!uri) {
|
||||
return;
|
||||
(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define([], factory)
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory()
|
||||
} else {
|
||||
root.images = factory()
|
||||
}
|
||||
}(this, function () {
|
||||
'use strict'
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
var API_KEY = 'b6d34c3af91d62ab0ae00ab1b6fa8733'
|
||||
var API_SECRET = '2c631802c2285d5d5d1502462fe42a2b'
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
var images = {
|
||||
|
||||
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;
|
||||
}
|
||||
DEFAULT_ALBUM_URL: 'images/default_cover.png',
|
||||
DEFAULT_ARTIST_URL: 'images/user_24x32.png',
|
||||
|
||||
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);
|
||||
lastFM: new LastFM({
|
||||
apiKey: API_KEY,
|
||||
apiSecret: API_SECRET,
|
||||
cache: new LastFMCache()
|
||||
}),
|
||||
|
||||
/* 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) {
|
||||
$(img_element).attr('src', imageResults[uri][0].uri)
|
||||
} else {
|
||||
// Also check deprecated 'album.images' in case backend does not
|
||||
// implement mopidy.library.getImages yet...
|
||||
images._setDeprecatedAlbumImage(uri, img_element, mopidy, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
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);
|
||||
// Note that this approach has been deprecated in Mopidy
|
||||
// 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) {
|
||||
$(img_element).attr('src', track.album.images[0])
|
||||
} else {
|
||||
// Fallback to last.fm
|
||||
images._setLastFmAlbumImage(track, img_element, size)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/* 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
|
||||
}
|
||||
size = size || 'extralarge'
|
||||
|
||||
images._getLastFmAlbumInfo(track).then(function (data) {
|
||||
for (var i = 0; i < data.album.image.length; i++) {
|
||||
if (data.album.image[i].size === size) {
|
||||
$(img_element).attr('src', data.album.image[i]['#text'] || images.DEFAULT_ALBUM_URL)
|
||||
break
|
||||
}
|
||||
}
|
||||
}, function (code, message) {
|
||||
$(img_element).attr('src', images.DEFAULT_ALBUM_URL)
|
||||
console.error('Error getting album info from last.fm (%s: %s)', code, message)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
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) {
|
||||
$(img_element).attr('src', data.artist.image[i]['#text'] || images.DEFAULT_ARTIST_URL)
|
||||
break
|
||||
}
|
||||
}
|
||||
}, error: function (code, message) {
|
||||
$(img_element).attr('src', images.DEFAULT_ARTIST_URL)
|
||||
console.error('Error retrieving artist info from last.fm. (%s: %s)', code, message)
|
||||
}})
|
||||
}
|
||||
}});
|
||||
}
|
||||
}
|
||||
return images
|
||||
}))
|
||||
|
||||
@ -1,344 +1,349 @@
|
||||
/*********************************
|
||||
* Search
|
||||
*********************************/
|
||||
function searchPressed(key) {
|
||||
var value = $('#searchinput').val();
|
||||
switchContent('search');
|
||||
|
||||
if (key == 13) {
|
||||
initSearch();
|
||||
return false;
|
||||
(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()
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}(this, function () {
|
||||
'use strict'
|
||||
|
||||
//init search
|
||||
function initSearch() {
|
||||
var value = $('#searchinput').val();
|
||||
var searchService = $('#selectSearchService').val();
|
||||
var library = {
|
||||
|
||||
if ((value.length < 100) && (value.length > 0)) {
|
||||
showLoading(true);
|
||||
//hide ios/android keyboard
|
||||
document.activeElement.blur();
|
||||
$("input").blur();
|
||||
/** *******************************
|
||||
* Search
|
||||
*********************************/
|
||||
searchPressed: function (key) {
|
||||
var value = $('#searchinput').val()
|
||||
switchContent('search')
|
||||
|
||||
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 (key === 13) {
|
||||
library.initSearch()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
if (searchService != 'all') {
|
||||
mopidy.library.search({'query': {any:[value]}, 'uris': [searchService + ':']}).then(processSearchResults, console.error);
|
||||
} else {
|
||||
mopidy.getUriSchemes().then(function (schemes) {
|
||||
var query = {},
|
||||
uris = [];
|
||||
// init search
|
||||
initSearch: function () {
|
||||
var value = $('#searchinput').val()
|
||||
var searchService = $('#selectSearchService').val()
|
||||
$.cookie('searchScheme', searchService, { expires: 365 })
|
||||
|
||||
var regexp = $.map(schemes, function (scheme) {
|
||||
return '^' + scheme + ':';
|
||||
}).join('|');
|
||||
if ((value.length < 100) && (value.length > 0)) {
|
||||
showLoading(true)
|
||||
// hide ios/android keyboard
|
||||
document.activeElement.blur()
|
||||
$('input').blur()
|
||||
|
||||
var match = value.match(regexp);
|
||||
if (match) {
|
||||
var scheme = match[0];
|
||||
query = {uri: [value]};
|
||||
uris = [scheme];
|
||||
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 {
|
||||
query = {any: [value]};
|
||||
}
|
||||
mopidy.library.search({'query': query, 'uris': uris}).then(processSearchResults, console.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
mopidy.getUriSchemes().then(function (schemes) {
|
||||
var query = {}
|
||||
var uris = []
|
||||
|
||||
/********************************************************
|
||||
* process results of a search
|
||||
*********************************************************/
|
||||
var regexp = $.map(schemes, function (scheme) {
|
||||
return '^' + scheme + ':'
|
||||
}).join('|')
|
||||
|
||||
//# 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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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) {
|
||||
toast('No results');
|
||||
showLoading(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (results.artists.length > 0) {
|
||||
$("#searchartists").show();
|
||||
}
|
||||
|
||||
if (results.albums.length > 0) {
|
||||
$("#searchalbums").show();
|
||||
}
|
||||
|
||||
if (results.tracks.length > 0) {
|
||||
$("#searchtracks").show();
|
||||
}
|
||||
|
||||
// Returns a string where {x} in template is replaced by tokens[x].
|
||||
function theme(template, tokens) {
|
||||
return template.replace(/{[^}]+}/g, function(match) {
|
||||
return tokens[match.slice(1,-1)];
|
||||
});
|
||||
}
|
||||
|
||||
// 'Show more' pattern
|
||||
var showMorePattern = '<li onclick="$(this).hide().siblings().show(); return false;"><a>Show {count} more</a></li>';
|
||||
|
||||
// Artist results
|
||||
var child = '';
|
||||
var pattern = '<li><a href="#" onclick="return showArtist(this.id)" id={id}><i class="{class}"></i> <strong>{name}</strong></a></li>';
|
||||
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('<li>', '<li class="overflow">');
|
||||
}
|
||||
|
||||
child += theme(pattern, tokens);
|
||||
}
|
||||
|
||||
// Inject list items, refresh listview and hide superfluous items.
|
||||
$(SEARCH_ARTIST_TABLE).html(child).listview('refresh').find('.overflow').hide();
|
||||
|
||||
// Album results
|
||||
child = '';
|
||||
pattern = '<li><a href="#" onclick="return showAlbum(this.id)" id="{albumId}">';
|
||||
pattern += '<h5 data-role="heading"><i class="{class}"></i> {albumName}</h5>';
|
||||
pattern += '<p data-role="desc">{artistName}</p>';
|
||||
pattern += '</a></li>';
|
||||
|
||||
for (var 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 (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 + ' ';
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tokens.albumYear) {
|
||||
tokens.artistName += '(' + tokens.albumYear + ')';
|
||||
}
|
||||
// Add 'Show all' item after a certain number of hits.
|
||||
if (i == 4 && results.albums.length > 5) {
|
||||
child += theme(showMorePattern, {'count': results.albums.length - i});
|
||||
pattern = pattern.replace('<li>', '<li class="overflow">');
|
||||
}
|
||||
},
|
||||
|
||||
child += theme(pattern, tokens);
|
||||
}
|
||||
// Inject list items, refresh listview and hide superfluous items.
|
||||
$(SEARCH_ALBUM_TABLE).html(child).listview('refresh').find('.overflow').hide();
|
||||
/** ******************************************************
|
||||
* process results of a search
|
||||
*********************************************************/
|
||||
processSearchResults: function (resultArr) {
|
||||
$(SEARCH_TRACK_TABLE).empty()
|
||||
$(SEARCH_ARTIST_TABLE).empty()
|
||||
$(SEARCH_ALBUM_TABLE).empty()
|
||||
|
||||
$('#expandsearch').show();
|
||||
// Merge results from different backends.
|
||||
// TODO should of coures have multiple tables
|
||||
var results = {'tracks': [], 'artists': [], 'albums': []}
|
||||
var i, j
|
||||
var emptyResult = true
|
||||
|
||||
// Track results
|
||||
resultsToTables(results.tracks, SEARCH_TRACK_TABLE, URI_SCHEME+':trackresultscache');
|
||||
|
||||
showLoading(false);
|
||||
}
|
||||
|
||||
function toggleSearch() {
|
||||
$("#albumresulttable tr").removeClass('hidden');
|
||||
$("#artistresulttable tr").removeClass('hidden');
|
||||
}
|
||||
|
||||
/*********************************
|
||||
* Playlists & Browse
|
||||
*********************************/
|
||||
|
||||
function getPlaylists() {
|
||||
// get playlists without tracks
|
||||
mopidy.playlists.asList().then(processGetPlaylists, 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);
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
* 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) {
|
||||
$(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');
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
/******
|
||||
* 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 {
|
||||
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;
|
||||
}
|
||||
|
||||
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]));
|
||||
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].albums) {
|
||||
for (j = 0; j < resultArr[i].albums.length; j++) {
|
||||
results.albums.push(resultArr[i].albums[j])
|
||||
emptyResult = false
|
||||
}
|
||||
}
|
||||
}
|
||||
$("#selectSearchService").selectmenu( "refresh", true );
|
||||
}, console.error
|
||||
);
|
||||
}
|
||||
|
||||
customTracklists[URI_SCHEME + ':trackresultscache'] = results.tracks
|
||||
|
||||
if (emptyResult) {
|
||||
$('#searchtracks').show()
|
||||
$(SEARCH_TRACK_TABLE).append(
|
||||
'<li class="song albumli"><a href="#"><h1><i></i>No tracks found...</h1></a></li>'
|
||||
)
|
||||
toast('No results')
|
||||
showLoading(false)
|
||||
return false
|
||||
}
|
||||
|
||||
if (results.artists.length > 0) {
|
||||
$('#searchartists').show()
|
||||
}
|
||||
|
||||
if (results.albums.length > 0) {
|
||||
$('#searchalbums').show()
|
||||
}
|
||||
|
||||
if (results.tracks.length > 0) {
|
||||
$('#searchtracks').show()
|
||||
}
|
||||
|
||||
// Returns a string where {x} in template is replaced by tokens[x].
|
||||
function theme (template, tokens) {
|
||||
return template.replace(/{[^}]+}/g, function (match) {
|
||||
return tokens[match.slice(1, -1)]
|
||||
})
|
||||
}
|
||||
|
||||
// 'Show more' pattern
|
||||
var showMorePattern = '<li onclick="$(this).hide().siblings().show(); return false;"><a>Show {count} more</a></li>'
|
||||
|
||||
// Artist results
|
||||
var child = ''
|
||||
var pattern = '<li><a href="#" onclick="return library.showArtist(this.id)" id={id}><i class="{class}"></i> <strong>{name}</strong></a></li>'
|
||||
var tokens
|
||||
|
||||
for (i = 0; i < results.artists.length; i++) {
|
||||
tokens = {
|
||||
'id': results.artists[i].uri,
|
||||
'name': results.artists[i].name,
|
||||
'class': getMediaClass(results.artists[i].uri)
|
||||
}
|
||||
|
||||
// Add 'Show all' item after a certain number of hits.
|
||||
if (i === 4 && results.artists.length > 5) {
|
||||
child += theme(showMorePattern, {'count': results.artists.length - i})
|
||||
pattern = pattern.replace('<li>', '<li class="overflow">')
|
||||
}
|
||||
|
||||
child += theme(pattern, tokens)
|
||||
}
|
||||
|
||||
// Inject list items, refresh listview and hide superfluous items.
|
||||
$(SEARCH_ARTIST_TABLE).html(child).listview('refresh').find('.overflow').hide()
|
||||
|
||||
// Album results
|
||||
child = ''
|
||||
pattern = '<li><a href="#" onclick="return library.showAlbum(this.id)" id="{albumId}">'
|
||||
pattern += '<h5 data-role="heading"><i class="{class}"></i> {albumName}</h5>'
|
||||
pattern += '<p data-role="desc">{artistName}</p>'
|
||||
pattern += '</a></li>'
|
||||
|
||||
for (i = 0; i < results.albums.length; i++) {
|
||||
tokens = {
|
||||
'albumId': results.albums[i].uri,
|
||||
'albumName': results.albums[i].name,
|
||||
'artistName': '',
|
||||
'albumYear': results.albums[i].date,
|
||||
'class': getMediaClass(results.albums[i].uri)
|
||||
}
|
||||
if (results.albums[i].artists) {
|
||||
for (j = 0; j < results.albums[i].artists.length; j++) {
|
||||
if (results.albums[i].artists[j].name) {
|
||||
tokens.artistName += results.albums[i].artists[j].name + ' '
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tokens.albumYear) {
|
||||
tokens.artistName += '(' + tokens.albumYear + ')'
|
||||
}
|
||||
// Add 'Show all' item after a certain number of hits.
|
||||
if (i === 4 && results.albums.length > 5) {
|
||||
child += theme(showMorePattern, {'count': results.albums.length - i})
|
||||
pattern = pattern.replace('<li>', '<li class="overflow">')
|
||||
}
|
||||
|
||||
child += theme(pattern, tokens)
|
||||
}
|
||||
// Inject list items, refresh listview and hide superfluous items.
|
||||
$(SEARCH_ALBUM_TABLE).html(child).listview('refresh').find('.overflow').hide()
|
||||
|
||||
// Track results
|
||||
resultsToTables(results.tracks, SEARCH_TRACK_TABLE, URI_SCHEME + ':trackresultscache')
|
||||
|
||||
showLoading(false)
|
||||
},
|
||||
|
||||
/** *******************************
|
||||
* Playlists & Browse
|
||||
*********************************/
|
||||
getPlaylists: function () {
|
||||
// get playlists without tracks
|
||||
mopidy.playlists.asList().then(processGetPlaylists, console.error)
|
||||
},
|
||||
|
||||
getBrowseDir: function (rootdir) {
|
||||
// get directory to browse
|
||||
showLoading(true)
|
||||
if (!rootdir) {
|
||||
browseStack.pop()
|
||||
rootdir = browseStack[browseStack.length - 1]
|
||||
} else {
|
||||
browseStack.push(rootdir)
|
||||
}
|
||||
if (!rootdir) {
|
||||
rootdir = null
|
||||
}
|
||||
mopidy.library.browse({'uri': rootdir}).then(processBrowseDir, console.error)
|
||||
},
|
||||
|
||||
getCurrentPlaylist: function () {
|
||||
mopidy.tracklist.getTlTracks().then(processCurrentPlaylist, console.error)
|
||||
},
|
||||
|
||||
/** ******************************************************
|
||||
* Show tracks of playlist
|
||||
********************************************************/
|
||||
togglePlaylists: function () {
|
||||
if ($(window).width() <= 960) {
|
||||
$('#playlisttracksdiv').toggle();
|
||||
// Hide other div
|
||||
($('#playlisttracksdiv').is(':visible')) ? $('#playlistslistdiv').hide() : $('#playlistslistdiv').show()
|
||||
} else {
|
||||
$('#playlisttracksdiv').show()
|
||||
$('#playlistslistdiv').show()
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
/** **********
|
||||
* Lookups
|
||||
************/
|
||||
showTracklist: function (uri) {
|
||||
showLoading(true)
|
||||
$(PLAYLIST_TABLE).empty()
|
||||
library.togglePlaylists()
|
||||
var tracks = getPlaylistTracks(uri).then(function (tracks) {
|
||||
resultsToTables(tracks, PLAYLIST_TABLE, uri, 'return library.togglePlaylists();', true)
|
||||
showLoading(false)
|
||||
})
|
||||
updatePlayIcons(uri, '', controls.getIconForAction())
|
||||
$('#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)
|
||||
}
|
||||
}
|
||||
return library
|
||||
}))
|
||||
|
||||
@ -5,249 +5,250 @@
|
||||
*
|
||||
*/
|
||||
|
||||
/********************************************************
|
||||
/** ******************************************************
|
||||
* 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) {
|
||||
controls.setVolume(data)
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
/** ******************************************************
|
||||
* process results of mute
|
||||
*********************************************************/
|
||||
function processMute(data) {
|
||||
setMute(data);
|
||||
function processMute (data) {
|
||||
controls.setMute(data)
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
/** ******************************************************
|
||||
* process results of a repeat
|
||||
*********************************************************/
|
||||
function processRepeat(data) {
|
||||
setRepeat(data);
|
||||
function processRepeat (data) {
|
||||
controls.setRepeat(data)
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
/** ******************************************************
|
||||
* process results of random
|
||||
*********************************************************/
|
||||
function processRandom(data) {
|
||||
setRandom(data);
|
||||
function processRandom (data) {
|
||||
controls.setRandom(data)
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
/** ******************************************************
|
||||
* process results of consume
|
||||
*********************************************************/
|
||||
function processConsume(data) {
|
||||
setConsume(data);
|
||||
function processConsume (data) {
|
||||
controls.setConsume(data)
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
/** ******************************************************
|
||||
* process results of single
|
||||
*********************************************************/
|
||||
function processSingle(data) {
|
||||
setSingle(data);
|
||||
function processSingle (data) {
|
||||
controls.setSingle(data)
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
/** ******************************************************
|
||||
* process results of current position
|
||||
*********************************************************/
|
||||
function processCurrentposition(data) {
|
||||
setPosition(parseInt(data));
|
||||
function processCurrentposition (data) {
|
||||
controls.setPosition(parseInt(data))
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
/** ******************************************************
|
||||
* process results playstate
|
||||
*********************************************************/
|
||||
function processPlaystate(data) {
|
||||
if (data == 'playing') {
|
||||
setPlayState(true);
|
||||
function processPlaystate (data) {
|
||||
if (data === 'playing') {
|
||||
controls.setPlayState(true)
|
||||
} else {
|
||||
setPlayState(false);
|
||||
controls.setPlayState(false)
|
||||
}
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
/** ******************************************************
|
||||
* process results of a browse list
|
||||
*********************************************************/
|
||||
function processBrowseDir(resultArr) {
|
||||
var backHtml = '<li style="background-color:#ccc"><a href="#" onclick="return getBrowseDir();"><h1 class="trackname"><i class="fa fa-arrow-circle-left"></i> Back</h1></a></li>';
|
||||
if ( (!resultArr) || (resultArr === '') || (resultArr.length === 0) ) {
|
||||
$('#browsepath').html('No tracks found...');
|
||||
$('#browselist').html(backHtml);
|
||||
showLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
$('#browselist').empty();
|
||||
|
||||
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('/');
|
||||
|
||||
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);
|
||||
|
||||
function processBrowseDir (resultArr) {
|
||||
$(BROWSE_TABLE).empty()
|
||||
if (browseStack.length > 0) {
|
||||
child += backHtml;
|
||||
renderSongLiBackButton(resultArr, BROWSE_TABLE, 'return library.getBrowseDir();')
|
||||
}
|
||||
if (!resultArr || resultArr.length === 0) {
|
||||
showLoading(false)
|
||||
return
|
||||
}
|
||||
uris = []
|
||||
var ref, previousRef, nextRef
|
||||
var uri = resultArr[0].uri
|
||||
var length = 0 || resultArr.length
|
||||
customTracklists[BROWSE_TABLE] = []
|
||||
var html = ''
|
||||
|
||||
browseTracks = [];
|
||||
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 += '<li class="song albumli" id="browselisttracks-' + resultArr[i].uri + '">' +
|
||||
'<a href="#" class="moreBtn" onclick="return popupTracks(event, \'' + uri + '\', \'' + resultArr[i].uri + '\', \'' + index + '\');">' +
|
||||
'<i class="fa fa-ellipsis-v"></i></a>' +
|
||||
'<a href="#" class="browsetrack" onclick="return playBrowsedTracks(PLAY_ALL, ' + index + ');" id="' + resultArr[i].uri +
|
||||
'"><h1 class="trackname"><i class="' + iconClass + '"></i> ' + resultArr[i].name + '</h1></a></li>';
|
||||
index++;
|
||||
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
|
||||
customTracklists[BROWSE_TABLE].push(ref)
|
||||
uris.push(ref.uri)
|
||||
|
||||
html += renderSongLi(previousRef, ref, nextRef, BROWSE_TABLE, '', BROWSE_TABLE, index, resultArr.length)
|
||||
|
||||
index++
|
||||
} else {
|
||||
var iconClass = ''
|
||||
if (browseStack.length > 0) {
|
||||
iconClass="fa fa-folder-o";
|
||||
iconClass = 'fa fa-folder-o'
|
||||
} else {
|
||||
iconClass = getMediaClass(resultArr[i].uri)
|
||||
}
|
||||
child += '<li><a href="#" onclick="return getBrowseDir(this.id);" id="' + resultArr[i].uri +
|
||||
'""><h1 class="trackname"><i class="' + iconClass + '"></i> ' + resultArr[i].name + '</h1></a></li>';
|
||||
html += '<li><a href="#" onclick="return library.getBrowseDir(this.id);" id="' + resultArr[i].uri + '">' +
|
||||
'<h1><i class="' + iconClass + '"></i> ' + resultArr[i].name + '</h1></a></li>'
|
||||
}
|
||||
}
|
||||
|
||||
$('#browselist').html(child);
|
||||
if (browseStack.length > 0 ) {
|
||||
child = getMediaHuman(uri);
|
||||
iconClass = getMediaClass(uri);
|
||||
$('#browsepath').html('<i class="' + iconClass + '"></i> ' + child);
|
||||
$(BROWSE_TABLE).append(html)
|
||||
|
||||
updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction())
|
||||
|
||||
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]
|
||||
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)
|
||||
}
|
||||
renderSongLiDivider(previousTrack, track, nextTrack, BROWSE_TABLE)
|
||||
}
|
||||
})
|
||||
showLoading(false)
|
||||
}, console.error)
|
||||
} else {
|
||||
$('#browsepath').html('');
|
||||
showLoading(false)
|
||||
}
|
||||
|
||||
updatePlayIcons(songdata.track.uri, songdata.tlid);
|
||||
|
||||
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 = ''
|
||||
var favourites = ''
|
||||
var starred = ''
|
||||
|
||||
for (var i = 0; i < resultArr.length; i++) {
|
||||
var li_html = '<li><a href="#" onclick="return showTracklist(this.id);" id="' + resultArr[i].uri + '">';
|
||||
if(isSpotifyStarredPlaylist(resultArr[i])) {
|
||||
starred = li_html + '★ Spotify Starred Tracks</a></li>' + tmp;
|
||||
var li_html = '<li><a href="#" onclick="return library.showTracklist(this.id);" id="' + resultArr[i].uri + '">'
|
||||
if (isSpotifyStarredPlaylist(resultArr[i])) {
|
||||
starred = li_html + '★ Spotify Starred Tracks</a></li>' + tmp
|
||||
} else if (isFavouritesPlaylist(resultArr[i])) {
|
||||
favourites = li_html + '♥ Musicbox Favourites</a></li>';
|
||||
favourites = li_html + '♥ Musicbox Favourites</a></li>'
|
||||
} else {
|
||||
tmp = tmp + li_html + '<i class="' + getMediaClass(resultArr[i].uri) + '"></i> ' + resultArr[i].name + '</a></li>';
|
||||
tmp = tmp + li_html + '<i class="' + getMediaClass(resultArr[i].uri) + '"></i> ' + resultArr[i].name + '</a></li>'
|
||||
}
|
||||
}
|
||||
// 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, controls.getIconForAction())
|
||||
}
|
||||
|
||||
/********************************************************
|
||||
/** ******************************************************
|
||||
* 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...')
|
||||
images.setAlbumImage('', '#artistviewimage, #artistpopupimage', mopidy)
|
||||
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)
|
||||
images.setArtistImage(resultArr.uri, resultArr[0].uri, '#artistviewimage, #artistpopupimage', mopidy)
|
||||
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...')
|
||||
images.setAlbumImage('', '#albumviewcover, #coverpopupimage', mopidy)
|
||||
showLoading(false)
|
||||
return
|
||||
}
|
||||
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);
|
||||
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)
|
||||
images.setAlbumImage(resultArr[0].uri, '#albumviewcover, #coverpopupimage', mopidy)
|
||||
showLoading(false)
|
||||
}
|
||||
|
||||
@ -1,176 +0,0 @@
|
||||
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 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);
|
||||
|
||||
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;
|
||||
}
|
||||
207
mopidy_musicbox_webclient/static/js/synced_timer.js
Normal file
207
mopidy_musicbox_webclient/static/js/synced_timer.js
Normal file
@ -0,0 +1,207 @@
|
||||
(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').empty().append(this.positionNode)
|
||||
$('#songlength').empty().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.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 = {
|
||||
NOT_SYNCED: 0,
|
||||
SYNCING: 1,
|
||||
SYNCED: 2
|
||||
}
|
||||
|
||||
SyncedProgressTimer.format = function (milliseconds) {
|
||||
if (milliseconds === Infinity) {
|
||||
return ''
|
||||
} 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.timerCallback = function (position, duration) {
|
||||
this._update(position, duration)
|
||||
if (this._isSyncScheduled && this._isConnected) {
|
||||
this._doSync(position, duration)
|
||||
}
|
||||
}
|
||||
|
||||
SyncedProgressTimer.prototype._update = function (position, duration) {
|
||||
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._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) {
|
||||
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 (_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._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._scheduleSync(delay_exponential(0.25, 2, _this._maxAttempts - _this._syncAttemptsRemaining) * 1000)
|
||||
} else {
|
||||
// Drift is too large, re-sync with Mopidy.
|
||||
_this.syncState = SyncedProgressTimer.SYNC_STATE.SYNCING
|
||||
_this._syncAttemptsRemaining = _this._maxAttempts
|
||||
_this._previousSyncPosition = null
|
||||
_this._scheduleSync(1000)
|
||||
_this._progressTimer.set(targetPosition)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
SyncedProgressTimer.prototype.set = function (position, duration) {
|
||||
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
|
||||
}
|
||||
if (arguments.length === 1) {
|
||||
this._progressTimer.set(position)
|
||||
} else {
|
||||
this._duration = duration
|
||||
this._progressTimer.set(position, duration)
|
||||
this.durationNode.nodeValue = SyncedProgressTimer.format(duration)
|
||||
}
|
||||
|
||||
this.updatePosition(position, duration)
|
||||
$('#trackslider').val(position).slider('refresh')
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
SyncedProgressTimer.prototype.start = function () {
|
||||
this.syncState = SyncedProgressTimer.SYNC_STATE.NOT_SYNCED
|
||||
this._scheduleSync(0)
|
||||
this._progressTimer.start()
|
||||
return this
|
||||
}
|
||||
|
||||
SyncedProgressTimer.prototype.stop = function () {
|
||||
this._progressTimer.stop()
|
||||
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.stop()
|
||||
this.set(0, Infinity)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
SyncedProgressTimer.prototype.updatePosition = function (position) {
|
||||
if (!(this._duration === Infinity && position === 0)) {
|
||||
this.positionNode.nodeValue = SyncedProgressTimer.format(position)
|
||||
} else {
|
||||
this.positionNode.nodeValue = ''
|
||||
}
|
||||
}
|
||||
|
||||
return SyncedProgressTimer
|
||||
}))
|
||||
@ -1,6 +1,6 @@
|
||||
CACHE MANIFEST
|
||||
|
||||
# 2016-02-29:v1
|
||||
# 2016-05-13:v1
|
||||
|
||||
NETWORK:
|
||||
*
|
||||
@ -19,14 +19,14 @@ 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
|
||||
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
|
||||
|
||||
@ -23,8 +23,8 @@
|
||||
|
||||
<div data-role="content" data-theme="b">
|
||||
<h3>System</h3>
|
||||
<a href="#" onclick="haltSystem(); return false;" data-role="button" data-rel="dialog" data-transition="slidedown" data-theme="b">Shutdown</a>
|
||||
<a href="#" onclick="rebootSystem(); return false;" data-role="button" data-rel="dialog" data-transition="slidedown" data-theme="b">Reboot</a>
|
||||
<a href="#" onclick="controls.haltSystem(); return false;" data-role="button" data-rel="dialog" data-transition="slidedown" data-theme="b">Shutdown</a>
|
||||
<a href="#" onclick="controls.rebootSystem(); return false;" data-role="button" data-rel="dialog" data-transition="slidedown" data-theme="b">Reboot</a>
|
||||
<a href="index.html" data-role="button" data-rel="back" data-theme="a">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,128 +1,167 @@
|
||||
/*! 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 = /* 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.
|
||||
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 {
|
||||
return this;
|
||||
};
|
||||
|
||||
// Cancel the timer if it us currently tracking progress.
|
||||
ProgressTimer.prototype.stop = function() {
|
||||
if (this._updateId !== null) {
|
||||
this._cancel(this._updateId);
|
||||
|
||||
// Ensure we correctly reset the initial position and timestamp.
|
||||
this.set(this._state.position, this._state.duration);
|
||||
this._updateId = null; // Last step to avoid callback in set()
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
// Marks the timer as stopped, sets position to zero and duration to inf.
|
||||
ProgressTimer.prototype.reset = function() {
|
||||
return this.stop().set(0, Infinity);
|
||||
};
|
||||
|
||||
// Calls the user callback with the current position/duration and then
|
||||
// schedules the next update run via _schedule if we haven't finished.
|
||||
ProgressTimer.prototype._update = function(timestamp) {
|
||||
var state = this._state; // We refer a lot to state, this is shorter.
|
||||
|
||||
// Make sure setTimeout has a timestamp and store first reference time.
|
||||
timestamp = timestamp || now();
|
||||
state.initialTimestamp = state.initialTimestamp || timestamp;
|
||||
|
||||
// Recalculate position according to start location and reference.
|
||||
state.position = (
|
||||
state.initialPosition + timestamp - state.initialTimestamp);
|
||||
|
||||
// Ensure callback gets an integer and that 'position <= duration'.
|
||||
var userPosisition = Math.min(
|
||||
Math.floor(state.position), state.duration);
|
||||
|
||||
// TODO: Consider wrapping this in a try/catch?
|
||||
this._userCallback(userPosisition, state.duration);
|
||||
// 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);
|
||||
};
|
||||
// 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.
|
||||
//}
|
||||
};
|
||||
|
||||
if(typeof module !== 'undefined') {
|
||||
module.exports = ProgressTimer;
|
||||
} else {
|
||||
window.ProgressTimer = ProgressTimer;
|
||||
}
|
||||
|
||||
}());
|
||||
return ProgressTimer;
|
||||
}));
|
||||
@ -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,32 @@ 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()),
|
||||
'onTrackClick': webclient.get_default_click_action()
|
||||
}
|
||||
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
|
||||
|
||||
49
mopidy_musicbox_webclient/webclient.py
Normal file
49
mopidy_musicbox_webclient/webclient.py
Normal file
@ -0,0 +1,49 @@
|
||||
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)
|
||||
|
||||
def get_default_click_action(self):
|
||||
return self.ext_config.get('on_track_click', 'PLAY_ALL')
|
||||
53
package.json
Normal file
53
package.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "Mopidy-MusicBox-Webclient",
|
||||
"version": "2.1.1",
|
||||
"description": "Mopidy MusicBox web extension",
|
||||
"main": "gui.js",
|
||||
"directories": {
|
||||
"test": "tests"
|
||||
},
|
||||
"scripts": {
|
||||
"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"
|
||||
},
|
||||
"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": {
|
||||
"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"
|
||||
}
|
||||
BIN
screenshots/queue_desktop.png
Normal file
BIN
screenshots/queue_desktop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
2
setup.py
2
setup.py
@ -29,7 +29,7 @@ setup(
|
||||
],
|
||||
entry_points={
|
||||
'mopidy.ext': [
|
||||
'musicbox_webclient = mopidy_musicbox_webclient:MusicBoxExtension',
|
||||
'musicbox_webclient = mopidy_musicbox_webclient:Extension',
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
|
||||
108
tests/js/dummy_tracklist.js
Normal file
108
tests/js/dummy_tracklist.js
Normal file
@ -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
|
||||
}))
|
||||
265
tests/js/test_controls.js
Normal file
265
tests/js/test_controls.js
Normal file
@ -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('<div data-role="popup" id="popupTracks"></div>')
|
||||
$('#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-play-circle')
|
||||
assert.equal(controls.getIconForAction(PLAY_NOW), 'fa fa-play-circle-o')
|
||||
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-circle-o')
|
||||
})
|
||||
|
||||
it('should use default tracklist action if no parameter is provided', function () {
|
||||
assert.equal(controls.getIconForAction(), 'fa fa-play-circle')
|
||||
})
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
30
tests/js/test_custom_scripting.js
Normal file
30
tests/js/test_custom_scripting.js
Normal file
@ -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()
|
||||
})
|
||||
})
|
||||
567
tests/js/test_images.js
Normal file
567
tests/js/test_images.js
Normal file
@ -0,0 +1,567 @@
|
||||
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 images = require('../../mopidy_musicbox_webclient/static/js/images.js')
|
||||
|
||||
describe('images', function () {
|
||||
var mopidy
|
||||
var img_element
|
||||
before(function () {
|
||||
mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'}))
|
||||
img_element = $('<img id="img_mock">')
|
||||
})
|
||||
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 fall back to using album artist if track artist is not available', function () {
|
||||
var track = {
|
||||
album: {artists: [{name: 'albumArtistMock', musicbrainz_id: 'albumArtistIDMock'}]}
|
||||
}
|
||||
|
||||
var artist = images._getArtistInfo(track)
|
||||
assert.equal(artist.name, 'albumArtistMock')
|
||||
assert.equal(artist.mbid, 'albumArtistIDMock')
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
getImagesSpy = sinon.spy(mopidy.library, 'getImages')
|
||||
setDeprecatedAlbumImageSpy = sinon.spy(images, '_setDeprecatedAlbumImage')
|
||||
})
|
||||
afterEach(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)
|
||||
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)
|
||||
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('#_setDeprecatedAlbumImage()', function () {
|
||||
var lookupResultMock
|
||||
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')
|
||||
images._setDeprecatedAlbumImage('mock:track:uri', img_element, mopidy)
|
||||
|
||||
assert.isTrue(lookupSpy.calledOnce)
|
||||
expect($(img_element).prop('src')).to.endWith('mockAlbumImageUri')
|
||||
mopidy.library.lookup.restore()
|
||||
})
|
||||
|
||||
it('should use default image if track.album or track.artist is not available', function () {
|
||||
lookupResultMock = {'mock:track:uri': [{}]}
|
||||
|
||||
var lookupSpy = sinon.spy(mopidy.library, 'lookup')
|
||||
images._setDeprecatedAlbumImage('mock:track:uri', img_element, mopidy)
|
||||
|
||||
assert.isTrue(lookupSpy.calledOnce)
|
||||
expect($(img_element).prop('src')).to.endWith(images.DEFAULT_ALBUM_URL)
|
||||
mopidy.library.lookup.restore()
|
||||
})
|
||||
|
||||
it('should fall back to retrieving image from last.fm if none provided by Mopidy', function () {
|
||||
lookupResultMock = {'mock:track:uri': [{album: {images: []}}]}
|
||||
|
||||
var setLastFmAlbumImageStub = sinon.stub(images, '_setLastFmAlbumImage')
|
||||
images._setDeprecatedAlbumImage('mock:track:uri', img_element, mopidy)
|
||||
|
||||
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('#_setLastFmAlbumImage()', function () {
|
||||
var getInfoResultMock
|
||||
var getInfoStub
|
||||
|
||||
before(function () {
|
||||
getInfoStub = sinon.stub(images, '_getLastFmAlbumInfo')
|
||||
})
|
||||
beforeEach(function () {
|
||||
getInfoStub.reset()
|
||||
})
|
||||
after(function () {
|
||||
getInfoStub.restore()
|
||||
})
|
||||
|
||||
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'}
|
||||
]}}
|
||||
|
||||
getInfoStub.returns($.when(getInfoResultMock))
|
||||
|
||||
images._setLastFmAlbumImage(track, img_element, 'medium')
|
||||
expect($(img_element).prop('src')).to.endWith('mockAlbumMedImageUri')
|
||||
})
|
||||
|
||||
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'}
|
||||
]}}
|
||||
|
||||
getInfoStub.returns($.when(getInfoResultMock))
|
||||
|
||||
images._setLastFmAlbumImage(track, img_element)
|
||||
expect($(img_element).prop('src')).to.endWith('mockAlbumXLargeImageUri')
|
||||
})
|
||||
|
||||
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')
|
||||
images._setLastFmAlbumImage(track, img_element)
|
||||
|
||||
assert.isTrue(consoleStub.calledWith('Error getting album info from last.fm (%s: %s)', 'code', 'message'))
|
||||
consoleStub.restore()
|
||||
})
|
||||
})
|
||||
|
||||
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 () {
|
||||
mopidy.library.getImages.restore()
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
it('should get artist image from Mopidy, if available', function () {
|
||||
getImagesResultMock = {'mock:artist:uri': [{uri: 'mockImageUri'}]}
|
||||
|
||||
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 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')
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
56
tests/js/test_library.js
Normal file
56
tests/js/test_library.js
Normal file
@ -0,0 +1,56 @@
|
||||
var chai = require('chai')
|
||||
var expect = chai.expect
|
||||
var assert = chai.assert
|
||||
chai.use(require('chai-string'))
|
||||
chai.use(require('chai-jquery'))
|
||||
|
||||
var sinon = require('sinon')
|
||||
|
||||
var library = require('../../mopidy_musicbox_webclient/static/js/library.js')
|
||||
|
||||
describe('Library', function () {
|
||||
var selectID = '#selectSearchService'
|
||||
var schemesArray = ['mockScheme1', 'mockScheme2', 'mockScheme3']
|
||||
var mopidy = { getUriSchemes: function () { return $.when(schemesArray) } }
|
||||
|
||||
before(function () {
|
||||
$(document.body).append('<select id="selectSearchService"></select>')
|
||||
$('#selectSearchService').selectmenu()
|
||||
})
|
||||
describe('#getSearchSchemes()', function () {
|
||||
beforeEach(function () {
|
||||
$(selectID).empty()
|
||||
})
|
||||
|
||||
it('should add human-readable options for backend schemes', function () {
|
||||
uriHumanList = [['mockScheme2', 'mockUriHuman2']]
|
||||
|
||||
library.getSearchSchemes([], mopidy)
|
||||
assert.equal($(selectID).children().length, schemesArray.length + 1)
|
||||
expect($(selectID).children(':eq(2)')).to.have.text('MockUriHuman2')
|
||||
})
|
||||
|
||||
it('should get default value from cookie', function () {
|
||||
$.cookie('searchScheme', 'mockScheme3')
|
||||
library.getSearchSchemes([], mopidy)
|
||||
expect($(selectID + ' option:selected')).to.have.value('mockScheme3')
|
||||
})
|
||||
|
||||
it('should default to "all" backends if no cookie is available', function () {
|
||||
$.removeCookie('searchScheme')
|
||||
library.getSearchSchemes([], mopidy)
|
||||
expect($(selectID + ' option:selected')).to.have.value('all')
|
||||
})
|
||||
|
||||
it('should capitalize first character of backend schema', function () {
|
||||
library.getSearchSchemes([], mopidy)
|
||||
expect($(selectID).children(':eq(1)')).to.have.text('MockScheme1')
|
||||
})
|
||||
|
||||
it('should blacklist services that should not be searched', function () {
|
||||
library.getSearchSchemes(['mockScheme2'], mopidy)
|
||||
assert.equal($(selectID).children().length, schemesArray.length)
|
||||
expect($(selectID).children()).not.to.contain('mockScheme2')
|
||||
})
|
||||
})
|
||||
})
|
||||
529
tests/js/test_synced_timer.js
Normal file
529
tests/js/test_synced_timer.js
Normal file
@ -0,0 +1,529 @@
|
||||
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
|
||||
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(
|
||||
'<div id="slidercontainer"><!-- slider for track position -->' +
|
||||
'<span id="songelapsed"></span>' +
|
||||
'<span id="songlength"></span>' +
|
||||
'<label for="trackslider" disabled="disabled">Position</label>' +
|
||||
'<input id="trackslider" name="trackslider"/>' +
|
||||
'</div>'
|
||||
)
|
||||
$('#trackslider').slider() // Initialize slider
|
||||
$('#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') }
|
||||
}
|
||||
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))
|
||||
}
|
||||
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.reset()
|
||||
})
|
||||
|
||||
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), '')
|
||||
})
|
||||
|
||||
it('should handle zero', function () {
|
||||
assert.equal(SyncedProgressTimer.format(0), '0:00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#timerCallback()', function () {
|
||||
beforeEach(function () {
|
||||
setFakeTimers()
|
||||
})
|
||||
afterEach(function () {
|
||||
restoreFakeTimers()
|
||||
})
|
||||
|
||||
it('should not try to sync unless connected to mopidy', function () {
|
||||
var _doSyncStub = sinon.stub(syncedProgressTimer, '_doSync')
|
||||
|
||||
syncedProgressTimer._isConnected = false
|
||||
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 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 not attempt to perform a sync untill scheduled', function () {
|
||||
var syncStub = sinon.stub(syncedProgressTimer, '_doSync')
|
||||
|
||||
syncedProgressTimer.set(0, 5000).start()
|
||||
syncedProgressTimer._scheduleSync(500)
|
||||
clock.tick(250)
|
||||
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()
|
||||
})
|
||||
|
||||
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 < 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 set duration to "" for tracks with infinite duration (e.g. streams)', function () {
|
||||
syncedProgressTimer._update(1000, Infinity)
|
||||
assert.equal(syncedProgressTimer.durationNode.nodeValue, '')
|
||||
})
|
||||
|
||||
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)')
|
||||
})
|
||||
|
||||
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('#scheduleSync', function () {
|
||||
beforeEach(function () {
|
||||
setFakeTimers()
|
||||
})
|
||||
afterEach(function () {
|
||||
restoreFakeTimers()
|
||||
})
|
||||
|
||||
it('should schedule sync when scheduled time arrives', function () {
|
||||
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))
|
||||
window.clearTimeout.restore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('#_doSync', function () {
|
||||
beforeEach(function () {
|
||||
setFakeTimers()
|
||||
})
|
||||
afterEach(function () {
|
||||
restoreFakeTimers()
|
||||
})
|
||||
|
||||
it('should not try to sync until timer has been set', function () {
|
||||
syncedProgressTimer._doSync(0, Infinity)
|
||||
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(250)
|
||||
syncedProgressTimer._doSync(250, 2000)
|
||||
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING)
|
||||
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 () {
|
||||
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(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(250, 2000)
|
||||
assert(scheduleStub.calledWith(32000))
|
||||
scheduleStub.restore()
|
||||
})
|
||||
|
||||
it('should not sync unless track playback is progressing', function () {
|
||||
getTimePositionStub.restore()
|
||||
|
||||
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.NOT_SYNCED)
|
||||
clock.tick(250)
|
||||
syncedProgressTimer._doSync(250, 2000)
|
||||
assert.equal(syncedProgressTimer.syncState, SyncedProgressTimer.SYNC_STATE.SYNCING)
|
||||
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))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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 position and track slider immediately', function () {
|
||||
syncedProgressTimer.stop()
|
||||
syncedProgressTimer.set(1000, 2000)
|
||||
|
||||
expect($('#songelapsed').text()).to.equal('0:01')
|
||||
assert.equal($('#trackslider').val(), 1000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#start()', function () {
|
||||
it('should start timer', function () {
|
||||
var startStub = sinon.stub(syncedProgressTimer._progressTimer, 'start')
|
||||
syncedProgressTimer.start()
|
||||
assert(startStub.called)
|
||||
syncedProgressTimer.stop()
|
||||
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)
|
||||
syncedProgressTimer.stop()
|
||||
})
|
||||
|
||||
it('should schedule a sync immediately', function () {
|
||||
var scheduleSpy = sinon.spy(syncedProgressTimer, '_scheduleSync')
|
||||
|
||||
syncedProgressTimer.set(0, 1000)
|
||||
syncedProgressTimer._isSyncScheduled = false
|
||||
syncedProgressTimer.start()
|
||||
|
||||
assert(scheduleSpy.calledWith(0))
|
||||
syncedProgressTimer.stop()
|
||||
syncedProgressTimer._scheduleSync.restore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('#stop()', 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 last synced position if stopped while busy syncing', function () {
|
||||
syncedProgressTimer.set(1000, 5000)
|
||||
syncedProgressTimer.syncState = SyncedProgressTimer.SYNC_STATE.SYNCED
|
||||
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 () {
|
||||
var cancelSpy = sinon.spy(window, 'clearTimeout')
|
||||
|
||||
syncedProgressTimer._isSyncScheduled = true
|
||||
syncedProgressTimer.stop()
|
||||
|
||||
assert.isFalse(syncedProgressTimer._isSyncScheduled)
|
||||
assert(cancelSpy.calledWith(syncedProgressTimer._scheduleID))
|
||||
window.clearTimeout.restore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('#reset()', function () {
|
||||
it('should reset timer to "" - "" ', function () {
|
||||
var stopStub = sinon.stub(syncedProgressTimer, 'stop')
|
||||
var setStub = sinon.stub(syncedProgressTimer, 'set')
|
||||
|
||||
syncedProgressTimer.reset()
|
||||
|
||||
assert(stopStub.called)
|
||||
assert(setStub.called)
|
||||
|
||||
assert.equal(syncedProgressTimer.positionNode.nodeValue, '', 'Position node was not reset')
|
||||
assert.equal(syncedProgressTimer.durationNode.nodeValue, '', 'Duration node was not reset')
|
||||
|
||||
stopStub.restore()
|
||||
setStub.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.equal('0:01')
|
||||
SyncedProgressTimer.format.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()
|
||||
})
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,22 +1,39 @@
|
||||
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
|
||||
assert 'on_track_click = PLAY_ALL' 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
|
||||
assert 'on_track_click' 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)
|
||||
|
||||
67
tests/test_web.py
Normal file
67
tests/test_web.py
Normal file
@ -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 '<title>MusicBox on localhost</title>' 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
|
||||
81
tests/test_webclient.py
Normal file
81
tests/test_webclient.py
Normal file
@ -0,0 +1,81 @@
|
||||
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()
|
||||
|
||||
def test_default_click_action(self):
|
||||
assert self.mmw.get_default_click_action() == 'PLAY_ALL'
|
||||
88
tidy.js
Normal file
88
tidy.js
Normal file
@ -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()
|
||||
})
|
||||
59
tox.ini
59
tox.ini
@ -1,23 +1,76 @@
|
||||
[tox]
|
||||
envlist = py27, flake8
|
||||
envlist = py27, flake8, test, eslint, csslint, tidy
|
||||
|
||||
[testenv]
|
||||
passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH
|
||||
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} \
|
||||
--junit-xml=xunit-{envname}.xml \
|
||||
--cov=mopidy_musicbox_webclient --cov-report=term-missing \
|
||||
{posargs}
|
||||
{posargs:tests/}
|
||||
|
||||
[testenv:flake8]
|
||||
sitepackages = false
|
||||
deps =
|
||||
flake8
|
||||
flake8-import-order
|
||||
pep8-naming
|
||||
skip_install = true
|
||||
commands = flake8
|
||||
commands = flake8 --show-source --statistics --max-line-length 120 {posargs:mopidy_musicbox_webclient tests}
|
||||
|
||||
[testenv:test]
|
||||
sitepackages = false
|
||||
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]
|
||||
sitepackages = false
|
||||
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]
|
||||
sitepackages = false
|
||||
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]
|
||||
sitepackages = false
|
||||
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'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user