Begin unsucking kodi admin panel
[clinton/unknownlamer-kodi-addons.git] / party-upload / admin.cgi
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
29 import hashlib
30 import numbers
31 import os
32 import random
33 import subprocess
34 from xbmcjson import XBMC
35 from yattag import Doc
36
37 cgitb.enable()
38
39 print ("content-type: text/html; charset=utf-8\n\n")
40 print ("<!DOCTYPE html>\n<html><head><title>partyparty beb</title></head><body>")
41
42 SONG_PROPERTIES = ['album', 'artist', 'albumartist', 'dateadded', 'rating']
43
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'
53
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]
90
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'''
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>
145 <option value="artist">artist</option>
146 <option value="album">album</option>
147 </select>
148 <button type="submit" name="searchgo" value="1">Search</button>
149 </form>
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 ()
206
207 if 'songdel' in form:
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))
213 elif 'songup' in form:
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))
219 elif 'songdown' in form:
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))
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))
228 print_escaped (xbmc.Application.SetVolume (volume=newvolume))
229 elif 'volmute' in form:
230 print_escaped (xbmc.Application.SetMute (mute="toggle"))
231 elif 'navigate' in form:
232 action = form['navigate'].value
233 if action == 'prev':
234 print_escaped (xbmc.Player.GoTo (to="previous", playerid=0))
235 elif action == 'next':
236 print_escaped (xbmc.Player.GoTo (to="next", playerid=0))
237 elif action == 'playpause':
238 print_escaped (xbmc.Player.PlayPause (play="toggle", playerid=0))
239 elif 'searchgo' in form:
240 term = form['searchterm'].value
241 field = form['searchfield'].value
242 search = Search (term, field)
243 search.show_quick_search ()
244 search.show_search_results ()
245 elif 'randomqueue' in form:
246 songid = int(form['songkodiid'].value)
247 totalitems = xbmc.Playlist.GetItems (playlistid=0)['result']['limits']['total']
248 playpos = random.randint (1, totalitems / 3 + 1)
249 print_escaped (xbmc.Playlist.Insert (playlistid=0, item={"songid": songid}, position=playpos))
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)
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'])
263
264
265
266
267
268 playlist = get_playlist ()
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">&#x23ee;</button>
289 <button name="navigate" value="next" type="submit">&#x23ed;</button>
290 <button name="navigate" value="playpause" type="submit">&#x23ef;</button>
291 </form>
292 ''')
293
294
295 print ('<a name="playlist"></a><h1>Playlist</h1>')
296 print ('<ol class="flex_list">')
297 for song in playlist:
298 # print (song._song)
299 print (u'<li><div class="flex_row"><p><a href="admin.cgi?searchgo=1&amp;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>")
302 print ("</ol>")
303
304 print ('<a name="search"></a>')
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>')