├── .gitignore
├── .travis.yml
├── LICENSE
├── MANIFEST.in
├── README.rst
├── mopidy_party
├── __init__.py
├── ext.conf
└── static
│ ├── controller.js
│ ├── dark.css
│ ├── index.html
│ ├── original.css
│ ├── vendors
│ ├── angular.min.js
│ ├── bootstrap.min.css
│ ├── fontawesome-all.min.css
│ └── mopidy.min.js
│ └── webfonts
│ ├── fa-brands-400.ttf
│ ├── fa-brands-400.woff2
│ ├── fa-regular-400.ttf
│ ├── fa-regular-400.woff2
│ ├── fa-solid-900.ttf
│ └── fa-solid-900.woff2
├── setup.cfg
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | *.pyc
3 | *.swp
4 | .coverage
5 | .tox/
6 | MANIFEST
7 | build/
8 | dist/
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language: python
4 |
5 | python:
6 | - "2.7_with_system_site_packages"
7 |
8 | addons:
9 | apt:
10 | sources:
11 | - mopidy-stable
12 | packages:
13 | - mopidy
14 |
--------------------------------------------------------------------------------
/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.
203 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include .coveragerc
2 | include .travis.yml
3 | include LICENSE
4 | include MANIFEST.in
5 | include README.rst
6 | include mopidy_party/ext.conf
7 |
8 | recursive-include tests *.py
9 |
10 | # added by check_manifest.py
11 | recursive-include mopidy_party *.css
12 | recursive-include mopidy_party *.eot
13 | recursive-include mopidy_party *.html
14 | recursive-include mopidy_party *.js
15 | recursive-include mopidy_party *.svg
16 | recursive-include mopidy_party *.ttf
17 | recursive-include mopidy_party *.woff
18 | recursive-include mopidy_party *.woff2
19 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ****************************
2 | Mopidy-Party
3 | ****************************
4 |
5 | Mopidy web extension designed for party! Let your guests manage the sound ;)
6 |
7 | - Minimal, fast web interface designed for mobile
8 | - Search soundtracks and add it to the queue
9 | - Skip current track after a configurable number of votes (defaults to 3)
10 |
11 | See this `blog entry `_ for some details about this package.
12 |
13 | Installation
14 | ============
15 |
16 | You must install `mopidy `_ (version 3) and some backends (soundcloud, spotify, youtube...).
17 |
18 | **PROD:** you just have to install pip and then::
19 |
20 | sudo python3 -m pip install Mopidy-Party
21 |
22 | **DEV:** After cloning the repository, install by running::
23 |
24 | sudo python3 -m pip install -e .
25 |
26 | Usage
27 | =====
28 |
29 | To use the interface, simply use your browser to visit your Mopidy instance's IP at port 6680 to see all available web interfaces.
30 | For example, http://192.168.0.2:6680/
31 |
32 | Direct access to Mopidy Party should then be: http://192.168.0.2:6680/party/
33 |
34 | Configuration
35 | =============
36 |
37 | ::
38 |
39 | [party]
40 | enabled = true
41 | votes_to_skip = 3 # Votes needed from different users to allow skipping a song.
42 | max_tracks = 0 # Maximum number of tracks that can be added by a single user in a row, 0 for unlimited
43 | max_results = 50 # Maximum number of tracks to show when searching / browsing on a single page
44 | max_queue_length = 0 # Maximum number of tracks queued at the same time, 0 for unlimited
45 | hide_pause = false # Change to true to hide the pause button
46 | hide_skip = false # Change to true to hide the skip button
47 | style = dark.css # Stylesheet to use. Also embedded is original.css (light theme)
48 |
49 | Project resources
50 | =================
51 |
52 | - `Source code `_
53 | - `Issue tracker `_
54 | - `Development branch tarball `_
55 |
56 |
57 | Developer information
58 | =====================
59 |
60 | The RequestHandler 'config' makes ``mopidy.conf``'s section for the ``[party]`` configuration available via ``http GET`` requests. Useful if you want to make aspects of the controller configurable.
61 |
62 | Example: The controller uses the below request, to read the ``max_results`` value::
63 |
64 | $http.get('/party/config?key=max_results')
65 |
66 |
67 | Changelog
68 | =========
69 |
70 | v1.2.2 (2024-03-09)
71 | ----------------------------------------
72 | - Add max_queue_length, max_results config options (by grasdk)
73 |
74 | v1.2.1 (2023-08-14)
75 | ----------------------------------------
76 | - Add music source name and icon in search results (by grasdk)
77 | - Bump fontawesome version
78 |
79 | v1.2.0 (2022-12-21)
80 | ----------------------------------------
81 | - Add hide_pause, hide_skip, style config options (by grasdk)
82 | - Provide two default styles (dark and original)
83 |
84 | v1.1.0 (2022-10-12)
85 | ----------------------------------------
86 | - Use IP from X-Forwarded-For header if available (by girst)
87 | - Limit maximum number of tracks per user in a row (by girst)
88 | - Allows fallback tracks (added by other mopidy frontends) (by girst)
89 | - Dark mode (by girst)
90 |
91 | v1.0.0 (2020-01-03)
92 | ----------------------------------------
93 | - Port to python3 and Mopidy 3 (by girst)
94 |
95 | v0.3.1 (2018-10-17)
96 | ----------------------------------------
97 | - Vendorize mopidy javascript for 3.0 upstream compatibility
98 |
99 | v0.3.0 (2018-08-03)
100 | ----------------------------------------
101 | - Add browse when search string is empty and on load (supports both mopidy-local and mopidy-local-sqlite backends, by juniormonkey)
102 |
103 | v0.2.0 (2017-01-08)
104 | ----------------------------------------
105 | - Add vote to skip (by RealityFork)
106 |
107 | v0.1.2 (2016-10-10)
108 | ----------------------------------------
109 | - Add artists and album names in songs list
110 |
111 | v0.1.0 (2015-09-01)
112 | ----------------------------------------
113 | - Initial release.
114 |
--------------------------------------------------------------------------------
/mopidy_party/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import tornado.web
4 |
5 | from mopidy import config, ext
6 |
7 | __version__ = '1.2.2'
8 |
9 |
10 | class VoteRequestHandler(tornado.web.RequestHandler):
11 |
12 | def initialize(self, core, data, config):
13 | self.core = core
14 | self.data = data
15 | self.requiredVotes = config["party"]["votes_to_skip"]
16 |
17 | def _getip(self):
18 | return self.request.headers.get("X-Forwarded-For", self.request.remote_ip)
19 |
20 | def get(self):
21 | currentTrack = self.core.playback.get_current_track().get()
22 | if (currentTrack == None): return
23 | currentTrackURI = currentTrack.uri
24 |
25 | # If the current track is different to the one stored, clear votes
26 | if (currentTrackURI != self.data["track"]):
27 | self.data["track"] = currentTrackURI
28 | self.data["votes"] = []
29 |
30 | if (self._getip() in self.data["votes"]): # User has already voted
31 | self.write("You have already voted to skip this song =)")
32 | else: # Valid vote
33 | self.data["votes"].append(self._getip())
34 | if (len(self.data["votes"]) == self.requiredVotes):
35 | self.core.playback.next()
36 | self.write("Skipping...")
37 | else:
38 | self.write("You have voted to skip this song. ("+str(self.requiredVotes-len(self.data["votes"]))+" more votes needed)")
39 |
40 |
41 | class AddRequestHandler(tornado.web.RequestHandler):
42 |
43 | def initialize(self, core, data, config):
44 | self.core = core
45 | self.data = data
46 | self.maxQueueLength = config["party"]["max_queue_length"]
47 |
48 | def _getip(self):
49 | return self.request.headers.get("X-Forwarded-For", self.request.remote_ip)
50 |
51 | def post(self):
52 | # when the last n tracks were added by the same user, abort.
53 | if self.data["queue"] and all([e == self._getip() for e in self.data["queue"]]):
54 | self.write("You have requested too many songs")
55 | self.set_status(409)
56 | return
57 |
58 | track_uri = self.request.body.decode()
59 | if not track_uri:
60 | self.set_status(400)
61 | return
62 |
63 | n_queued = self.core.tracklist.get_length().get()
64 | if (self.maxQueueLength > 0) and (n_queued > self.maxQueueLength):
65 | self.write("Queue at max length, try again later.")
66 | self.set_status(409)
67 | return
68 |
69 | pos = 0
70 | if self.data["last"]:
71 | queue = self.core.tracklist.index(self.data["last"]).get() or 0
72 | current = self.core.tracklist.index().get() or 0
73 | pos = max(queue, current) # after lastly enqueued and after current track
74 |
75 | try:
76 | self.data["last"] = self.core.tracklist.add(uris=[track_uri], at_position=pos+1).get()[0]
77 | self.data["queue"].append(self._getip())
78 | self.data["queue"].pop(0)
79 | except Exception as e:
80 | self.write("Unable to add track. Internal Server Error: "+repr(e))
81 | self.set_status(500)
82 | return
83 |
84 | self.core.tracklist.set_consume(True)
85 | if self.core.playback.get_state().get() == "stopped":
86 | self.core.playback.play()
87 |
88 |
89 | class IndexHandler(tornado.web.RequestHandler):
90 |
91 | def initialize(self, config):
92 | self.__dict = {}
93 | # Make the configuration from mopidy.conf [party] section available as variables in index.html
94 | for conf_key, value in config["party"].items():
95 | if conf_key != "enabled":
96 | self.__dict[conf_key] = value
97 |
98 | def get(self):
99 | return self.render("static/index.html", **self.__dict)
100 |
101 | class ConfigHandler(tornado.web.RequestHandler):
102 |
103 | def initialize(self, config):
104 | self.party_cfg = config["party"]
105 |
106 | def get(self):
107 | conf_key = self.get_argument("key")
108 | if conf_key == []:
109 | self.set_status(400)
110 | self.write("Query parameter 'key' not present")
111 | return
112 | try:
113 | value = self.party_cfg[conf_key]
114 | self.write(repr(value))
115 | except KeyError:
116 | self.set_status(404)
117 | self.write("Party configuration '" + conf_key + "' not found")
118 | return
119 | except Exception as e:
120 | self.set_status(500)
121 | self.write("Internal server error: "+repr(e))
122 | return
123 |
124 |
125 | def party_factory(config, core):
126 | from tornado.web import RedirectHandler
127 | data = {'track':"", 'votes':[], 'queue': [None] * config["party"]["max_tracks"], 'last':None}
128 |
129 | return [
130 | ('/', RedirectHandler, {'url': 'index.html'}), #always redirect from extension root to the html
131 | ('/index.html', IndexHandler, {'config': config }),
132 | ('/vote', VoteRequestHandler, {'core': core, 'data':data, 'config':config}),
133 | ('/add', AddRequestHandler, {'core': core, 'data':data, 'config':config}),
134 | ('/config', ConfigHandler, {'config':config})
135 | ]
136 |
137 |
138 | class Extension(ext.Extension):
139 | dist_name = 'Mopidy-Party'
140 | ext_name = 'party'
141 | version = __version__
142 |
143 | def get_default_config(self):
144 | conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
145 | return config.read(conf_file)
146 |
147 | def get_config_schema(self):
148 | schema = super(Extension, self).get_config_schema()
149 | schema['votes_to_skip'] = config.Integer(minimum=0)
150 | schema['max_tracks'] = config.Integer(minimum=0)
151 | schema['hide_pause'] = config.Boolean(optional=True)
152 | schema['hide_skip'] = config.Boolean(optional=True)
153 | schema['style'] = config.String()
154 | schema['max_results'] = config.Integer(minimum=0, optional=True)
155 | schema['max_queue_length'] = config.Integer(minimum=0, optional=True)
156 | return schema
157 |
158 | def setup(self, registry):
159 | registry.add('http:static', {
160 | 'name': self.ext_name,
161 | 'path': os.path.join(os.path.dirname(__file__), 'static'),
162 | })
163 | registry.add('http:app', {
164 | 'name': self.ext_name,
165 | 'factory': party_factory,
166 | })
167 |
--------------------------------------------------------------------------------
/mopidy_party/ext.conf:
--------------------------------------------------------------------------------
1 | [party]
2 | enabled = true
3 | votes_to_skip = 3
4 | max_tracks = 0
5 | max_results = 50
6 | max_queue_length = 0
7 | hide_pause = false
8 | hide_skip = false
9 | style = dark.css
--------------------------------------------------------------------------------
/mopidy_party/static/controller.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // TODO : add a mopidy service designed for angular, to avoid ugly $scope.$apply()...
4 | angular.module('partyApp', [])
5 | .controller('MainController', function ($scope, $http) {
6 |
7 | // Scope variables
8 | $scope.message = [];
9 | $scope.tracks = [];
10 | $scope.tracksToLookup = [];
11 | $scope.maxTracksToLookup = 50; // Will be overwritten later by module config
12 | $scope.loading = true;
13 | $scope.ready = false;
14 | $scope.currentState = {
15 | paused: false,
16 | length: 0,
17 | track: {
18 | length: 0,
19 | name: 'Nothing playing, add some songs to get the party going!'
20 | }
21 | };
22 |
23 | // Get the max tracks to lookup at once from the 'max_results' config value in mopidy.conf
24 | $http.get('/party/config?key=max_results').then(function success(response) {
25 | if (response.status == 200) {
26 | $scope.maxTracksToLookup = +response.data;
27 | }
28 | }, null);
29 |
30 | var mopidy = new Mopidy({
31 | 'callingConvention': 'by-position-or-by-name'
32 | });
33 |
34 | mopidy.on('state:online', function () {
35 | mopidy.playback
36 | .getCurrentTrack()
37 | .then(function (track) {
38 | if (track)
39 | $scope.currentState.track = track;
40 | return mopidy.playback.getState();
41 | })
42 | .then(function (state) {
43 | $scope.currentState.paused = (state === 'paused');
44 | return mopidy.tracklist.getLength();
45 | })
46 | .then(function (length) {
47 | $scope.currentState.length = length;
48 | })
49 | .done(function () {
50 | $scope.ready = true;
51 | $scope.loading = false;
52 | $scope.$apply();
53 | $scope.search();
54 | });
55 | });
56 |
57 | mopidy.on('event:playbackStateChanged', function (event) {
58 | $scope.currentState.paused = (event.new_state === 'paused');
59 | $scope.$apply();
60 | });
61 |
62 | mopidy.on('event:trackPlaybackStarted', function (event) {
63 | $scope.currentState.track = event.tl_track.track;
64 | $scope.$apply();
65 | });
66 |
67 | mopidy.on('event:tracklistChanged', function () {
68 | mopidy.tracklist.getLength().done(function (length) {
69 | $scope.currentState.length = length;
70 | $scope.$apply();
71 | });
72 | });
73 |
74 | $scope.printDuration = function (track) {
75 | if (!track.length)
76 | return '';
77 |
78 | var _sum = parseInt(track.length / 1000);
79 | var _min = parseInt(_sum / 60);
80 | var _sec = _sum % 60;
81 |
82 | return '(' + _min + ':' + (_sec < 10 ? '0' + _sec : _sec) + ')';
83 | };
84 |
85 | $scope.search = function () {
86 | $scope.message = [];
87 | $scope.loading = true;
88 |
89 | if (!$scope.searchField) {
90 | mopidy.library.browse({
91 | 'uri': 'local:directory'
92 | }).done($scope.handleBrowseResult);
93 | return;
94 | }
95 |
96 | mopidy.library.search({
97 | 'query': {
98 | 'any': [$scope.searchField]
99 | }
100 | }).done($scope.handleSearchResult);
101 | };
102 |
103 | $scope.handleBrowseResult = function (res) {
104 | $scope.loading = false;
105 | $scope.tracks = [];
106 | $scope.tracksToLookup = [];
107 |
108 | for (var i = 0; i < res.length; i++) {
109 | if (res[i].type == 'directory' && res[i].uri == 'local:directory?type=track') {
110 | mopidy.library.browse({
111 | 'uri': res[i].uri
112 | }).done($scope.handleBrowseResult);
113 | } else if (res[i].type == 'track') {
114 | $scope.tracksToLookup.push(res[i].uri);
115 | }
116 | }
117 |
118 | if ($scope.tracksToLookup) {
119 | $scope.lookupOnePageOfTracks();
120 | }
121 | }
122 |
123 | $scope.lookupOnePageOfTracks = function () {
124 | mopidy.library.lookup({ 'uris': $scope.tracksToLookup.splice(0, $scope.maxTracksToLookup) }).done(function (tracklistResult) {
125 | Object.values(tracklistResult).map(function(singleTrackResult) { return singleTrackResult[0]; }).forEach($scope.addTrackResult);
126 | });
127 | };
128 |
129 |
130 | $scope.handleSearchResult = function (res) {
131 | $scope.loading = false;
132 | $scope.tracks = [];
133 | $scope.tracksToLookup = [];
134 |
135 | var _index = 0;
136 | var _found = true;
137 | while (_found && _index < $scope.maxTracksToLookup) {
138 | _found = false;
139 | for (var i = 0; i < res.length; i++) {
140 | if (res[i].tracks && res[i].tracks[_index]) {
141 | $scope.addTrackResult(res[i].tracks[_index]);
142 | _found = true;
143 | }
144 | }
145 | _index++;
146 | }
147 |
148 | $scope.$apply();
149 | };
150 |
151 | $scope.addTrackResult = function (track) {
152 | $scope.tracks.push(track);
153 | mopidy.tracklist.filter([{ 'uri': [track.uri] }]).done(
154 | function (matches) {
155 | if (matches.length) {
156 | for (var i = 0; i < $scope.tracks.length; i++) {
157 | if ($scope.tracks[i].uri == matches[0].track.uri)
158 | $scope.tracks[i].disabled = true;
159 | }
160 | }
161 | $scope.$apply();
162 | });
163 | };
164 |
165 | $scope.addTrack = function (track) {
166 | track.disabled = true;
167 |
168 | $http.post('/party/add', track.uri).then(
169 | function success(response) {
170 | $scope.message = ['success', 'Queued: ' + track.name];
171 | },
172 | function error(response) {
173 | if (response.status === 409) {
174 | $scope.message = ['error', '' + response.data];
175 | } else {
176 | $scope.message = ['error', 'Code ' + response.status + ' - ' + response.data];
177 | }
178 | }
179 | );
180 | };
181 |
182 | $scope.nextTrack = function () {
183 | $http.get('/party/vote').then(
184 | function success(response) {
185 | $scope.message = ['success', '' + response.data];
186 | },
187 | function error(response) {
188 | $scope.message = ['error', '' + response.data];
189 | }
190 | );
191 | };
192 |
193 | $scope.getTrackSource = function (track) {
194 | var sourceAsText = 'unknown';
195 | if (track.uri) {
196 | sourceAsText = track.uri.split(':', '1')[0];
197 | }
198 |
199 | return sourceAsText;
200 | };
201 |
202 | $scope.getFontAwesomeIcon = function (source) {
203 | var sources_with_fa_icon = ['bandcamp', 'mixcloud', 'soundcloud', 'spotify', 'youtube'];
204 | var css_class = 'fa fa-music';
205 |
206 | if (source == 'local') {
207 | css_class = 'fa fa-folder';
208 | } else if (sources_with_fa_icon.includes(source)) {
209 | css_class = 'fa-brands fa-' + source;
210 | }
211 |
212 | return css_class;
213 | };
214 |
215 | $scope.togglePause = function () {
216 | var _fn = $scope.currentState.paused ? mopidy.playback.resume : mopidy.playback.pause;
217 | _fn().done();
218 | };
219 | });
220 |
--------------------------------------------------------------------------------
/mopidy_party/static/dark.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: black;
3 | color: white;
4 | }
5 | .list-group-item, .card-body {
6 | background: #333 !important;
7 | }
8 | .alert-success {
9 | background-color: #155724;
10 | color: #d4edda;
11 | border-color: transparent;
12 | }
13 | .alert-danger {
14 | background-color: #721c24;
15 | color: #f8d7da;
16 | border-color: transparent;
17 | }
18 | .alert {
19 | margin-top: 20px;
20 | }
21 | .add-button {
22 | float: left;
23 | margin: 10px 10px 10px 0px;
24 | }
25 | .tracksource {
26 | float: right;
27 | text-transform: uppercase;
28 | }
29 |
--------------------------------------------------------------------------------
/mopidy_party/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Party!
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {% if not hide_pause %}
23 |
26 | {% end %}
27 | {% if not hide_skip %}
28 |
31 | {% end %}
32 |