├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── mopidy_websettings ├── __init__.py ├── ext.conf ├── index.html ├── js │ ├── mopidy.min.js │ └── simple.js └── settingsspec.ini ├── setup.cfg ├── setup.py ├── tests ├── __init__.py └── test_extension.py ├── tox.ini └── xunit-py27.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_store 2 | *.pyc 3 | *.swp 4 | .coverage 5 | .idea 6 | .noseids 7 | .tox 8 | MANIFEST 9 | build/ 10 | cover/ 11 | coverage.xml 12 | dist/ 13 | docs/_build/ 14 | mopidy.log* 15 | node_modules/ 16 | nosetests.xml 17 | .project 18 | *.pbxproj 19 | *.egg-info 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | language: python 5 | 6 | python: 7 | - "2.7_with_system_site_packages" 8 | 9 | env: 10 | - TOX_ENV=py27 11 | - TOX_ENV=flake8 12 | 13 | before_install: 14 | - "sudo apt-get update -qq" 15 | - "sudo apt-get install -y gir1.2-gst-plugins-base-1.0 gir1.2-gstreamer-1.0 python-gst-1.0" 16 | 17 | install: 18 | - "pip install tox" 19 | 20 | script: 21 | - "tox -e $TOX_ENV" 22 | 23 | after_success: 24 | - "if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; fi" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include .coveragerc 2 | include .travis.yml 3 | include LICENSE 4 | include MANIFEST.in 5 | include README.rst 6 | include mopidy_websettings/* 7 | include mopidy_websettings/js/ * 8 | 9 | recursive-include tests *.py 10 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **************************** 2 | Mopidy-WebSettings 3 | **************************** 4 | 5 | .. image:: https://img.shields.io/pypi/v/Mopidy-WebSettings.svg?style=flat 6 | :target: https://pypi.python.org/pypi/Mopidy-WebSettings/ 7 | :alt: Latest PyPI version 8 | 9 | .. image:: https://img.shields.io/pypi/dm/Mopidy-WebSettings.svg?style=flat 10 | :target: https://pypi.python.org/pypi/Mopidy-WebSettings/ 11 | :alt: Number of PyPI downloads 12 | 13 | .. image:: https://img.shields.io/travis/pimusicbox/mopidy-websettings/develop.svg?style=flat 14 | :target: https://travis-ci.org/pimusicbox/mopidy-websettings 15 | :alt: Travis CI build status 16 | 17 | .. image:: https://img.shields.io/coveralls/pimusicbox/mopidy-websettings/develop.svg?style=flat 18 | :target: https://coveralls.io/r/pimusicbox/mopidy-websettings?branch=develop 19 | :alt: Test coverage 20 | 21 | Mopidy extension for editing settings in a webinterface. Used by Pi MusicBox, but also usable for personal installations or other projects, since MusicBox-only settings are hidden automatically. 22 | 23 | 24 | This project is no longer maintained. 25 | ============ 26 | 27 | There will be no more releases and no more support from myself. 28 | 29 | 30 | Installation 31 | ============ 32 | 33 | Install by running:: 34 | 35 | sudo pip install Mopidy-WebSettings 36 | 37 | Or, if available, install the Debian/Ubuntu package from `apt.mopidy.com 38 | `_. 39 | 40 | 41 | Configuration 42 | ============= 43 | 44 | Before starting Mopidy, you can add configuration for 45 | Mopidy-WebSettings to your Mopidy configuration file:: 46 | 47 | [websettings] 48 | enabled = true 49 | musicbox = false 50 | config_file = /etc/mopidy/mopidy.conf 51 | 52 | Make sure the config file is writable by the user under which mopidy is running! And make sure the http extension is working. Go to the ip or url of your mopidy computer and add /settings (e.g. http://musicbox.local/settings or http://192.168.1.10:6680/settings ) 53 | 54 | For now, the settings are only applied after a reboot, which this extension will try to do. If it doesn't work, a (manual) restart of mopidy is needed. 55 | 56 | Project resources 57 | ================= 58 | 59 | - `Source code `_ 60 | - `Issue tracker `_ 61 | - `Development branch tarball `_ 62 | 63 | 64 | Changelog 65 | ========= 66 | 67 | v0.2.3 (25/2/2018) 68 | ------------------ 69 | 70 | - Support for AudioInjector stereo and octo cards. 71 | 72 | 73 | v0.2.2 (14/1/2018) 74 | ------------------ 75 | 76 | - Updated wording for the new required Spotify credentials. 77 | 78 | 79 | v0.2.1 (12/1/2018) 80 | ------------------ 81 | 82 | - Moved spotify_web's client_id and client_secret to spotify section. 83 | - Added support for Allo DigiOne and Mini Boss HiFi DAC. 84 | - Added support for the latest confusingly named HiFiBerry products (cummon, guys). 85 | 86 | v0.2.0 (18/7/2017) 87 | ------------------ 88 | 89 | - Added Mopidy logging configuration setting. 90 | 91 | v0.1.7 (18/7/2017) 92 | ------------------ 93 | 94 | - Added WiFi country setting. 95 | - Added MPD Watchdog setting. 96 | - Increased WiFi password input to support 64 character hex keys. 97 | - Added Spotify Connect setting. 98 | 99 | v0.1.6.2 (21/4/2017) 100 | -------------------- 101 | 102 | - Added firewall setting. 103 | - Updated support for IQ Audio and Allo DAC soundcards. 104 | - Updated podcast settings. 105 | - Fixed missing javascript files from previous release. 106 | 107 | v0.1.6.1 (18/3/2017) 108 | -------------------- 109 | 110 | - Added JustBoom audio cards. 111 | 112 | v0.1.6 (18/3/2017) 113 | ------------------ 114 | 115 | - Updated and improved explanations for settings. 116 | - Removed development-orientated resize_once option. 117 | - Fixed corrupt settings when expected subsections missing from settings.ini. 118 | - Fixed webclient not changing. 119 | - Changing the webclient requires a Mopidy restart rather than a reboot. 120 | - Fixed outdated Exception syntax. 121 | - Option to populate autoplay with currently playing track. 122 | 123 | v0.1.5 (2/3/2016) 124 | ----------------- 125 | 126 | - Fixed password field length restrictions. 127 | - Apply changes with Mopidy service restart rather than system reboot where possible. 128 | - Added/updated options as required for integration with PiMusicbox v0.7. 129 | 130 | v0.1.4.2 (26/3/2015) 131 | -------------------- 132 | 133 | - Fixed length of Autoplay URL input box. 134 | 135 | v0.1.4.1 136 | -------- 137 | 138 | - Small fix for passwords not updated well 139 | 140 | v0.1.4 141 | ------ 142 | 143 | - AudioAddict added (by Nilicule) 144 | - Passwords mask same size as password 145 | 146 | v0.1.3 147 | ------ 148 | 149 | - Added YouTube and local support (on or off) 150 | - Enabled YouTube, SomaFM, Local, Internetarchive, Podcast by default when not in ini-file (the default of the extensions) 151 | - Settings for enabling streaming services shairport and upmpdcli (for MusicBox only) 152 | - Writing the config is more reliable 153 | - Better layout 154 | 155 | v0.1.2 156 | ------ 157 | 158 | - Fixes for writing ini file 159 | 160 | 161 | v0.1.1 162 | ------ 163 | 164 | - Fixed missing template files in dist 165 | - Fixed template 166 | - Reboot after settings are updated (if mopidy instance has the permission to do that, like on musicbox) 167 | 168 | v0.1.0 169 | ------ 170 | 171 | - Initial release 172 | -------------------------------------------------------------------------------- /mopidy_websettings/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import logging 4 | import os 5 | import sys 6 | 7 | 8 | from configobj import ConfigObj, ConfigObjError 9 | 10 | import jinja2 11 | 12 | from mopidy import config, ext 13 | 14 | import tornado.web 15 | 16 | __version__ = '0.2.3' 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | spec_file = os.path.join(os.path.dirname(__file__), 'settingsspec.ini') 21 | template_file = os.path.join(os.path.dirname(__file__), 'index.html') 22 | 23 | 24 | def can_change_root_password(ini): 25 | root_password = ini.get('musicbox', {}).get('root_password', '') 26 | return len(root_password) 27 | 28 | 29 | def get_name(item, subitem): 30 | return item + '__' + subitem 31 | 32 | 33 | def restart_program(): 34 | """ 35 | DOES NOT WORK WELL WITH MOPIDY 36 | Hack from 37 | https://www.daniweb.com/software-development/python/code/260268/restart-your-python-program 38 | to support updating the settings, since mopidy is not able to do that yet 39 | Restarts the current program 40 | Note: this function does not return. Any cleanup action (like 41 | saving data) must be done before calling this function 42 | """ 43 | 44 | python = sys.executable 45 | os.execl(python, python, * sys.argv) 46 | 47 | 48 | class Extension(ext.Extension): 49 | dist_name = 'Mopidy-WebSettings' 50 | ext_name = 'websettings' 51 | version = __version__ 52 | 53 | def get_default_config(self): 54 | conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf') 55 | return config.read(conf_file) 56 | 57 | def get_config_schema(self): 58 | schema = super(Extension, self).get_config_schema() 59 | schema['musicbox'] = config.Boolean() 60 | schema['config_file'] = config.String() 61 | return schema 62 | 63 | def setup(self, registry): 64 | registry.add('http:app', { 65 | # 'name': self.ext_name, 66 | 'name': 'settings', 67 | 'factory': websettings_app_factory, 68 | }) 69 | 70 | 71 | class WebSettingsRequestHandler(tornado.web.RequestHandler): 72 | 73 | def initialize(self, config): 74 | self.config_file = config['websettings']['config_file'] 75 | 76 | def get(self): 77 | templateLoader = jinja2.FileSystemLoader(searchpath="/") 78 | templateEnv = jinja2.Environment(loader=templateLoader) 79 | template = templateEnv.get_template(template_file) 80 | 81 | # Read config file 82 | try: 83 | iniconfig = ConfigObj(self.config_file, configspec=spec_file, 84 | file_error=True, encoding='utf8') 85 | except (ConfigObjError, IOError) as e: 86 | error = 'Could not load ini file! %s' % e 87 | logger.error(error) 88 | self.write(error) 89 | return 90 | 91 | templateVars = { 92 | 'change_root_password': can_change_root_password(iniconfig) 93 | } 94 | # Read values of valid items (in the spec-file) 95 | validItems = ConfigObj(spec_file, encoding='utf8') 96 | # Iterate over the valid items to get them into the template 97 | for item in validItems: 98 | for subitem in validItems[item]: 99 | configValue = iniconfig.get(item, {}).get(subitem, None) 100 | if configValue is not None: 101 | itemName = get_name(item, subitem) 102 | templateVars[itemName] = configValue 103 | 104 | self.write(template.render(templateVars)) 105 | 106 | 107 | class WebPostRequestHandler(tornado.web.RequestHandler): 108 | 109 | def initialize(self, config): 110 | self.config_file = config.get('websettings')['config_file'] 111 | 112 | def needs_reboot(self, item, subItem): 113 | reboot_sections = ['network', 'musicbox'] 114 | restart_subsections = ['webclient'] 115 | if item in reboot_sections: 116 | # Restarting Mopidy is sufficient for some subsections. 117 | if subItem not in restart_subsections: 118 | return True 119 | return False 120 | 121 | def post(self): 122 | apply_html = '' 123 | apply_string = 'restart Mopidy' 124 | status = '' 125 | try: 126 | iniconfig = ConfigObj(self.config_file, configspec=spec_file, 127 | file_error=True, encoding='utf8') 128 | validItems = ConfigObj(spec_file, encoding='utf8') 129 | # Iterate over the items, so that only valid items are processed 130 | for item in validItems: 131 | for subitem in validItems[item]: 132 | itemName = get_name(item, subitem) 133 | 134 | if itemName == get_name('musicbox', 'root_password') and \ 135 | not can_change_root_password(iniconfig): 136 | continue 137 | 138 | value = self.get_argument(itemName, default='') 139 | if value: 140 | # Create default entry if it doesn't already exist 141 | oldItem = iniconfig.setdefault(item, {}).setdefault( 142 | subitem, '') 143 | # Does changing the setting requires a system reboot. 144 | if oldItem != value and \ 145 | self.needs_reboot(item, subitem): 146 | apply_string = 'reboot system' 147 | iniconfig[item][subitem] = value 148 | # Ensure that some of the settings are consistent. 149 | try: 150 | if iniconfig['audio']['mixer'] == 'alsamixer': 151 | iniconfig['alsamixer']['enabled'] = 'true' 152 | else: 153 | iniconfig['alsamixer']['enabled'] = 'false' 154 | # spotify and spotify_web should have the same values here. 155 | for subitem in ['client_id', 'client_secret']: 156 | iniconfig['spotify'][subitem] = ( 157 | iniconfig['spotify'][subitem] or 158 | iniconfig['spotify_web'][subitem]) 159 | iniconfig['spotify_web'][subitem] = ( 160 | iniconfig['spotify'][subitem]) 161 | except KeyError as e: 162 | logger.error('Error making config consistent %s', e) 163 | 164 | iniconfig.write() 165 | status = 'Settings Saved!' 166 | apply_html = ('
' 167 | '' 169 | ) 170 | except (ConfigObjError, IOError) as e: 171 | status = 'Could not load ini file! %s' % e 172 | logger.error(status) 173 | 174 | message = ('' 175 | '' 176 | '

' + status + '

' 177 | '

' + apply_html + 178 | '

' 179 | 'Home' 180 | '

' 181 | '' 182 | '') 183 | self.write(message) 184 | 185 | 186 | class WebApplyRequestHandler(tornado.web.RequestHandler): 187 | 188 | def initialize(self): pass 189 | 190 | def post(self): 191 | method = self.get_argument('method', None) 192 | if method is not None: 193 | if 'reboot' in method: 194 | status = 'Rebooting Musicbox system...' 195 | os.system("sudo shutdown -r now") 196 | elif 'restart' in method: 197 | status = 'Restarting Mopidy service...' 198 | os.system("sudo /etc/init.d/mopidy restart") 199 | else: 200 | status = 'Error while applying ' + method 201 | logger.info(status) 202 | message = ('' 203 | '' 204 | '

' + status + '

' 205 | '

' 206 | 'Home' 207 | '

' 208 | '' 209 | '') 210 | self.write(message) 211 | 212 | 213 | class WebRebootRequestHandler(tornado.web.RequestHandler): 214 | 215 | def initialize(self): pass 216 | 217 | def post(self): 218 | logger.info('Halting system') 219 | os.system("sudo shutdown -r now") 220 | os.system("shutdown -r now") 221 | 222 | 223 | class WebShutdownRequestHandler(tornado.web.RequestHandler): 224 | 225 | def initialize(self): pass 226 | 227 | def post(self): 228 | logger.info('Halting system') 229 | os.system("sudo shutdown -h now") 230 | os.system("shutdown -h now") 231 | 232 | 233 | def websettings_app_factory(config, core): 234 | from mopidy.http.handlers import StaticFileHandler 235 | path = os.path.join(os.path.dirname(__file__), 'js') 236 | return [ 237 | ('/', WebSettingsRequestHandler, {'config': config}), 238 | ('/save', WebPostRequestHandler, {'config': config}), 239 | ('/apply', WebApplyRequestHandler,), 240 | ('/reboot', WebRebootRequestHandler,), 241 | ('/shutdown', WebShutdownRequestHandler,), 242 | (r'/js/(.*)', StaticFileHandler, {'path': path}) 243 | ] 244 | -------------------------------------------------------------------------------- /mopidy_websettings/ext.conf: -------------------------------------------------------------------------------- 1 | [websettings] 2 | enabled = true 3 | musicbox = false 4 | config_file = /etc/mopidy/mopidy.conf 5 | -------------------------------------------------------------------------------- /mopidy_websettings/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | MusicBox Settings 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 42 | 43 | 44 | 45 |
46 |

Settings

47 |
{{ error }}
48 | 49 | 50 | {% if websettings__musicbox == "true" %} 51 | 52 |
53 |

Network

54 | 55 | 56 | 57 |

Enter your passphrase or 64 character hex key.

58 | 59 | 60 |

Optionally specify your country code 61 | to ensure all available wifi channels are made enabled. For example: GB

62 | 63 | 64 | 65 |

Here you can change the default workgroup of the Windows network. This will set the workgroup to the name you want

66 | 67 | 68 | 69 | {% if change_root_password %} 70 |

You should change the root password before enabling this.

71 | {% endif %} 72 |
73 | 77 |
78 | 79 | 80 |

Enable this to block network connections on ports other than those explicitly required by Musicbox. 81 | This may prevent any additional network programs from working correctly. 82 | This should be enabled alongside other security hardening methods if using Musicbox outside of your LAN (not recommended).

83 |
84 | 88 |
89 |
90 | 91 |
92 |

MusicBox

93 | 94 | {% if change_root_password %} 95 |

To secure your device you should change the default system password to something else.

96 | 97 | {% else %} 98 |

Manually edit settings.ini to change the system password again.

99 | {% endif %} 100 | 101 |

Set the name of the MusicBox. In this way you can create different devices on the same network (in different rooms). The webinterface is accessible via the name you use here, e.g. http://kitchen.local/, and multiple devices will show up in AirTunes. You can only use normal caracters and numbers in the name (no spaces, dots, etc). You can even have different devices with different Spotify accounts when needed.

102 | 103 | 104 | 105 |

MusicBox can automatically start playing a stream or track at startup. The URI to use can be obtained from the Streams page, the track's popup menu, or get currently playing.

106 | 107 | 108 | 109 |

The maximum waiting time (in seconds) for the system to become ready for autoplaying. If this expires, autoplaying is skipped. Some systems are slow to start and may require longer.

110 | 111 | 112 | 113 |

Set the default webclient.

114 |
115 | 123 |
124 | 125 | 126 |

Monitor and automatically restart internet streams when the network connection is restored following a dropout.

127 |
128 | 132 |
133 | 134 | 135 |

Enable streaming audio to MusicBox from iPhone/iPad/Mac/iPod using ShairPort-Sync (AirPlay).

136 |
137 | 141 |
142 | 143 | 144 |

Enable streaming audio to MusicBox from DLNA/UPnP/OpenHome devices using upmpdcli (UPnP).

145 |
146 | 150 |
151 | 152 | 153 |

Select the logging level required. Debug logging will be found in /tmp/mopidy-debug.log

154 |
155 | 159 |
160 |
161 | 162 | {% endif %} 163 | 164 |
165 |

Audio

166 | 167 | 168 | 171 | 172 | {% if websettings__musicbox == "true" %} 173 | 174 | 175 |

Automatic detection selects USB audio if a USB audio device is found, 176 | HDMI if a HDMI device is detected at boot, otherwise analog output. 177 | Addon cards from JustBoom, HifiBerry, IQ Audio etc cannot be detected automatically and must be explicitly set.

178 | 198 | 199 | 200 |

Because of limitations with some USB-DACs, MusicBox downsamples USB sound to 44k by default. You can disable this here.

201 |
202 | 206 |
207 | 208 | 209 |

For some USB audiocards (DACs), you can enable hardware mixing of the volume.

210 |
211 | 215 |
216 | 217 | {% endif %} 218 | 219 |
220 | 221 | {% if websettings__musicbox == "true" %} 222 | 223 |
224 |

Music files

225 | 226 |

Use this setting to let Mopidy/MusicBox scan on startup for new music files on the SD Card, USB or the Network. This could take a while! This can slowdown the boot a lot. Disable it again if your music doesn't change.

227 |
228 | 232 |
233 | 234 | 235 |

Mount Windows Network Drive on boot. Type the address exactly like this (samba mount): //servername/mountpoint/directory e.g. //192.168.1.5/musicmount or //server.local/shared/music
236 | If the mount needs a username/password, also set it (leave empty for guest-access). Don't forget to let Mopidy/MusicBox scan the contents at first boot (see MusicBox Settings section)

237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 |

Depending on your file server you may need to specify a different protocol version or other options. 247 | See here for further details.

248 | 249 | 250 |
251 | 252 | {% endif %} 253 | 254 |

Services

255 |
256 |

Spotify

257 |

Enable playing music from Spotify. This now also requires additional credentials obtained from 258 | mopidy.com/ext/spotify 259 |

260 |
261 | 265 |
266 | 267 | 268 | 269 | 270 | 271 |

From mopidy.com/ext/spotify 272 | e.g. 1b3c56f8-2a4e-1b3d-a9b2-1d3e56f89c

273 | 274 | 275 |

From mopidy.com/ext/spotify 276 | e.g. aBcdEFGhj-jk12z3548d8sma-LKdDYvbR674GDGDa=

277 | 278 | 279 | 284 | 285 | 286 |

Enable streaming audio via Spotify Connect (librespot). ** BETA **

287 | 291 | 292 | 293 |

Enable extended Spotify browsing

294 | 298 |
299 | 300 |
301 |

AudioAddict

302 |

Enable playing music from the AudioAddict network of sites, including Digitally Imported, RadioTunes, RockRadio, JazzRadio and FrescaRadio.

303 |
304 | 308 |
309 | 310 | 311 | 312 | 313 | 314 | 320 |
321 | 322 | 326 |
327 |
328 | 329 | 333 |
334 |
335 | 336 | 340 |
341 |
342 | 343 | 347 |
348 |
349 | 350 | 354 |
355 |
356 | 357 |
358 |

SoundCloud

359 |

SoundCloud requires credentials obtained from 360 | mopidy.com/ext/soundcloud 361 |

362 |
363 | 367 |
368 | 369 |

Fill the token here e.g. 1-1111-1111111

370 | 371 |
372 | 373 |
374 |

Google Music

375 |

Please provide your username, password to enable Google Music.

376 |
377 | 381 |
382 | 383 | 384 | 385 |

If you use 2-way authentication, you need an application-specific password.

386 | 387 |

You may need to supply a device ID. See here for more information.

388 | 389 |
390 | 391 |
392 |

Last.FM

393 |

Please provide your username, password to enable scrobbling music to Last.FM.

394 |
395 | 399 |
400 | 401 | 402 | 403 | 404 |
405 | 406 |
407 |

YouTube

408 |

Play sound from YouTube videos (use search to find them)

409 |
410 | 414 |
415 |
416 | 417 |
418 |

Podcasts

419 |

Browse podcasts distributed as RSS feeds and play individual episodes in a variety of audio formats. 420 | Edit the subscription file at /boot/config/Podcasts.opml

421 |
422 | 426 |
427 | 433 |
434 | 435 |
436 |

TuneIn

437 |

Play radio stations from TuneIn.

438 |
439 | 443 |
444 |
445 | 446 |
447 |

Dirble

448 |

Play radio stations from Dirble.

449 |
450 | 454 |
455 | 456 | 460 | 461 |

Optionally you can get an api-key by creating an account at Dirble.com. Then go to the section 'Your API-Keys' and copy the key you see and fill it in e.g 473279e3fa0e7010cbbbb40ecc31890d46e57a2e

462 | 463 | 464 |
465 | 466 |
467 |

Soma FM

468 |

Play radiostations from Soma FM.

469 |
470 | 474 |
475 |
476 | 477 |
478 |

Local Files

479 |

Play audio files from this device or the network

480 |
481 | 485 |
486 |
487 | 488 |
489 |

The Internet Archive

490 |

Play (old) sounds, music and speech from The Internet Archive

491 |
492 | 496 |
497 |
498 | 499 |
500 |

Tidal

501 |

Enable playing music from Tidal.

502 |
503 | 507 |
508 | 509 | 510 | 511 | 512 | 513 |

Lossless quality requires a Tidal HiFi Subscription

514 | 519 |
520 | 521 | 548 | 549 | 550 |

551 |
552 | 553 | 554 | 555 | 556 | -------------------------------------------------------------------------------- /mopidy_websettings/js/mopidy.min.js: -------------------------------------------------------------------------------- 1 | /*! Mopidy.js v0.5.0 - built 2015-01-31 2 | * http://www.mopidy.com/ 3 | * Copyright (c) 2015 Stein Magnus Jodal and contributors 4 | * Licensed under the Apache License, Version 2.0 */ 5 | !function(a){if("object"==typeof exports)module.exports=a();else if("function"==typeof define&&define.amd)define(a);else{var b;"undefined"!=typeof window?b=window:"undefined"!=typeof global?b=global:"undefined"!=typeof self&&(b=self),b.Mopidy=a()}}(function(){var a;return function b(a,c,d){function e(g,h){if(!c[g]){if(!a[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};a[g][0].call(j.exports,function(b){var c=a[g][1][b];return e(c?c:b)},j,j.exports,b,a,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g0)for(d=0;e>d;++d)c[d](a,b);else setTimeout(function(){throw b.message=a+" listener threw error: "+b.message,b},0)}function b(a){if("function"!=typeof a)throw new TypeError("Listener is not function");return a}function c(a){return a.supervisors||(a.supervisors=[]),a.supervisors}function d(a,b){return a.listeners||(a.listeners={}),b&&!a.listeners[b]&&(a.listeners[b]=[]),b?a.listeners[b]:a.listeners}function e(a){return a.errbacks||(a.errbacks=[]),a.errbacks}function f(f){function h(b,c,d){try{c.listener.apply(c.thisp||f,d)}catch(g){a(b,g,e(f))}}return f=f||{},f.on=function(a,e,f){return"function"==typeof a?c(this).push({listener:a,thisp:e}):void d(this,a).push({listener:b(e),thisp:f})},f.off=function(a,b){var f,g,h,i;if(!a){f=c(this),f.splice(0,f.length),g=d(this);for(h in g)g.hasOwnProperty(h)&&(f=d(this,h),f.splice(0,f.length));return f=e(this),void f.splice(0,f.length)}if("function"==typeof a?(f=c(this),b=a):f=d(this,a),!b)return void f.splice(0,f.length);for(h=0,i=f.length;i>h;++h)if(f[h].listener===b)return void f.splice(h,1)},f.once=function(a,b,c){var d=function(){f.off(a,d),b.apply(this,arguments)};f.on(a,d,c)},f.bind=function(a,b){var c,d,e;if(b)for(d=0,e=b.length;e>d;++d){if("function"!=typeof a[b[d]])throw new Error("No such method "+b[d]);this.on(b[d],a[b[d]],a)}else for(c in a)"function"==typeof a[c]&&this.on(c,a[c],a);return a},f.emit=function(a){var b,e,f=c(this),i=g.call(arguments);for(b=0,e=f.length;e>b;++b)h(a,f[b],i);for(f=d(this,a).slice(),i=g.call(arguments,1),b=0,e=f.length;e>b;++b)h(a,f[b],i)},f.errback=function(a){this.errbacks||(this.errbacks=[]),this.errbacks.push(b(a))},f}var g=Array.prototype.slice;return{createEventEmitter:f,aggregate:function(a){var b=f();return a.forEach(function(a){a.on(function(a,c){b.emit(a,c)})}),b}}})},{}],3:[function(a,b){function c(){}var d=b.exports={};d.nextTick=function(){var a="undefined"!=typeof window&&window.setImmediate,b="undefined"!=typeof window&&window.postMessage&&window.addEventListener;if(a)return function(a){return window.setImmediate(a)};if(b){var c=[];return window.addEventListener("message",function(a){var b=a.source;if((b===window||null===b)&&"process-tick"===a.data&&(a.stopPropagation(),c.length>0)){var d=c.shift();d()}},!0),function(a){c.push(a),window.postMessage("process-tick","*")}}return function(a){setTimeout(a,0)}}(),d.title="browser",d.browser=!0,d.env={},d.argv=[],d.on=c,d.addListener=c,d.once=c,d.off=c,d.removeListener=c,d.removeAllListeners=c,d.emit=c,d.binding=function(){throw new Error("process.binding is not supported")},d.cwd=function(){return"/"},d.chdir=function(){throw new Error("process.chdir is not supported")}},{}],4:[function(b,c){!function(a){"use strict";a(function(a){var b=a("./makePromise"),c=a("./Scheduler"),d=a("./env").asap;return b({scheduler:new c(d)})})}("function"==typeof a&&a.amd?a:function(a){c.exports=a(b)})},{"./Scheduler":5,"./env":17,"./makePromise":19}],5:[function(b,c){!function(a){"use strict";a(function(){function a(a){this._async=a,this._running=!1,this._queue=this,this._queueLen=0,this._afterQueue={},this._afterQueueLen=0;var b=this;this.drain=function(){b._drain()}}return a.prototype.enqueue=function(a){this._queue[this._queueLen++]=a,this.run()},a.prototype.afterQueue=function(a){this._afterQueue[this._afterQueueLen++]=a,this.run()},a.prototype.run=function(){this._running||(this._running=!0,this._async(this.drain))},a.prototype._drain=function(){for(var a=0;a>>0,j=i,k=[],l=0;i>l;++l)if(f=b[l],void 0!==f||l in b){if(e=a._handler(f),e.state()>0){h.become(e),a._visitRemaining(b,l,e);break}e.visit(h,c,d)}else--j;return 0===j&&h.reject(new RangeError("any(): array must not be empty")),g}function e(b,c){function d(a){this.resolved||(k.push(a),0===--n&&(l=null,this.resolve(k)))}function e(a){this.resolved||(l.push(a),0===--f&&(k=null,this.reject(l)))}var f,g,h,i=a._defer(),j=i._handler,k=[],l=[],m=b.length>>>0,n=0;for(h=0;m>h;++h)g=b[h],(void 0!==g||h in b)&&++n;for(c=Math.max(c,0),f=n-c+1,n=Math.min(c,n),c>n?j.reject(new RangeError("some(): array must contain at least "+c+" item(s), but had "+n)):0===n&&j.resolve(k),h=0;m>h;++h)g=b[h],(void 0!==g||h in b)&&a._handler(g).visit(j,d,e,j.notify);return i}function f(b,c){return a._traverse(c,b)}function g(b,c){var d=s.call(b);return a._traverse(c,d).then(function(a){return h(d,a)})}function h(b,c){for(var d=c.length,e=new Array(d),f=0,g=0;d>f;++f)c[f]&&(e[g++]=a._handler(b[f]).value);return e.length=g,e}function i(a){return p(a.map(j))}function j(c){var d=a._handler(c);return 0===d.state()?o(c).then(b.fulfilled,b.rejected):(d._unreport(),b.inspect(d))}function k(a,b){return arguments.length>2?q.call(a,m(b),arguments[2]):q.call(a,m(b))}function l(a,b){return arguments.length>2?r.call(a,m(b),arguments[2]):r.call(a,m(b))}function m(a){return function(b,c,d){return n(a,void 0,[b,c,d])}}var n=c(a),o=a.resolve,p=a.all,q=Array.prototype.reduce,r=Array.prototype.reduceRight,s=Array.prototype.slice;return a.any=d,a.some=e,a.settle=i,a.map=f,a.filter=g,a.reduce=k,a.reduceRight=l,a.prototype.spread=function(a){return this.then(p).then(function(b){return a.apply(this,b)})},a}})}("function"==typeof a&&a.amd?a:function(a){c.exports=a(b)})},{"../apply":7,"../state":20}],9:[function(b,c){!function(a){"use strict";a(function(){function a(){throw new TypeError("catch predicate must be a function")}function b(a,b){return c(b)?a instanceof b:b(a)}function c(a){return a===Error||null!=a&&a.prototype instanceof Error}function d(a){return("object"==typeof a||"function"==typeof a)&&null!==a}function e(a){return a}return function(c){function f(a,c){return function(d){return b(d,c)?a.call(this,d):j(d)}}function g(a,b,c,e){var f=a.call(b);return d(f)?h(f,c,e):c(e)}function h(a,b,c){return i(a).then(function(){return b(c)})}var i=c.resolve,j=c.reject,k=c.prototype["catch"];return c.prototype.done=function(a,b){this._handler.visit(this._handler.receiver,a,b)},c.prototype["catch"]=c.prototype.otherwise=function(b){return arguments.length<2?k.call(this,b):"function"!=typeof b?this.ensure(a):k.call(this,f(arguments[1],b))},c.prototype["finally"]=c.prototype.ensure=function(a){return"function"!=typeof a?this:this.then(function(b){return g(a,this,e,b)},function(b){return g(a,this,j,b)})},c.prototype["else"]=c.prototype.orElse=function(a){return this.then(void 0,function(){return a})},c.prototype["yield"]=function(a){return this.then(function(){return a})},c.prototype.tap=function(a){return this.then(a)["yield"](this)},c}})}("function"==typeof a&&a.amd?a:function(a){c.exports=a()})},{}],10:[function(b,c){!function(a){"use strict";a(function(){return function(a){return a.prototype.fold=function(b,c){var d=this._beget();return this._handler.fold(function(c,d,e){a._handler(c).fold(function(a,c,d){d.resolve(b.call(this,c,a))},d,this,e)},c,d._handler.receiver,d._handler),d},a}})}("function"==typeof a&&a.amd?a:function(a){c.exports=a()})},{}],11:[function(b,c){!function(a){"use strict";a(function(a){var b=a("../state").inspect;return function(a){return a.prototype.inspect=function(){return b(a._handler(this))},a}})}("function"==typeof a&&a.amd?a:function(a){c.exports=a(b)})},{"../state":20}],12:[function(b,c){!function(a){"use strict";a(function(){return function(a){function b(a,b,d,e){return c(function(b){return[b,a(b)]},b,d,e)}function c(a,b,e,f){function g(f,g){return d(e(f)).then(function(){return c(a,b,e,g)})}return d(f).then(function(c){return d(b(c)).then(function(b){return b?c:d(a(c)).spread(g)})})}var d=a.resolve;return a.iterate=b,a.unfold=c,a}})}("function"==typeof a&&a.amd?a:function(a){c.exports=a()})},{}],13:[function(b,c){!function(a){"use strict";a(function(){return function(a){return a.prototype.progress=function(a){return this.then(void 0,void 0,a)},a}})}("function"==typeof a&&a.amd?a:function(a){c.exports=a()})},{}],14:[function(b,c){!function(a){"use strict";a(function(a){function b(a,b,d,e){return c.setTimer(function(){a(d,e,b)},b)}var c=a("../env"),d=a("../TimeoutError");return function(a){function e(a,c,d){b(f,a,c,d)}function f(a,b){b.resolve(a)}function g(a,b,c){var e="undefined"==typeof a?new d("timed out after "+c+"ms"):a;b.reject(e)}return a.prototype.delay=function(a){var b=this._beget();return this._handler.fold(e,a,void 0,b._handler),b},a.prototype.timeout=function(a,d){var e=this._beget(),f=e._handler,h=b(g,a,d,e._handler);return this._handler.visit(f,function(a){c.clearTimer(h),this.resolve(a)},function(a){c.clearTimer(h),this.reject(a)},f.notify),e},a}})}("function"==typeof a&&a.amd?a:function(a){c.exports=a(b)})},{"../TimeoutError":6,"../env":17}],15:[function(b,c){!function(a){"use strict";a(function(a){function b(a){throw a}function c(){}var d=a("../env").setTimer,e=a("../format");return function(a){function f(a){a.handled||(n.push(a),k("Potentially unhandled rejection ["+a.id+"] "+e.formatError(a.value)))}function g(a){var b=n.indexOf(a);b>=0&&(n.splice(b,1),l("Handled previous rejection ["+a.id+"] "+e.formatObject(a.value)))}function h(a,b){m.push(a,b),null===o&&(o=d(i,0))}function i(){for(o=null;m.length>0;)m.shift()(m.shift())}var j,k=c,l=c;"undefined"!=typeof console&&(j=console,k="undefined"!=typeof j.error?function(a){j.error(a)}:function(a){j.log(a)},l="undefined"!=typeof j.info?function(a){j.info(a)}:function(a){j.log(a)}),a.onPotentiallyUnhandledRejection=function(a){h(f,a)},a.onPotentiallyUnhandledRejectionHandled=function(a){h(g,a)},a.onFatalRejection=function(a){h(b,a.value)};var m=[],n=[],o=null;return a}})}("function"==typeof a&&a.amd?a:function(a){c.exports=a(b)})},{"../env":17,"../format":18}],16:[function(b,c){!function(a){"use strict";a(function(){return function(a){return a.prototype["with"]=a.prototype.withThis=function(a){var b=this._beget(),c=b._handler;return c.receiver=a,this._handler.chain(c,a),b},a}})}("function"==typeof a&&a.amd?a:function(a){c.exports=a()})},{}],17:[function(b,c){(function(d){!function(a){"use strict";a(function(a){function b(){return"undefined"!=typeof d&&null!==d&&"function"==typeof d.nextTick}function c(){return"function"==typeof MutationObserver&&MutationObserver||"function"==typeof WebKitMutationObserver&&WebKitMutationObserver}function e(a){function b(){var a=c;c=void 0,a()}var c,d=document.createTextNode(""),e=new a(b);e.observe(d,{characterData:!0});var f=0;return function(a){c=a,d.data=f^=1}}var f,g="undefined"!=typeof setTimeout&&setTimeout,h=function(a,b){return setTimeout(a,b)},i=function(a){return clearTimeout(a)},j=function(a){return g(a,0)};if(b())j=function(a){return d.nextTick(a)};else if(f=c())j=e(f);else if(!g){var k=a,l=k("vertx");h=function(a,b){return l.setTimer(b,a)},i=l.cancelTimer,j=l.runOnLoop||l.runOnContext}return{setTimer:h,clearTimer:i,asap:j}})}("function"==typeof a&&a.amd?a:function(a){c.exports=a(b)})}).call(this,b("FWaASH"))},{FWaASH:3}],18:[function(b,c){!function(a){"use strict";a(function(){function a(a){var c="object"==typeof a&&null!==a&&a.stack?a.stack:b(a);return a instanceof Error?c:c+" (WARNING: non-Error used)"}function b(a){var b=String(a);return"[object Object]"===b&&"undefined"!=typeof JSON&&(b=c(a,b)),b}function c(a,b){try{return JSON.stringify(a)}catch(c){return b}}return{formatError:a,formatObject:b,tryStringify:c}})}("function"==typeof a&&a.amd?a:function(a){c.exports=a()})},{}],19:[function(b,c){(function(b){!function(a){"use strict";a(function(){return function(a){function c(a,b){this._handler=a===u?b:d(a)}function d(a){function b(a){e.resolve(a)}function c(a){e.reject(a)}function d(a){e.notify(a)}var e=new w;try{a(b,c,d)}catch(f){c(f)}return e}function e(a){return J(a)?a:new c(u,new x(r(a)))}function f(a){return new c(u,new x(new A(a)))}function g(){return ab}function h(){return new c(u,new w)}function i(a,b){var c=new w(a.receiver,a.join().context);return new b(u,c)}function j(a){return l(T,null,a)}function k(a,b){return l(O,a,b)}function l(a,b,d){function e(c,e,g){g.resolved||m(d,f,c,a(b,e,c),g)}function f(a,b,c){k[a]=b,0===--j&&c.become(new z(k))}for(var g,h="function"==typeof b?e:f,i=new w,j=d.length>>>0,k=new Array(j),l=0;l0?b(c,f.value,e):(e.become(f),n(a,c+1,f))}else b(c,d,e)}function n(a,b,c){for(var d=b;dc&&a._unreport()}}function p(a){return"object"!=typeof a||null===a?f(new TypeError("non-iterable passed to race()")):0===a.length?g():1===a.length?e(a[0]):q(a)}function q(a){var b,d,e,f=new w;for(b=0;b0||"function"!=typeof b&&0>e)return new this.constructor(u,d);var f=this._beget(),g=f._handler;return d.chain(g,d.receiver,a,b,c),f},c.prototype["catch"]=function(a){return this.then(void 0,a)},c.prototype._beget=function(){return i(this._handler,this.constructor)},c.all=j,c.race=p,c._traverse=k,c._visitRemaining=n,u.prototype.when=u.prototype.become=u.prototype.notify=u.prototype.fail=u.prototype._unreport=u.prototype._report=U,u.prototype._state=0,u.prototype.state=function(){return this._state},u.prototype.join=function(){for(var a=this;void 0!==a.handler;)a=a.handler;return a},u.prototype.chain=function(a,b,c,d,e){this.when({resolver:a,receiver:b,fulfilled:c,rejected:d,progress:e})},u.prototype.visit=function(a,b,c,d){this.chain(Z,a,b,c,d)},u.prototype.fold=function(a,b,c,d){this.when(new I(a,b,c,d))},S(u,v),v.prototype.become=function(a){a.fail()};var Z=new v;S(u,w),w.prototype._state=0,w.prototype.resolve=function(a){this.become(r(a))},w.prototype.reject=function(a){this.resolved||this.become(new A(a))},w.prototype.join=function(){if(!this.resolved)return this;for(var a=this;void 0!==a.handler;)if(a=a.handler,a===this)return this.handler=D();return a},w.prototype.run=function(){var a=this.consumers,b=this.handler;this.handler=this.handler.join(),this.consumers=void 0;for(var c=0;c0?c(d.value):b(d.value)}return{pending:a,fulfilled:c,rejected:b,inspect:d}})}("function"==typeof a&&a.amd?a:function(a){c.exports=a()})},{}],21:[function(b,c){!function(a){"use strict";a(function(a){function b(a,b,c,d){var e=x.resolve(a);return arguments.length<2?e:e.then(b,c,d)}function c(a){return new x(a)}function d(a){return function(){for(var b=0,c=arguments.length,d=new Array(c);c>b;++b)d[b]=arguments[b];return y(a,this,d)}}function e(a){for(var b=0,c=arguments.length-1,d=new Array(c);c>b;++b)d[b]=arguments[b+1];return y(a,this,d)}function f(){return new g}function g(){function a(a){d._handler.resolve(a)}function b(a){d._handler.reject(a)}function c(a){d._handler.notify(a)}var d=x._defer();this.promise=d,this.resolve=a,this.reject=b,this.notify=c,this.resolver={resolve:a,reject:b,notify:c}}function h(a){return a&&"function"==typeof a.then}function i(){return x.all(arguments)}function j(a){return b(a,x.all)}function k(a){return b(a,x.settle)}function l(a,c){return b(a,function(a){return x.map(a,c)})}function m(a,c){return b(a,function(a){return x.filter(a,c)})}var n=a("./lib/decorators/timed"),o=a("./lib/decorators/array"),p=a("./lib/decorators/flow"),q=a("./lib/decorators/fold"),r=a("./lib/decorators/inspect"),s=a("./lib/decorators/iterate"),t=a("./lib/decorators/progress"),u=a("./lib/decorators/with"),v=a("./lib/decorators/unhandledRejection"),w=a("./lib/TimeoutError"),x=[o,p,q,s,t,r,u,n,v].reduce(function(a,b){return b(a)},a("./lib/Promise")),y=a("./lib/apply")(x);return b.promise=c,b.resolve=x.resolve,b.reject=x.reject,b.lift=d,b["try"]=e,b.attempt=e,b.iterate=x.iterate,b.unfold=x.unfold,b.join=i,b.all=j,b.settle=k,b.any=d(x.any),b.some=d(x.some),b.race=d(x.race),b.map=l,b.filter=m,b.reduce=d(x.reduce),b.reduceRight=d(x.reduceRight),b.isPromiseLike=h,b.Promise=x,b.defer=f,b.TimeoutError=w,b})}("function"==typeof a&&a.amd?a:function(a){c.exports=a(b)})},{"./lib/Promise":4,"./lib/TimeoutError":6,"./lib/apply":7,"./lib/decorators/array":8,"./lib/decorators/flow":9,"./lib/decorators/fold":10,"./lib/decorators/inspect":11,"./lib/decorators/iterate":12,"./lib/decorators/progress":13,"./lib/decorators/timed":14,"./lib/decorators/unhandledRejection":15,"./lib/decorators/with":16}],22:[function(a,b){function c(a){return this instanceof c?(this._console=this._getConsole(a||{}),this._settings=this._configure(a||{}),this._backoffDelay=this._settings.backoffDelayMin,this._pendingRequests={},this._webSocket=null,d.createEventEmitter(this),this._delegateEvents(),void(this._settings.autoConnect&&this.connect())):new c(a)}var d=a("bane"),e=a("../lib/websocket/"),f=a("when");c.ConnectionError=function(a){this.name="ConnectionError",this.message=a},c.ConnectionError.prototype=Object.create(Error.prototype),c.ConnectionError.prototype.constructor=c.ConnectionError,c.ServerError=function(a){this.name="ServerError",this.message=a},c.ServerError.prototype=Object.create(Error.prototype),c.ServerError.prototype.constructor=c.ServerError,c.WebSocket=e.Client,c.when=f,c.prototype._getConsole=function(a){if("undefined"!=typeof a.console)return a.console;var b="undefined"!=typeof console&&console||{};return b.log=b.log||function(){},b.warn=b.warn||function(){},b.error=b.error||function(){},b},c.prototype._configure=function(a){var b="undefined"!=typeof document&&"https:"===document.location.protocol?"wss://":"ws://",c="undefined"!=typeof document&&document.location.host||"localhost";return a.webSocketUrl=a.webSocketUrl||b+c+"/mopidy/ws",a.autoConnect!==!1&&(a.autoConnect=!0),a.backoffDelayMin=a.backoffDelayMin||1e3,a.backoffDelayMax=a.backoffDelayMax||64e3,"undefined"==typeof a.callingConvention&&this._console.warn("Mopidy.js is using the default calling convention. The default will change in the future. You should explicitly specify which calling convention you use."),a.callingConvention=a.callingConvention||"by-position-only",a},c.prototype._delegateEvents=function(){this.off("websocket:close"),this.off("websocket:error"),this.off("websocket:incomingMessage"),this.off("websocket:open"),this.off("state:offline"),this.on("websocket:close",this._cleanup),this.on("websocket:error",this._handleWebSocketError),this.on("websocket:incomingMessage",this._handleMessage),this.on("websocket:open",this._resetBackoffDelay),this.on("websocket:open",this._getApiSpec),this.on("state:offline",this._reconnect)},c.prototype.connect=function(){if(this._webSocket){if(this._webSocket.readyState===c.WebSocket.OPEN)return;this._webSocket.close()}this._webSocket=this._settings.webSocket||new c.WebSocket(this._settings.webSocketUrl),this._webSocket.onclose=function(a){this.emit("websocket:close",a)}.bind(this),this._webSocket.onerror=function(a){this.emit("websocket:error",a)}.bind(this),this._webSocket.onopen=function(){this.emit("websocket:open")}.bind(this),this._webSocket.onmessage=function(a){this.emit("websocket:incomingMessage",a)}.bind(this)},c.prototype._cleanup=function(a){Object.keys(this._pendingRequests).forEach(function(b){var d=this._pendingRequests[b];delete this._pendingRequests[b];var e=new c.ConnectionError("WebSocket closed");e.closeEvent=a,d.reject(e)}.bind(this)),this.emit("state:offline")},c.prototype._reconnect=function(){this.emit("reconnectionPending",{timeToAttempt:this._backoffDelay}),setTimeout(function(){this.emit("reconnecting"),this.connect()}.bind(this),this._backoffDelay),this._backoffDelay=2*this._backoffDelay,this._backoffDelay>this._settings.backoffDelayMax&&(this._backoffDelay=this._settings.backoffDelayMax)},c.prototype._resetBackoffDelay=function(){this._backoffDelay=this._settings.backoffDelayMin},c.prototype.close=function(){this.off("state:offline",this._reconnect),this._webSocket.close()},c.prototype._handleWebSocketError=function(a){this._console.warn("WebSocket error:",a.stack||a)},c.prototype._send=function(a){switch(this._webSocket.readyState){case c.WebSocket.CONNECTING:return f.reject(new c.ConnectionError("WebSocket is still connecting"));case c.WebSocket.CLOSING:return f.reject(new c.ConnectionError("WebSocket is closing"));case c.WebSocket.CLOSED:return f.reject(new c.ConnectionError("WebSocket is closed"));default:var b=f.defer();return a.jsonrpc="2.0",a.id=this._nextRequestId(),this._pendingRequests[a.id]=b.resolver,this._webSocket.send(JSON.stringify(a)),this.emit("websocket:outgoingMessage",a),b.promise}},c.prototype._nextRequestId=function(){var a=-1;return function(){return a+=1}}(),c.prototype._handleMessage=function(a){try{var b=JSON.parse(a.data);b.hasOwnProperty("id")?this._handleResponse(b):b.hasOwnProperty("event")?this._handleEvent(b):this._console.warn("Unknown message type received. Message was: "+a.data)}catch(c){if(!(c instanceof SyntaxError))throw c;this._console.warn("WebSocket message parsing failed. Message was: "+a.data)}},c.prototype._handleResponse=function(a){if(!this._pendingRequests.hasOwnProperty(a.id))return void this._console.warn("Unexpected response received. Message was:",a);var b,d=this._pendingRequests[a.id];delete this._pendingRequests[a.id],a.hasOwnProperty("result")?d.resolve(a.result):a.hasOwnProperty("error")?(b=new c.ServerError(a.error.message),b.code=a.error.code,b.data=a.error.data,d.reject(b),this._console.warn("Server returned error:",a.error)):(b=new Error("Response without 'result' or 'error' received"),b.data={response:a},d.reject(b),this._console.warn("Response without 'result' or 'error' received. Message was:",a))},c.prototype._handleEvent=function(a){var b=a.event,c=a;delete c.event,this.emit("event:"+this._snakeToCamel(b),c)},c.prototype._getApiSpec=function(){return this._send({method:"core.describe"}).then(this._createApi.bind(this))["catch"](this._handleWebSocketError)},c.prototype._createApi=function(a){var b="by-position-or-by-name"===this._settings.callingConvention,c=function(a){return function(){var c={method:a};return 0===arguments.length?this._send(c):b?arguments.length>1?f.reject(new Error("Expected zero arguments, a single array, or a single object.")):Array.isArray(arguments[0])||arguments[0]===Object(arguments[0])?(c.params=arguments[0],this._send(c)):f.reject(new TypeError("Expected an array or an object.")):(c.params=Array.prototype.slice.call(arguments),this._send(c))}.bind(this)}.bind(this),d=function(a){var b=a.split(".");return b.length>=1&&"core"===b[0]&&(b=b.slice(1)),b},e=function(a){var b=this;return a.forEach(function(a){a=this._snakeToCamel(a),b[a]=b[a]||{},b=b[a]}.bind(this)),b}.bind(this),g=function(b){var f=d(b),g=this._snakeToCamel(f.slice(-1)[0]),h=e(f.slice(0,-1));h[g]=c(b),h[g].description=a[b].description,h[g].params=a[b].params}.bind(this);Object.keys(a).forEach(g),this.emit("state:online")},c.prototype._snakeToCamel=function(a){return a.replace(/(_[a-z])/g,function(a){return a.toUpperCase().replace("_","")})},b.exports=c},{"../lib/websocket/":1,bane:2,when:21}]},{},[22])(22)}); -------------------------------------------------------------------------------- /mopidy_websettings/js/simple.js: -------------------------------------------------------------------------------- 1 | var mopidy = new Mopidy({callingConvention: 'by-position-or-by-name'}) 2 | 3 | function getCurrentTrackUri() { 4 | if (mopidy && mopidy.playback) { 5 | mopidy.playback.getCurrentTrack().then(function(track) { 6 | text = document.getElementsByName('musicbox__autoplay')[0] 7 | text.value = '' 8 | if (track && track.uri) { 9 | text.value = track.uri; 10 | } 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /mopidy_websettings/settingsspec.ini: -------------------------------------------------------------------------------- 1 | [spotify] 2 | enabled = "boolean(default=false)" 3 | username = "string(default='')" 4 | client_id = "string(default='')" 5 | client_secret = "string(default='')" 6 | password = "string(default='')" 7 | bitrate = "float(min=96, max=320, default=160)" 8 | 9 | [spotify_web] 10 | enabled = "boolean(default=false)" 11 | client_id = "string(default='')" 12 | client_secret = "string(default='')" 13 | 14 | [gmusic] 15 | enabled = "boolean(default=false)" 16 | username = "string(default='')" 17 | password = "string(default='')" 18 | deviceid = "string(default='')" 19 | 20 | [tunein] 21 | enabled = "boolean(default=true)" 22 | 23 | [podcast] 24 | enabled = "boolean(default=true)" 25 | 26 | [internetarchive] 27 | enabled = "boolean(default=true)" 28 | 29 | [youtube] 30 | enabled = "boolean(default=true)" 31 | 32 | [audioaddict] 33 | enabled = "boolean(default=true)" 34 | username = "string(default='')" 35 | password = "string(default='')" 36 | quality = "string(default='320k')" 37 | difm = "boolean(default=true)" 38 | radiotunes = "boolean(default=true)" 39 | rockradio = "boolean(default=true)" 40 | jazzradio = "boolean(default=true)" 41 | frescaradio = "boolean(default=true)" 42 | 43 | [somafm] 44 | enabled = "boolean(default=true)" 45 | 46 | [local] 47 | enabled = "boolean(default=true)" 48 | 49 | [soundcloud] 50 | enabled = "boolean(default=false)" 51 | auth_token = "string(default='')" 52 | 53 | [network] 54 | wifi_network = "string(default='')" 55 | wifi_password = "string(default='')" 56 | wifi_country = = "string(default='')" 57 | name = "string(default='MusicBox')" 58 | mount_address = "string(default='')" 59 | mount_user = "string(default='')" 60 | mount_password = "string(default='')" 61 | mount_options = "string(default='vers=1.0')" 62 | workgroup = "string(default='WORKGROUP')" 63 | enable_ssh = "boolean(default=false)" 64 | enable_firewall = "boolean(default=false)" 65 | wait_for_network = "boolean(default=false)" 66 | 67 | [musicbox] 68 | root_password = "string(default='')" 69 | scan_once = "boolean(default=false)" 70 | scan_always = "boolean(default=true)" 71 | webclient = "string(default='')" 72 | autoplay = "string(default='')" 73 | autoplaymaxwait = "float(min=0, max=1000, default=60)" 74 | downsample_usb = "boolean(default=true)" 75 | output = "string(default='')" 76 | enable_upnp = "boolean(default=false)" 77 | enable_shairport = "boolean(default=false)" 78 | enable_mpd_watchdog = "boolean(default=false)" 79 | enable_connect = "boolean(default=false)" 80 | 81 | [audio] 82 | mixer_volume = "float(min=0, max=100, default=85)" 83 | mixer = "string(default='software')" 84 | 85 | [websettings] 86 | musicbox = "boolean(default=false)" 87 | 88 | [dirble] 89 | enabled = "boolean(default=false)" 90 | api_key = "string(default='')" 91 | 92 | [scrobbler] 93 | enabled = "boolean(default=false)" 94 | username = "string(default='')" 95 | password = "string(default='')" 96 | 97 | [tidal] 98 | enabled = "boolean(default=false)" 99 | username = "string(default='')" 100 | password = "string(default='')" 101 | quality = "string(default='HIGH')" 102 | 103 | [logging] 104 | config_file = "string(default='/etc/mopidy/logging.conf')" 105 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | application-import-names = mopidy_websettings,tests 3 | exclude = .git,.tox 4 | 5 | [wheel] 6 | universal = 1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import re 4 | 5 | from setuptools import find_packages, setup 6 | 7 | 8 | def get_version(filename): 9 | content = open(filename).read() 10 | metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", content)) 11 | return metadata['version'] 12 | 13 | 14 | setup( 15 | name='Mopidy-WebSettings', 16 | version=get_version('mopidy_websettings/__init__.py'), 17 | url='https://github.com/woutervanwijk/mopidy-websettings', 18 | license='Apache License, Version 2.0', 19 | author='Wouter van Wijk', 20 | author_email='woutervanwijk@gmail.com', 21 | description='Mopidy extension for editing settings in a webinterface', 22 | long_description=open('README.rst').read(), 23 | packages=find_packages(exclude=['tests', 'tests.*']), 24 | zip_safe=False, 25 | include_package_data=True, 26 | install_requires=[ 27 | 'setuptools', 28 | 'Mopidy >= 0.19', 29 | 'Pykka >= 1.1', 30 | 'ConfigObj >= 4.0.0', 31 | 'Jinja2 >= 2.7', 32 | ], 33 | test_suite='nose.collector', 34 | tests_require=[ 35 | 'nose', 36 | 'mock >= 1.0', 37 | ], 38 | entry_points={ 39 | 'mopidy.ext': [ 40 | 'websettings = mopidy_websettings:Extension', 41 | ], 42 | }, 43 | classifiers=[ 44 | 'Environment :: No Input/Output (Daemon)', 45 | 'Intended Audience :: End Users/Desktop', 46 | 'License :: OSI Approved :: Apache Software License', 47 | 'Operating System :: OS Independent', 48 | 'Programming Language :: Python :: 2', 49 | 'Topic :: Multimedia :: Sound/Audio :: Players', 50 | ], 51 | ) 52 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimusicbox/mopidy-websettings/e4c5a4e638a5581b4c028ef13ea2a9725e69c980/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_extension.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import unittest 4 | 5 | from mopidy_websettings import Extension 6 | 7 | 8 | class ExtensionTest(unittest.TestCase): 9 | 10 | def test_get_default_config(self): 11 | ext = Extension() 12 | 13 | config = ext.get_default_config() 14 | 15 | self.assertIn('[websettings]', config) 16 | self.assertIn('enabled = true', config) 17 | 18 | def test_get_config_schema(self): 19 | ext = Extension() 20 | 21 | schema = ext.get_config_schema() 22 | 23 | self.assertIn('musicbox', schema) 24 | self.assertIn('config_file', schema) 25 | 26 | # TODO Write more tests 27 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, flake8 3 | 4 | [testenv] 5 | sitepackages = true 6 | deps = 7 | coverage 8 | mock 9 | nose 10 | commands = nosetests -v --with-xunit --xunit-file=xunit-{envname}.xml --with-coverage --cover-package=mopidy_websettings 11 | 12 | [testenv:flake8] 13 | deps = 14 | flake8 15 | flake8-import-order 16 | skip_install = true 17 | commands = flake8 18 | -------------------------------------------------------------------------------- /xunit-py27.xml: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------