Commit | Line | Data |
---|---|---|
fffe2166 CE |
1 | #!/usr/bin/python |
2 | ||
3 | # ADD COMMAND TO RESTART PARTY MODE | |
4 | # (probably should require confirmation) | |
5 | # also add undelete link to post-del link just in case (reinsert at old pos) | |
6 | ||
7 | # Trivial xbmc admin script to view active playlist, control volume, | |
8 | # etc. | |
9 | ||
10 | # I would not recommend putting this online, no attempt is made at | |
11 | # being even trivially secure (e.g. form values are passed directly to | |
12 | # kodi with zero verification) | |
13 | ||
14 | # todo | |
15 | # any kind of error checking | |
16 | ||
17 | from __future__ import unicode_literals | |
18 | ||
19 | # Python is being obnoxious as hell and refusing to .format() utf-8 | |
20 | # strings. I have no idea. Just hack around it and deal with the | |
21 | # actual problem later instead of scattering the code with .encode | |
22 | # calls. | |
23 | ||
24 | import sys | |
25 | reload(sys) | |
26 | sys.setdefaultencoding ('utf-8') | |
27 | ||
28 | import cgi, cgitb | |
a68240a2 CE |
29 | import hashlib |
30 | import numbers | |
fffe2166 | 31 | import os |
fffe2166 | 32 | import random |
a68240a2 | 33 | import subprocess |
fffe2166 | 34 | from xbmcjson import XBMC |
a68240a2 | 35 | from yattag import Doc |
fffe2166 CE |
36 | |
37 | cgitb.enable() | |
38 | ||
39 | print ("content-type: text/html; charset=utf-8\n\n") | |
a68240a2 | 40 | print ("<!DOCTYPE html>\n<html><head><title>partyparty beb</title></head><body>") |
fffe2166 | 41 | |
a68240a2 | 42 | SONG_PROPERTIES = ['album', 'artist', 'albumartist', 'dateadded', 'rating'] |
fffe2166 | 43 | |
a68240a2 CE |
44 | class Song: |
45 | def __init__ (self, song): | |
46 | self._song = song | |
47 | if len(song['artist']) > 0: | |
48 | self.artist = song['artist'][0] | |
49 | elif 'albumartist' in song and len(song['albumartist']) > 0: | |
50 | self.artist = song['albumartist'][0] | |
51 | else: | |
52 | self.artist = 'who fucking knows' | |
fffe2166 | 53 | |
a68240a2 CE |
54 | if len(song['album']) > 0: |
55 | self.album = song['album'] | |
56 | else: | |
57 | self.album = 'album is for losers' | |
58 | ||
59 | if 'id' in song: | |
60 | # item from playlist | |
61 | self.key = hashlib.sha256(str(song['id'])).hexdigest() | |
62 | self.kodi_id = song['id'] | |
63 | # the playlist will not update things like ratings if we | |
64 | # update via RPC. Just grab it from the library instead. | |
65 | if 'rating' in song: | |
66 | libsong = xbmc.AudioLibrary.GetSongDetails (songid = song['id'], properties = ['rating']) | |
67 | #print (libsong) | |
68 | if 'result' in libsong and 'songdetails' in libsong['result']: | |
69 | song['rating'] = libsong['result']['songdetails']['rating'] | |
70 | elif 'songid' in song: | |
71 | # search results | |
72 | self.key = hashlib.sha256(str(song['songid'])).hexdigest() | |
73 | self.kodi_id = song['songid'] | |
74 | else: | |
75 | self.key = hashlib.sha256(song['label'] + self.artist).hexdigest() | |
76 | self.kodi_id = 0 | |
77 | ||
78 | # videos can still be labeled as songs, but the rating will be a | |
79 | # float... | |
80 | if 'rating' in song and isinstance (song['rating'], numbers.Integral): | |
81 | self.rating = song['rating'] | |
82 | else: | |
83 | self.rating = -1 # might be better to use None here | |
84 | ||
85 | self.label = song['label'] | |
86 | ||
87 | def songs(items): | |
88 | '''Convert list of Kodi Items into Song instances''' | |
89 | return [Song(item) for item in items] | |
fffe2166 | 90 | |
a68240a2 CE |
91 | def get_playlist (playlistid=0): |
92 | return songs (xbmc.Playlist.GetItems (playlistid=playlistid, properties=SONG_PROPERTIES)['result']['items']) | |
93 | ||
94 | class SongControls: | |
95 | aactions = {'songup': 'up', 'songdown': 'down', 'songdel': 'del', 'randomqueue': 'yeh', 'songrate': 'rate'} | |
96 | ||
97 | def __init__ (self, song, actions = ['songup', 'songdown', 'songdel']): | |
98 | self.song = song | |
99 | self.actions = actions | |
100 | ||
101 | def controls (self): | |
102 | doc, tag, text = Doc().tagtext() | |
103 | with tag ('form', method = 'post', action = 'admin.cgi', klass = 'song_controls'): #, style = 'display: inline-block'): | |
104 | for action in self.actions: | |
105 | with tag ('button', name = action, value = self.song.key): | |
106 | text (self.aactions[action]) | |
107 | doc.asis (self.extra_elements (action)) | |
108 | return doc.getvalue() | |
109 | ||
110 | def extra_elements (self, action): | |
111 | doc, tag, text = Doc().tagtext() | |
112 | if action == 'randomqueue': | |
113 | doc.stag ('input', type = 'hidden', name = 'songkodiid', value = self.song.kodi_id) | |
114 | elif action == 'songrate': | |
115 | if self.song.rating > -1: | |
116 | doc.defaults = {'songrating': self.song.rating} | |
117 | with doc.select (name = 'songrating'): | |
118 | with doc.option (value = 0): | |
119 | text ('na') | |
120 | for i in range (1,6): | |
121 | with doc.option (value = i): | |
122 | text (str (i)) | |
123 | doc.stag ('input', type = 'hidden', name = 'songkodiid', value = self.song.kodi_id) | |
124 | ||
125 | return doc.getvalue () | |
126 | ||
127 | ||
128 | class Search: | |
129 | def __init__ (self, term = '', prop = 'title'): | |
130 | self.term = term | |
131 | self.prop = prop | |
132 | if (term != ''): | |
133 | res = xbmc.AudioLibrary.GetSongs (filter={'operator': "contains", 'field': self.prop, 'value': self.term}, properties=SONG_PROPERTIES, sort={'order': 'ascending', 'method': 'artist'})['result'] | |
134 | if 'songs' in res: | |
135 | self.results = songs(res['songs']) | |
136 | else: | |
137 | self.results = [] | |
138 | ||
139 | def show_quick_search (self, thereal=False): | |
140 | print (u''' | |
fffe2166 CE |
141 | <form method="get" action="admin.cgi" style="display: inline-block"> |
142 | <input type="text" name="searchterm" value="{}" {} /> | |
143 | <select name="searchfield"> | |
144 | <option value="title">name</option> | |
a68240a2 | 145 | <option value="artist">artist</option> |
fffe2166 CE |
146 | <option value="album">album</option> |
147 | </select> | |
148 | <button type="submit" name="searchgo" value="1">Search</button> | |
149 | </form> | |
a68240a2 CE |
150 | ''').format(self.term, 'id="quicksearch"' if thereal else '') |
151 | ||
152 | def show_search_results (self): | |
153 | doc, tag, text = Doc().tagtext() | |
154 | with tag ('h1'): | |
155 | text ('Results') | |
156 | if len (self.results) > 0: | |
157 | with tag ('ol'): | |
158 | for song in self.results: | |
159 | with tag ('li'): | |
160 | text (u'{} ({}) {}'.format (song.artist, song.album, song.label)) | |
161 | doc.asis (SongControls (song, actions = ['randomqueue']).controls ()) | |
162 | else: | |
163 | with tag ('p'): | |
164 | text ('You are unworthy. No results.') | |
165 | ||
166 | print (doc.getvalue ()) | |
167 | ||
168 | ||
169 | def show_menu (): | |
170 | doc, tag, text = Doc().tagtext() | |
171 | with tag ('style'): | |
172 | text (''' | |
173 | input, select, button { font-size: 200%; margin: 0.1em; } | |
174 | .horiz-menu li { display: inline; padding-right: 0.5em; } | |
175 | body { /* background-image: url("fire-under-construction-animation.gif"); */ | |
176 | color: white; | |
177 | background-color: black; | |
178 | } | |
179 | a { color: #5dfc0a} | |
180 | button[name=songdel] { margin-left: 1em; margin-right: 1em; } | |
181 | div.flex_row { | |
182 | display: flex; | |
183 | flex-flow: row nowrap; | |
184 | justify-content: space-between; | |
185 | } | |
186 | ||
187 | ol li:nth-child(even) { background-color: #202020 } | |
188 | ''') | |
189 | with tag ('ul', klass = 'horiz-menu'): | |
190 | for target, description in [('admin.cgi', 'reload'), ('#playlist', 'playlist'), | |
191 | ('#controls', 'controls'), ('javascript:document.getElementById("quicksearch").focus()', 'search')]: | |
192 | with tag ('li'): | |
193 | with tag ('a', href = target): | |
194 | text (description) | |
195 | ||
196 | print (doc.getvalue ()) | |
197 | ||
198 | form = cgi.FieldStorage () | |
199 | ||
200 | xbmc = XBMC ("http://localhost:8080/jsonrpc") | |
201 | ||
202 | def print_escaped (item): | |
203 | print (u"<p>{}</p>".format (cgi.escape (u"{}".format (item)))) | |
204 | ||
205 | show_menu () | |
fffe2166 CE |
206 | |
207 | if 'songdel' in form: | |
a68240a2 CE |
208 | songid = form['songdel'].value |
209 | print (u"<p>{}</p>".format (songid)) | |
210 | (pos,song) = next ((i,s) for i,s in enumerate(get_playlist ()) if s.key == songid) | |
211 | print (u'<p>Deleted {}</p>'.format(cgi.escape (song.label))) | |
212 | print_escaped (xbmc.Playlist.Remove (playlistid=0, position=pos)) | |
fffe2166 | 213 | elif 'songup' in form: |
a68240a2 CE |
214 | songid = form['songup'].value |
215 | print (u"<p>{}</p>".format (songid)) | |
216 | (pos,song) = next ((i,s) for i,s in enumerate(get_playlist ()) if s.key == songid) | |
217 | print (u"<p>Promoted {}</p>".format(cgi.escape(song.label))) | |
218 | print_escaped (xbmc.Playlist.Swap (playlistid=0, position1=pos, position2=pos-1)) | |
fffe2166 | 219 | elif 'songdown' in form: |
a68240a2 CE |
220 | songid = form['songdown'].value |
221 | print (u"<p>{}</p>".format (songid)) | |
222 | (pos,song) = next ((i,s) for i,s in enumerate(get_playlist ()) if s.key == songid) | |
223 | print (u"<p>Demoted {}</p>".format(cgi.escape(song.label))) | |
224 | print_escaped (xbmc.Playlist.Swap (playlistid=0, position1=pos, position2=pos+1)) | |
fffe2166 CE |
225 | elif 'volchange' in form: |
226 | curvolume = xbmc.Application.GetProperties (properties=['volume'])['result']['volume'] | |
227 | newvolume = max (0, min (int (form['volchange'].value) + curvolume, 100)) | |
a68240a2 | 228 | print_escaped (xbmc.Application.SetVolume (volume=newvolume)) |
fffe2166 | 229 | elif 'volmute' in form: |
a68240a2 | 230 | print_escaped (xbmc.Application.SetMute (mute="toggle")) |
fffe2166 CE |
231 | elif 'navigate' in form: |
232 | action = form['navigate'].value | |
233 | if action == 'prev': | |
a68240a2 | 234 | print_escaped (xbmc.Player.GoTo (to="previous", playerid=0)) |
fffe2166 | 235 | elif action == 'next': |
a68240a2 | 236 | print_escaped (xbmc.Player.GoTo (to="next", playerid=0)) |
fffe2166 | 237 | elif action == 'playpause': |
a68240a2 | 238 | print_escaped (xbmc.Player.PlayPause (play="toggle", playerid=0)) |
fffe2166 CE |
239 | elif 'searchgo' in form: |
240 | term = form['searchterm'].value | |
241 | field = form['searchfield'].value | |
a68240a2 CE |
242 | search = Search (term, field) |
243 | search.show_quick_search () | |
244 | search.show_search_results () | |
fffe2166 | 245 | elif 'randomqueue' in form: |
a68240a2 | 246 | songid = int(form['songkodiid'].value) |
fffe2166 CE |
247 | totalitems = xbmc.Playlist.GetItems (playlistid=0)['result']['limits']['total'] |
248 | playpos = random.randint (1, totalitems / 3 + 1) | |
a68240a2 | 249 | print_escaped (xbmc.Playlist.Insert (playlistid=0, item={"songid": songid}, position=playpos)) |
fffe2166 | 250 | print '<p style="font-size: x-large">Your song is number {0} in the queue ({1} songs in playlist).</p>'.format (playpos, totalitems+1) |
a68240a2 CE |
251 | elif 'songrate' in form: |
252 | songid = int(form['songkodiid'].value) | |
253 | newrating = int(form['songrating'].value) | |
254 | print (songid) | |
255 | print (newrating) | |
256 | print_escaped (xbmc.AudioLibrary.SetSongDetails (songid = songid, rating = newrating)) | |
257 | print_escaped (u'Rating Changed') | |
258 | elif 'partyon' in form: | |
259 | if 'error' in xbmc.Player.SetPartymode (partymode=True, playerid=0): | |
260 | xbmc.Player.Open (item={"partymode": "music"}) | |
261 | elif 'lockon' in form: | |
262 | subprocess.call (['/usr/bin/xscreensaver-command', 'lock']) | |
fffe2166 CE |
263 | |
264 | ||
265 | ||
266 | ||
267 | ||
a68240a2 | 268 | playlist = get_playlist () |
fffe2166 CE |
269 | #playpos = random.randint (1, totalitems / (1 if 'asap' not in form else 3)) |
270 | ||
271 | print ('<a name="controls"></a>') | |
272 | print ('<p>Volume: {}%</p>'.format(xbmc.Application.GetProperties (properties=['volume'])['result']['volume'])) | |
273 | print (''' | |
274 | <form method="post" action="admin.cgi" style="display: inline-block"> | |
275 | <button name="volchange" value="5" type="submit">+5</button> | |
276 | <button name="volchange" value="-5" type="submit">-5</button> | |
277 | ||
278 | <button name="volchange" value="10" type="submit">+10</button> | |
279 | <button name="volchange" value="-10" type="submit">-10</button> | |
280 | ||
281 | <button name="volmute" value="1">Toggle Mute</button> | |
282 | ||
283 | </form> | |
284 | ''') | |
285 | ||
286 | print (''' | |
287 | <form method="post" action="admin.cgi" style="display: inline-block"> | |
288 | <button name="navigate" value="prev" type="submit">⏮</button> | |
289 | <button name="navigate" value="next" type="submit">⏭</button> | |
290 | <button name="navigate" value="playpause" type="submit">⏯</button> | |
291 | </form> | |
292 | ''') | |
293 | ||
294 | ||
295 | print ('<a name="playlist"></a><h1>Playlist</h1>') | |
a68240a2 | 296 | print ('<ol class="flex_list">') |
fffe2166 | 297 | for song in playlist: |
a68240a2 CE |
298 | # print (song._song) |
299 | print (u'<li><div class="flex_row"><p><a href="admin.cgi?searchgo=1&searchterm={0};searchfield=artist">{0}</a> {1}</p>'.format(song.artist, song.label).encode('UTF-8')) | |
300 | print (SongControls (song, ['songup', 'songdown', 'songdel', 'songrate']).controls ()) | |
301 | print ("</div></li>") | |
fffe2166 CE |
302 | print ("</ol>") |
303 | ||
304 | print ('<a name="search"></a>') | |
a68240a2 CE |
305 | Search ().show_quick_search (thereal=True) |
306 | show_menu () | |
307 | ||
308 | print ('<form method="post" action="admin.cgi" style="display: inline-block">') | |
309 | print ('<button name="partyon" value="true">re-enable party</button>') | |
310 | print ('<button name="lockon" value="true">lock em out</button>') | |
311 | print ('</form>') | |
312 | print ('</body></html>') |