├── .coveragerc
├── .gitignore
├── .travis.yml
├── LICENSE
├── MANIFEST.in
├── README.rst
├── mopidy_IRControl
├── __init__.py
├── actor.py
└── ext.conf
├── setup.py
└── tests
├── __init__.py
└── test_extension.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [report]
2 | omit =
3 | */pyshared/*
4 | */python?.?/*
5 | */site-packages/nose/*
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | *.pyc
3 | *.swp
4 | .coverage
5 | MANIFEST
6 | build/
7 | dist/
8 | *~
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | install:
4 | - "wget -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -"
5 | - "sudo wget -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list"
6 | - "sudo apt-get update || true"
7 | - "sudo apt-get install mopidy liblircclient-dev python-gobject-2-dbg python-gst0.10-dev python-gst0.10-dbg gnome-keyring"
8 | - "pip install pylirc2 coveralls flake8 mopidy"
9 |
10 | before_script:
11 | - "rm $VIRTUAL_ENV/lib/python$TRAVIS_PYTHON_VERSION/no-global-site-packages.txt"
12 | - "mopidy --version"
13 |
14 | script:
15 | - "flake8 $(find . -iname '*.py')"
16 | - "nosetests --with-coverage --cover-package=mopidy_IRControl"
17 |
18 | after_success:
19 | - "coveralls"
20 |
21 |
--------------------------------------------------------------------------------
/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 2014 Camillo Dell'mour
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.
203 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include MANIFEST.in
3 | include README.rst
4 | include mopidy_IRControl/ext.conf
5 | include mopidy_IRControl/*.py
6 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ****************************
2 | Mopidy-IRControl
3 | ****************************
4 |
5 | .. image:: https://img.shields.io/pypi/v/Mopidy-IRControl.svg
6 | :target: https://pypi.python.org/pypi/Mopidy-IRControl/
7 | :alt: Latest PyPI version
8 |
9 | .. image:: https://img.shields.io/pypi/dm/Mopidy-IRControl.svg
10 | :target: https://pypi.python.org/pypi/Mopidy-IRControl/
11 | :alt: Number of PyPI downloads
12 |
13 | .. image:: https://api.travis-ci.org/spjoe/mopidy-ircontrol.png?branch=master
14 | :target: https://travis-ci.org/spjoe/mopidy-ircontrol
15 | :alt: Travis CI build status
16 |
17 | .. image:: https://coveralls.io/repos/spjoe/mopidy-ircontrol/badge.png?branch=master
18 | :target: https://coveralls.io/r/spjoe/mopidy-ircontrol?branch=master
19 | :alt: Test coverage
20 |
21 |
22 | A Mopidy frontend to control mopidy with an infrared remote control. It is using lirc as IR receiver deamon.
23 |
24 |
25 | Installation
26 | ============
27 |
28 | Install by running::
29 |
30 | pip install Mopidy-IRControl
31 |
32 | Or, if available, install the Debian/Ubuntu package from `apt.mopidy.com
33 | `_.
34 |
35 |
36 | Configuration
37 | =============
38 |
39 | Before starting Mopidy, you must add configuration for
40 | Mopidy-IRControl to your Mopidy configuration file::
41 |
42 | [IRControl]
43 | enabled = true
44 | #look at your lircd.conf (/etc/lirc/lircd.conf) to find you configured buttons names
45 | mute = KEY_MUTE
46 | next = KEY_NEXT
47 | previous = KEY_PREVIOUS
48 | playpause = KEY_PLAYPAUSE
49 | stop = KEY_STOP
50 | volumeup = KEY_VOLUMEUP
51 | volumedown = KEY_VOLUMEDOWN
52 |
53 | Project resources
54 | =================
55 |
56 | - `Source code `_
57 | - `Issue tracker `_
58 | - `Download development snapshot `_
59 |
60 |
61 | Changelog
62 | =========
63 |
64 | v0.1.0 - 08.10.2015
65 | ----------------------------------------
66 |
67 | - Initial release.
68 |
69 | Contributors
70 | ============
71 | - `logogin `_
72 |
--------------------------------------------------------------------------------
/mopidy_IRControl/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | import os
4 | from mopidy import config, ext
5 |
6 |
7 | __version__ = '0.1.0'
8 |
9 |
10 | class Extension(ext.Extension):
11 |
12 | dist_name = 'Mopidy-IRControl'
13 | ext_name = 'IRControl'
14 | version = __version__
15 |
16 | def get_default_config(self):
17 | conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
18 | return config.read(conf_file)
19 |
20 | def get_config_schema(self):
21 | schema = super(Extension, self).get_config_schema()
22 | schema['mute'] = config.String()
23 | schema['next'] = config.String()
24 | schema['previous'] = config.String()
25 | schema['playpause'] = config.String()
26 | schema['stop'] = config.String()
27 | schema['volumeup'] = config.String()
28 | schema['volumedown'] = config.String()
29 | schema['power'] = config.String()
30 | schema['menu'] = config.String()
31 | schema['favorites'] = config.String()
32 | schema['search'] = config.String()
33 | schema['playlist_uri_template'] = config.String()
34 | schema['num0'] = config.String()
35 | schema['num1'] = config.String()
36 | schema['num2'] = config.String()
37 | schema['num3'] = config.String()
38 | schema['num4'] = config.String()
39 | schema['num5'] = config.String()
40 | schema['num6'] = config.String()
41 | schema['num7'] = config.String()
42 | schema['num8'] = config.String()
43 | schema['num9'] = config.String()
44 | return schema
45 |
46 | def setup(self, registry):
47 | from .actor import IRControlFrontend
48 | registry.add('frontend', IRControlFrontend)
49 |
--------------------------------------------------------------------------------
/mopidy_IRControl/actor.py:
--------------------------------------------------------------------------------
1 | import pykka
2 | import pylirc
3 | import logging
4 | import tempfile
5 | import threading
6 | import select
7 |
8 | from mopidy.core import PlaybackState
9 | from mopidy.core import CoreListener
10 |
11 | logger = logging.getLogger('mopidy_IRControl')
12 |
13 | LIRC_PROG_NAME = "mopidyIRControl"
14 |
15 |
16 | class Event(list):
17 | """Event subscription.
18 |
19 | A list of callable objects. Calling an instance of this will cause a
20 | call to each item in the list in ascending order by index."""
21 | def __call__(self, *args, **kwargs):
22 | for f in self:
23 | f(*args, **kwargs)
24 |
25 | def __repr__(self):
26 | return "Event(%s)" % list.__repr__(self)
27 |
28 |
29 | class CommandDispatcher(object):
30 | def __init__(self, core, config, buttonPressEvent):
31 | self.core = core
32 | self.config = config
33 | self._handlers = {}
34 | self.registerHandler('playpause', self._playpauseHandler)
35 | self.registerHandler('mute', self._muteHandler)
36 | self.registerHandler('stop', lambda: self.core.playback.stop().get())
37 | self.registerHandler('next', lambda: self.core.playback.next().get())
38 | self.registerHandler('previous',
39 | lambda: self.core.playback.previous().get())
40 | self.registerHandler('volumedown',
41 | self._volumeFunction(lambda vol: vol - 5))
42 | self.registerHandler('volumeup',
43 | self._volumeFunction(lambda vol: vol + 5))
44 |
45 | for i in range(10):
46 | self.registerHandler('num{0}'.format(i), self._playlistFunction(i))
47 |
48 | buttonPressEvent.append(self.handleCommand)
49 |
50 | def handleCommand(self, cmd):
51 | if cmd in self._handlers:
52 | logger.debug("Command {0} was handled".format(cmd))
53 | self._handlers[cmd]()
54 | else:
55 | logger.debug("Command {0} was not handled".format(cmd))
56 |
57 | def registerHandler(self, cmd, handler):
58 | self._handlers[cmd] = handler
59 |
60 | def _playpauseHandler(self):
61 | state = self.core.playback.get_state().get()
62 | if(state == PlaybackState.PAUSED):
63 | self.core.playback.resume().get()
64 | elif (state == PlaybackState.PLAYING):
65 | self.core.playback.pause().get()
66 | elif (state == PlaybackState.STOPPED):
67 | self.core.playback.play().get()
68 |
69 | def _muteHandler(self):
70 | self.core.mixer.set_mute(not self.core.mixer.get_mute().get())
71 |
72 | def _volumeFunction(self, changeFct):
73 | def volumeChange():
74 | vol = self.core.mixer.get_volume().get()
75 | self.core.mixer.set_volume(min(max(0, changeFct(vol)), 100))
76 | return volumeChange
77 |
78 | def _playPlaylist(self, uri):
79 | refs = self.core.playlists.get_items(uri).get()
80 | if not refs:
81 | logger.warn("Playlist '%s' does not exist", uri)
82 | return
83 | self.core.tracklist.clear()
84 | uris = map(lambda ref: ref.uri, refs)
85 | self.core.tracklist.add(uris=uris)
86 | self.core.tracklist.set_consume(False)
87 | self.core.tracklist.set_repeat(True)
88 | self.core.playback.play()
89 |
90 | def _playlistFunction(self, num):
91 | return lambda: self._playPlaylist(self.config['playlist_uri_template'].format(num))
92 |
93 | class LircThread(threading.Thread):
94 | def __init__(self, configFile):
95 | threading.Thread.__init__(self)
96 | self.name = 'Lirc worker thread'
97 | self.configFile = configFile
98 | self.frontendActive = True
99 | self.ButtonPressed = Event()
100 |
101 | def run(self):
102 | try:
103 | self.run_inside_try()
104 | except Exception as e:
105 | logger.warning('IRControl has problems starting pylirc: ' + str(e))
106 |
107 | def run_inside_try(self):
108 | self.startPyLirc()
109 |
110 | def startPyLirc(self):
111 | lircHandle = pylirc.init(LIRC_PROG_NAME, self.configFile, 0)
112 | if(lircHandle != 0):
113 | while(self.frontendActive):
114 | self.consumePylirc(lircHandle)
115 | pylirc.exit()
116 |
117 | def consumePylirc(self, lircHandle):
118 | try:
119 | if(select.select([lircHandle], [], [], 1) == ([], [], [])):
120 | pass
121 | else:
122 | s = pylirc.nextcode(1)
123 | self.handleNextCode(s)
124 | except Exception as e:
125 | logger.warning('Exception during handling a command: ' + str(e))
126 |
127 | def handleNextCode(self, s):
128 | if s:
129 | self.handleLircCode(s)
130 |
131 | def handleLircCode(self, s):
132 | for code in s:
133 | self.handleCommand(code['config'])
134 |
135 | def handleCommand(self, cmd):
136 | logger.debug('Command: {0}'.format(cmd))
137 | self.ButtonPressed(cmd)
138 |
139 |
140 | class IRControlFrontend(pykka.ThreadingActor, CoreListener):
141 | def __init__(self, config, core):
142 | super(IRControlFrontend, self).__init__()
143 | self.core = core
144 | self.config = config['IRControl']
145 | self.configFile = self.generateLircConfigFile(config['IRControl'])
146 | logger.debug('lircrc file:{0}'.format(self.configFile))
147 |
148 | def on_start(self):
149 | try:
150 | logger.debug('IRControl starting')
151 | self.thread = LircThread(self.configFile)
152 | self.dispatcher = CommandDispatcher(
153 | self.core,
154 | self.config,
155 | self.thread.ButtonPressed)
156 | self.thread.ButtonPressed.append(self.handleButtonPress)
157 | self.thread.start()
158 | logger.debug('IRControl started')
159 | except Exception as e:
160 | logger.warning('IRControl has not started: ' + str(e))
161 | self.stop()
162 |
163 | def on_stop(self):
164 | logger.info('IRControl stopped')
165 | self.thread.frontendActive = False
166 | self.thread.join()
167 |
168 | def on_failure(self):
169 | logger.warning('IRControl failing')
170 | self.thread.frontendActive = False
171 | self.thread.join()
172 |
173 | def handleButtonPress(self, cmd):
174 | pass
175 | # CoreListener.send("IRButtonPressed", button=cmd)
176 |
177 | def generateLircConfigFile(self, config):
178 | '''Returns file name of generate config file for pylirc'''
179 | f = tempfile.NamedTemporaryFile(delete=False)
180 | skeleton = 'begin\n prog={2}\n button={0}\n config={1}\nend\n'
181 | for action in config:
182 | entry = skeleton.format(config[action], action, LIRC_PROG_NAME)
183 | f.write(entry)
184 | f.close()
185 | return f.name
186 |
--------------------------------------------------------------------------------
/mopidy_IRControl/ext.conf:
--------------------------------------------------------------------------------
1 | [IRControl]
2 | enabled = true
3 | #look at your lircd.conf (/etc/lirc/lircd.conf) to find you configured buttons
4 | mute = KEY_MUTE
5 | next = KEY_NEXT
6 | previous = KEY_PREVIOUS
7 | playpause = KEY_PLAYPAUSE
8 | stop = KEY_STOP
9 | volumeup = KEY_VOLUMEUP
10 | volumedown = KEY_VOLUMEDOWN
11 | playlist_uri_template = m3u:playlist{0}.m3u
12 | num0 = KEY_0
13 | num1 = KEY_1
14 | num2 = KEY_2
15 | num3 = KEY_3
16 | num4 = KEY_4
17 | num5 = KEY_5
18 | num6 = KEY_6
19 | num7 = KEY_7
20 | num8 = KEY_8
21 | num9 = KEY_9
22 | # The following are not used yet.
23 | power = KEY_POWER
24 | menu = KEY_MENU
25 | favorites = KEY_FAVORITES
26 | search = KEY_SEARCH
27 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | import re
4 | from setuptools import setup, find_packages
5 |
6 |
7 | def get_version(filename):
8 | content = open(filename).read()
9 | metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", content))
10 | return metadata['version']
11 |
12 |
13 | setup(
14 | name='Mopidy-IRControl',
15 | version=get_version('mopidy_IRControl/__init__.py'),
16 | url='https://github.com/spjoe/mopidy-ircontrol',
17 | license='Apache License, Version 2.0',
18 | author="Camillo Dell'mour",
19 | author_email='cdellmour@gmail.com',
20 | description='Mopidy frontend to be controlled with an IR controller',
21 | long_description=open('README.rst').read(),
22 | packages=find_packages(exclude=['tests', 'tests.*']),
23 | zip_safe=False,
24 | include_package_data=True,
25 | install_requires=[
26 | 'setuptools',
27 | 'Mopidy >=1.1.1',
28 | 'Pykka >= 1.1',
29 | 'pylirc2 >= 0.1',
30 | ],
31 | test_suite='nose.collector',
32 | tests_require=[
33 | 'nose',
34 | 'mock >= 1.0',
35 | ],
36 | entry_points={
37 | 'mopidy.ext': [
38 | 'IRControl = mopidy_IRControl:Extension',
39 | ],
40 | },
41 | classifiers=[
42 | 'Environment :: No Input/Output (Daemon)',
43 | 'Intended Audience :: End Users/Desktop',
44 | 'License :: OSI Approved :: Apache Software License',
45 | 'Operating System :: OS Independent',
46 | 'Programming Language :: Python :: 2',
47 | 'Topic :: Multimedia :: Sound/Audio :: Players',
48 | ],
49 | )
50 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spjoe/mopidy-ircontrol/752a5df08c36845e580ae5256e26525a288e1438/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_extension.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | import unittest
4 | import mopidy
5 | import pylirc
6 | import time
7 |
8 | from mock import Mock, patch, MagicMock
9 | from mopidy_IRControl import Extension, actor as lib
10 | from mopidy.models import Ref
11 | from pykka.threading import ThreadingFuture
12 |
13 | import sys
14 | import logging
15 | logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
16 |
17 |
18 | class ExtensionTest(unittest.TestCase):
19 | def test_get_default_config(self):
20 | ext = Extension()
21 |
22 | config = ext.get_default_config()
23 |
24 | self.assertIn('[IRControl]', config)
25 | self.assertIn('enabled = true', config)
26 | self.assertIn('playlist_uri_template = m3u:playlist{0}.m3u', config)
27 |
28 | def test_get_config_schema(self):
29 | ext = Extension()
30 |
31 | schema = ext.get_config_schema()
32 | self.assertIn('mute', schema)
33 | self.assertIn('playpause', schema)
34 | self.assertIn('next', schema)
35 | self.assertIn('previous', schema)
36 | self.assertIn('stop', schema)
37 | self.assertIn('volumedown', schema)
38 | self.assertIn('volumeup', schema)
39 |
40 | self.assertIn('playlist_uri_template', schema)
41 | for i in range(10):
42 | self.assertIn('num{0}'.format(i), schema)
43 |
44 |
45 | class FrontendTest(unittest.TestCase):
46 |
47 | def setUp(self):
48 | IRconfig = {}
49 | IRconfig['next'] = 'KEY_NEXT'
50 | IRconfig['previous'] = 'KEY_PREVIOUS'
51 | IRconfig['playpause'] = 'KEY_PLAYPAUSE'
52 | IRconfig['stop'] = 'KEY_STOP'
53 | IRconfig['volumeup'] = 'KEY_VOLUMEUP'
54 | IRconfig['volumedown'] = 'KEY_VOLUMEDOWN'
55 | IRconfig['enabled'] = True
56 | self.config = {'IRControl': IRconfig}
57 |
58 | def test_on_start_should_spawn_thread(self):
59 | actor = lib.IRControlFrontend(self.config, None)
60 | actor.on_start()
61 | assert actor.thread is not None
62 | actor.on_stop()
63 |
64 | def test_on_stop(self):
65 | actor = lib.IRControlFrontend(self.config, None)
66 | actor.on_start()
67 | actor.on_stop()
68 | assert not actor.thread.isAlive()
69 |
70 | def test_on_failure(self):
71 | actor = lib.IRControlFrontend(self.config, None)
72 | actor.on_start()
73 |
74 | actor.on_failure()
75 | assert not actor.thread.isAlive()
76 |
77 | @patch('mopidy_IRControl.actor.logger')
78 | def test_on_start_log_exception(self, mock_logger):
79 | actor = lib.IRControlFrontend(self.config, None)
80 | with patch('mopidy_IRControl.actor.LircThread.start') as MockMethod:
81 | MockMethod.side_effect = Exception('Boom!')
82 | actor.on_start()
83 | self.assertTrue(mock_logger.warning.called)
84 |
85 |
86 | class CommandDispatcherTest(unittest.TestCase):
87 | class WithGet:
88 | def __init__(self, value):
89 | self.value = value
90 |
91 | def get(self):
92 | return self.value
93 |
94 | def __call__(self):
95 | return self
96 |
97 | def setUp(self):
98 | self.coreMock = mopidy.core.Core(None, None, [], None)
99 | self.buttonPressEvent = lib.Event()
100 | playback = Mock()
101 | playback.get_state = self.WithGet(mopidy.core.PlaybackState.STOPPED)
102 |
103 | mixer = Mock()
104 | mixer.get_mute = self.WithGet(False)
105 | mixer.get_volume = self.WithGet(50)
106 | mixer.set_mute = MagicMock()
107 | mixer.set_volume = MagicMock()
108 |
109 | self.coreMock.playback = playback
110 | self.coreMock.mixer = mixer
111 | self.coreMock.playlists = Mock()
112 | self.coreMock.tracklist = Mock()
113 |
114 | self.dispatcher = lib.CommandDispatcher(self.coreMock,
115 | {'playlist_uri_template': 'local:playlist:playlist{0}.m3u'},
116 | self.buttonPressEvent)
117 |
118 | def commandXYZHandler(self):
119 | self.executed = True
120 |
121 | def test_registerHandler(self):
122 | self.executed = False
123 | self.dispatcher.registerHandler('commandXYZ', self.commandXYZHandler)
124 |
125 | self.dispatcher.handleCommand('commandXYZ')
126 | assert self.executed
127 |
128 | def test_handleCommand(self):
129 | self.executed = False
130 |
131 | self.dispatcher.handleCommand('commandXYZ')
132 | assert not self.executed
133 |
134 | def test_default_registered_handler(self):
135 | self.assertIn('mute', self.dispatcher._handlers)
136 | self.assertIn('playpause', self.dispatcher._handlers)
137 | self.assertIn('next', self.dispatcher._handlers)
138 | self.assertIn('previous', self.dispatcher._handlers)
139 | self.assertIn('stop', self.dispatcher._handlers)
140 | self.assertIn('volumedown', self.dispatcher._handlers)
141 | self.assertIn('volumeup', self.dispatcher._handlers)
142 |
143 | for i in range(10):
144 | self.assertIn('num{0}'.format(i), self.dispatcher._handlers)
145 |
146 | def test_stop_handler(self):
147 | self.dispatcher.handleCommand('stop')
148 | self.coreMock.playback.stop.assert_called_with()
149 |
150 | def test_mute_handler(self):
151 | self.dispatcher.handleCommand('mute')
152 | self.coreMock.mixer.set_mute.assert_called_with(True)
153 |
154 | def test_next_handler(self):
155 | self.dispatcher.handleCommand('next')
156 | self.coreMock.playback.next.assert_called_with()
157 |
158 | def test_previous_handler(self):
159 | self.dispatcher.handleCommand('previous')
160 | self.coreMock.playback.previous.assert_called_with()
161 |
162 | def test_playpause_play_handler(self):
163 | self.dispatcher.handleCommand('playpause')
164 | self.coreMock.playback.play.assert_called_with()
165 |
166 | def test_playpause_resume_handler(self):
167 | self.coreMock.playback.get_state = \
168 | self.WithGet(mopidy.core.PlaybackState.PAUSED)
169 |
170 | self.dispatcher.handleCommand('playpause')
171 | self.coreMock.playback.resume.assert_called_with()
172 |
173 | def test_playpause_pause_handler(self):
174 | self.coreMock.playback.get_state = \
175 | self.WithGet(mopidy.core.PlaybackState.PLAYING)
176 |
177 | self.dispatcher.handleCommand('playpause')
178 | self.coreMock.playback.pause.assert_called_with()
179 |
180 | def test_volumedown_handler(self):
181 | self.dispatcher.handleCommand('volumedown')
182 | self.coreMock.mixer.set_volume.assert_called_with(45)
183 |
184 | def test_volumeup_handler(self):
185 | self.dispatcher.handleCommand('volumeup')
186 | self.coreMock.mixer.set_volume.assert_called_with(55)
187 |
188 | def test_num_handler_no_playlist(self):
189 | refs = ThreadingFuture()
190 | refs.set(None)
191 | self.coreMock.playlists.get_items = MagicMock(return_value=refs)
192 |
193 | self.dispatcher.handleCommand('num0')
194 | self.coreMock.playlists.get_items.assert_called_with('local:playlist:playlist0.m3u')
195 | self.assertEqual(0, len(self.coreMock.playback.mock_calls))
196 | self.assertFalse(0, len(self.coreMock.tracklist.mock_calls))
197 |
198 | def test_num_handler_add_items(self):
199 | refs = ThreadingFuture()
200 | refs.set([Ref(uri='track1'), Ref(uri='track2')])
201 | self.coreMock.playlists.get_items = MagicMock(return_value=refs)
202 |
203 | self.dispatcher.handleCommand('num9')
204 |
205 | self.coreMock.playlists.get_items.assert_called_with('local:playlist:playlist9.m3u')
206 | self.coreMock.tracklist.add.assert_called_with(uris=['track1', 'track2'])
207 | self.coreMock.playback.play.assert_called_with()
208 |
209 |
210 | class LircThreadTest(unittest.TestCase):
211 | def setUp(self):
212 | pylirc.init = Mock(return_value=True)
213 | pylirc.nextcode = Mock(return_value=[])
214 | pylirc.exit = Mock(return_value=True)
215 |
216 | def test_startPyLirc(self):
217 | thread = lib.LircThread(None)
218 | thread.start()
219 | thread.frontendActive = False
220 | thread.join()
221 |
222 | pylirc.init.assert_called_with('mopidyIRControl', None, 0)
223 |
224 | @patch('mopidy_IRControl.actor.logger')
225 | def test_handleCommand(self, mock_logger):
226 | pylirc.nextcode = Mock(return_value=[{'config': 'commandXYZ'}])
227 |
228 | with patch('select.select') as MockClass:
229 | MockClass.return_value = ([1], [], [])
230 | thread = lib.LircThread(None)
231 | thread.start()
232 | time.sleep(0.1)
233 | thread.frontendActive = False
234 | thread.join()
235 |
236 | assert mock_logger.debug.called
237 |
--------------------------------------------------------------------------------