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)
7 # Trivial xbmc admin script to view active playlist, control volume,
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)
15 # any kind of error checking
17 from __future__ import unicode_literals
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
26 sys
.setdefaultencoding
('utf-8')
34 from xbmcjson import XBMC
35 from yattag import Doc
39 print ("content-type: text/html; charset=utf-8\n\n")
40 print ("<!DOCTYPE html>\n<html><head><title>partyparty beb</title></head><body>")
42 SONG_PROPERTIES
= ['album', 'artist', 'albumartist', 'dateadded', 'rating']
45 def __init__
(self
, 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]
52 self
.artist
= 'who fucking knows'
54 if len
(song
['album']) > 0:
55 self
.album
= song
['album']
57 self
.album
= 'album is for losers'
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.
66 libsong
= xbmc
.AudioLibrary
.GetSongDetails
(songid
= song
['id'], properties
= ['rating'])
68 if 'result' in libsong
and 'songdetails' in libsong
['result']:
69 song
['rating'] = libsong
['result']['songdetails']['rating']
70 elif
'songid' in song
:
72 self
.key
= hashlib
.sha256
(str
(song
['songid'])).hexdigest
()
73 self
.kodi_id
= song
['songid']
75 self
.key
= hashlib
.sha256
(song
['label'] + self
.artist
).hexdigest
()
78 # videos can still be labeled as songs, but the rating will be a
80 if 'rating' in song
and isinstance
(song
['rating'], numbers
.Integral
):
81 self
.rating
= song
['rating']
83 self
.rating
= -1 # might be better to use None here
85 self
.label
= song
['label']
88 '''Convert list of Kodi Items into Song instances'''
89 return [Song
(item
) for item
in items
]
91 def get_playlist
(playlistid
=0):
92 return songs
(xbmc
.Playlist
.GetItems
(playlistid
=playlistid
, properties
=SONG_PROPERTIES
)['result']['items'])
95 aactions
= {'songup': 'up', 'songdown': 'down', 'songdel': 'del', 'randomqueue': 'yeh', 'songrate': 'rate'}
97 def __init__
(self
, song
, actions
= ['songup', 'songdown', 'songdel']):
99 self
.actions
= actions
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
()
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):
120 for i
in range
(1,6):
121 with doc
.option
(value
= i
):
123 doc
.stag
('input', type
= 'hidden', name
= 'songkodiid', value
= self
.song
.kodi_id
)
125 return doc
.getvalue
()
129 def __init__
(self
, term
= '', prop
= 'title'):
133 res
= xbmc
.AudioLibrary
.GetSongs
(filter
={'operator': "contains", 'field': self
.prop
, 'value': self
.term
}, properties
=SONG_PROPERTIES
, sort={'order': 'ascending', 'method': 'artist'})['result']
135 self
.results
= songs
(res
['songs'])
139 def show_quick_search
(self
, thereal
=False
):
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>
148 <button type="submit" name="searchgo" value="1">Search</button>
150 ''').format
(self
.term
, 'id="quicksearch"' if thereal
else '')
152 def show_search_results
(self
):
153 doc
, tag
, text
= Doc
().tagtext
()
156 if len
(self
.results
) > 0:
158 for song
in self
.results
:
160 text
(u
'{} ({}) {}'.format
(song
.artist
, song
.album
, song
.label
))
161 doc
.asis
(SongControls
(song
, actions
= ['randomqueue']).controls
())
164 text
('You are unworthy. No results.')
166 print (doc
.getvalue
())
170 doc
, tag
, text
= Doc
().tagtext
()
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"); */
177 background-color: black;
180 button[name=songdel] { margin-left: 1em; margin-right: 1em; }
183 flex-flow: row nowrap;
184 justify-content: space-between;
187 ol li:nth-child(even) { background-color: #202020 }
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')]:
193 with tag
('a', href
= target
):
196 print (doc
.getvalue
())
198 form
= cgi
.FieldStorage
()
200 xbmc
= XBMC
("http://localhost:8080/jsonrpc")
202 def print_escaped
(item
):
203 print (u
"<p>{}</p>".format
(cgi
.escape
(u
"{}".format
(item
))))
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
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
)
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'])
268 playlist
= get_playlist
()
269 #playpos = random.randint (1, totalitems / (1 if 'asap' not in form else 3))
271 print ('<a name="controls"></a>')
272 print ('<p>Volume: {}%</p>'.format
(xbmc
.Application
.GetProperties
(properties
=['volume'])['result']['volume']))
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>
278 <button name="volchange" value="10" type="submit">+10</button>
279 <button name="volchange" value="-10" type="submit">-10</button>
281 <button name="volmute" value="1">Toggle Mute</button>
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>
295 print ('<a name="playlist"></a><h1>Playlist</h1>')
296 print ('<ol class="flex_list">')
297 for song
in playlist
:
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>")
304 print ('<a name="search"></a>')
305 Search
().show_quick_search
(thereal
=True
)
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>')
312 print ('</body></html>')