Begin unsucking kodi admin panel
authorClinton Ebadi <clinton@unknownlamer.org>
Sun, 29 Nov 2015 20:40:14 +0000 (15:40 -0500)
committerGive Me Party Or Give Me Death <partyparty@neurasthenic.home.unknownlamer.org>
Sun, 29 Nov 2015 20:46:53 +0000 (15:46 -0500)
After hitting 300 lines of drunken extensions to make up for defects
discovered during parties, I decided to start cleaning up this code.

This is the beginning -- enough changes to need to save my place before
doing anything more radical. Goals (partially achieved, for some
definition of achieve):

 - Use yattag for html formatting instead of gross blocks of `print'
   that don't correctly escape anything
 - Centralize hacks to work around Kodi JSON RPC deficiencies
 - Code split up so that it can be reused in normal party goer interface

party-upload/README
party-upload/admin.cgi

index 5ad758c..582662d 100644 (file)
@@ -1,10 +1,14 @@
-Trivial Party Mode Upload Script
+Trivial Party Mode Upload and Admin Scripts
 
-Requires python-xbmc <https://github.com/jcsaaddupuy/python-xbmc>.
+Requires python-xbmc <https://github.com/jcsaaddupuy/python-xbmc> and
+yattag (http://yattag.org)
 
 Throw upload.cgi and upload.html into a directory together, generate a
 QR code URL for friends to scan, and now anyone can upload music from
 their phones to your party playlist. No configuration file, modify
 upload.cgi directly if your user/pass differ from the default.
 
+admin.cgi allows for basic playlist management -- moving songs,
+deleting, rating, search, and randomly queuing search results.
+
 Patches to add a config file or anything really are welcome.
\ No newline at end of file
index 4c67bb9..88fea04 100755 (executable)
@@ -26,112 +26,246 @@ reload(sys)
 sys.setdefaultencoding ('utf-8')
 
 import cgi, cgitb
+import hashlib
+import numbers
 import os
-import subprocess
 import random
+import subprocess
 from xbmcjson import XBMC
+from yattag import Doc
 
 cgitb.enable()
 
 print ("content-type: text/html; charset=utf-8\n\n")
+print ("<!DOCTYPE html>\n<html><head><title>partyparty beb</title></head><body>")
 
-print (u'''
-<style>
-input, select, button { font-size: 200%}
-.horiz-menu li { display: inline }
- body { /* background-image: url("fire-under-construction-animation.gif"); */
-color: white;
-background-color: black;
-}
-</style>
-
-<ul class="horiz-menu"">
-<li><a href="admin.cgi">reload</a></li>
-<li><a href="#playlist">playlist</a></li>
-<li><a href="#controls">control</a></li>
-<li><a href="javascript:document.getElementById(&quot;quicksearch&quot;).focus()">search</a></li>
-</ul>
-''')
+SONG_PROPERTIES = ['album', 'artist', 'albumartist', 'dateadded', 'rating']
 
-music_dir = "/srv/archive/incoming/stolen-moosic"
-form = cgi.FieldStorage ()
+class Song:
+   def __init__ (self, song):
+      self._song = song
+      if len(song['artist']) > 0:
+         self.artist = song['artist'][0]
+      elif 'albumartist' in song and len(song['albumartist']) > 0:
+         self.artist = song['albumartist'][0]
+      else:
+         self.artist = 'who fucking knows'
 
-xbmc = XBMC ("http://localhost:8080/jsonrpc")
+      if len(song['album']) > 0:
+         self.album = song['album']
+      else:
+         self.album = 'album is for losers'
+
+      if 'id' in song:
+         # item from playlist
+         self.key = hashlib.sha256(str(song['id'])).hexdigest()
+         self.kodi_id  = song['id']
+         # the playlist will not update things like ratings if we
+         # update via RPC. Just grab it from the library instead.
+         if 'rating' in song:
+            libsong = xbmc.AudioLibrary.GetSongDetails (songid = song['id'], properties = ['rating'])
+            #print (libsong)
+            if 'result' in libsong and 'songdetails' in libsong['result']:
+               song['rating'] = libsong['result']['songdetails']['rating']
+      elif 'songid' in song:
+         # search results
+         self.key = hashlib.sha256(str(song['songid'])).hexdigest()
+         self.kodi_id  = song['songid']
+      else:
+         self.key = hashlib.sha256(song['label'] + self.artist).hexdigest()
+         self.kodi_id = 0
+
+      # videos can still be labeled as songs, but the rating will be a
+      # float...
+      if 'rating' in song and isinstance (song['rating'], numbers.Integral):
+         self.rating = song['rating']
+      else:
+         self.rating = -1 # might be better to use None here
+
+      self.label = song['label']
+         
+def songs(items):
+   '''Convert list of Kodi Items into Song instances'''
+   return [Song(item) for item in items]
 
-def show_quick_search (term="",thereal=False):
-   print (u'''
+def get_playlist (playlistid=0):
+   return songs (xbmc.Playlist.GetItems (playlistid=playlistid, properties=SONG_PROPERTIES)['result']['items'])
+
+class SongControls:
+   aactions = {'songup': 'up', 'songdown': 'down', 'songdel': 'del', 'randomqueue': 'yeh', 'songrate': 'rate'}
+
+   def __init__ (self, song, actions = ['songup', 'songdown', 'songdel']):
+      self.song = song
+      self.actions = actions
+
+   def controls (self):
+      doc, tag, text = Doc().tagtext()
+      with tag ('form', method = 'post', action = 'admin.cgi', klass = 'song_controls'): #, style = 'display: inline-block'):
+         for action in self.actions:
+            with tag ('button', name = action, value = self.song.key):
+               text (self.aactions[action])
+            doc.asis (self.extra_elements (action))
+      return doc.getvalue()
+
+   def extra_elements (self, action):
+      doc, tag, text = Doc().tagtext()
+      if action == 'randomqueue':
+         doc.stag ('input', type = 'hidden', name = 'songkodiid', value = self.song.kodi_id)
+      elif action == 'songrate':
+         if self.song.rating > -1:
+            doc.defaults = {'songrating': self.song.rating}
+            with doc.select (name = 'songrating'):
+               with doc.option (value = 0):
+                  text ('na')
+               for i in range (1,6):
+                  with doc.option (value = i):
+                     text (str (i))
+            doc.stag ('input', type = 'hidden', name = 'songkodiid', value = self.song.kodi_id)
+
+      return doc.getvalue ()
+      
+
+class Search:
+   def __init__ (self, term = '', prop = 'title'):
+      self.term = term
+      self.prop = prop
+      if (term != ''):
+         res = xbmc.AudioLibrary.GetSongs (filter={'operator': "contains", 'field': self.prop, 'value': self.term}, properties=SONG_PROPERTIES, sort={'order': 'ascending', 'method': 'artist'})['result']
+         if 'songs' in res:
+            self.results = songs(res['songs'])
+         else:
+            self.results = []
+
+   def show_quick_search (self, thereal=False):
+      print (u'''
 <form method="get" action="admin.cgi" style="display: inline-block">
 <input type="text" name="searchterm" value="{}" {} />
 <select name="searchfield">
 <option value="title">name</option>
-<option value="albumartist">artist</option>
+<option value="artist">artist</option>
 <option value="album">album</option>
 </select>
 <button type="submit" name="searchgo" value="1">Search</button>
 </form>
-   ''').format(term, 'id="quicksearch"' if thereal else '')
-
-def show_search_results (songs):
-   print (u"<h1>Results</h1>")
-   print (u"<ol>")
-   for song in songs:
-      print (u'<li>{} ({}) {}'.format (song['albumartist'][0], song['album'], song['label']))
-      print ('<form method="post" action="admin.cgi" style="display: inline-block">')
-      print ('<button type="submit" name="randomqueue" value="{}">yeh</button>'.format(song['songid']))
-      print ('</form>')
-      print ('</li>')
-   print (u"</ol>")
+   ''').format(self.term, 'id="quicksearch"' if thereal else '')
+
+   def show_search_results (self):
+      doc, tag, text = Doc().tagtext()
+      with tag ('h1'):
+         text ('Results')
+      if len (self.results) > 0:
+         with tag ('ol'):
+            for song in self.results:
+               with tag ('li'):
+                  text (u'{} ({}) {}'.format (song.artist, song.album, song.label))
+                  doc.asis (SongControls (song, actions = ['randomqueue']).controls ())
+      else:
+         with tag ('p'):
+            text ('You are unworthy. No results.')
+
+      print (doc.getvalue ())
+      
+
+def show_menu ():
+   doc, tag, text = Doc().tagtext()
+   with tag ('style'):
+      text ('''
+input, select, button { font-size: 200%; margin: 0.1em; }
+.horiz-menu li { display: inline; padding-right: 0.5em; }
+body {  /* background-image: url("fire-under-construction-animation.gif");  */
+  color: white;
+  background-color: black;
+}
+a { color: #5dfc0a}
+button[name=songdel] { margin-left: 1em; margin-right: 1em; }
+div.flex_row {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: space-between;
+}
+
+ol li:nth-child(even) { background-color: #202020 }
+''')
+   with tag ('ul', klass = 'horiz-menu'):
+      for target, description in [('admin.cgi', 'reload'), ('#playlist', 'playlist'),
+                                  ('#controls', 'controls'), ('javascript:document.getElementById("quicksearch").focus()', 'search')]:
+         with tag ('li'):
+            with tag ('a', href = target):
+               text (description)
+
+   print (doc.getvalue ())
+
+form = cgi.FieldStorage ()
+
+xbmc = XBMC ("http://localhost:8080/jsonrpc")
+
+def print_escaped (item):
+   print (u"<p>{}</p>".format (cgi.escape (u"{}".format (item))))
+   
+show_menu ()
 
 if 'songdel' in form:
-   songid = int(form['songdel'].value)
-   print (songid)
-   (pos,name) = next ((i,s) for i,s in enumerate(xbmc.Playlist.GetItems (playlistid=0)['result']['items']) if s['id'] == songid)
-   print (u"<p>Deleted {}</p>".format(name['label']))
-   print (xbmc.Playlist.Remove (playlistid=0, position=pos))
+   songid = form['songdel'].value
+   print (u"<p>{}</p>".format (songid))
+   (pos,song) = next ((i,s) for i,s in enumerate(get_playlist ()) if s.key == songid)
+   print (u'<p>Deleted {}</p>'.format(cgi.escape (song.label)))
+   print_escaped (xbmc.Playlist.Remove (playlistid=0, position=pos))
 elif 'songup' in form:
-   songid = int(form['songup'].value)
-   print (songid)
-   (pos,name) = next ((i,s) for i,s in enumerate(xbmc.Playlist.GetItems (playlistid=0)['result']['items']) if s['id'] == songid)
-   print (u"<p>Promoted {}</p>".format(name['label']))
-   print (xbmc.Playlist.Swap (playlistid=0, position1=pos, position2=pos-1))
+   songid = form['songup'].value
+   print (u"<p>{}</p>".format (songid))
+   (pos,song) = next ((i,s) for i,s in enumerate(get_playlist ()) if s.key == songid)
+   print (u"<p>Promoted {}</p>".format(cgi.escape(song.label)))
+   print_escaped (xbmc.Playlist.Swap (playlistid=0, position1=pos, position2=pos-1))
 elif 'songdown' in form:
-   songid = int(form['songdown'].value)
-   print (songid)
-   (pos,name) = next ((i,s) for i,s in enumerate(xbmc.Playlist.GetItems (playlistid=0)['result']['items']) if s['id'] == songid)
-   print (u"<p>Demoted {}</p>".format(name['label']))
-   print (xbmc.Playlist.Swap (playlistid=0, position1=pos, position2=pos+1))
+   songid = form['songdown'].value
+   print (u"<p>{}</p>".format (songid))
+   (pos,song) = next ((i,s) for i,s in enumerate(get_playlist ()) if s.key == songid)
+   print (u"<p>Demoted {}</p>".format(cgi.escape(song.label)))
+   print_escaped (xbmc.Playlist.Swap (playlistid=0, position1=pos, position2=pos+1))
 elif 'volchange' in form:
    curvolume = xbmc.Application.GetProperties (properties=['volume'])['result']['volume']
    newvolume = max (0, min (int (form['volchange'].value) + curvolume, 100))
-   print (xbmc.Application.SetVolume (volume=newvolume))
+   print_escaped (xbmc.Application.SetVolume (volume=newvolume))
 elif 'volmute' in form:
-   print (xbmc.Application.SetMute (mute="toggle"))
+   print_escaped (xbmc.Application.SetMute (mute="toggle"))
 elif 'navigate' in form:
    action = form['navigate'].value
    if action == 'prev':
-      print xbmc.Player.GoTo (to="previous", playerid=0)
+      print_escaped (xbmc.Player.GoTo (to="previous", playerid=0))
    elif action == 'next':
-      print xbmc.Player.GoTo (to="next", playerid=0)
+      print_escaped (xbmc.Player.GoTo (to="next", playerid=0))
    elif action == 'playpause':
-      print xbmc.Player.PlayPause (play="toggle",  playerid=0)
+      print_escaped (xbmc.Player.PlayPause (play="toggle",  playerid=0))
 elif 'searchgo' in form:
    term = form['searchterm'].value
    field = form['searchfield'].value
-   results = xbmc.AudioLibrary.GetSongs (filter={'operator': "contains", 'field': field, 'value': term}, properties=['album', 'albumartist'], sort={'order': 'ascending', 'method': 'artist'})['result']['songs']
-   show_quick_search (term)
-   show_search_results (results)
+   search = Search (term, field)
+   search.show_quick_search ()
+   search.show_search_results ()
 elif 'randomqueue' in form:
-   songid = int(form['randomqueue'].value)
+   songid = int(form['songkodiid'].value)
    totalitems = xbmc.Playlist.GetItems (playlistid=0)['result']['limits']['total']
    playpos = random.randint (1, totalitems / 3 + 1)
-   print xbmc.Playlist.Insert (playlistid=0, item={"songid": songid}, position=playpos)
+   print_escaped (xbmc.Playlist.Insert (playlistid=0, item={"songid": songid}, position=playpos))
    print '<p style="font-size: x-large">Your song is number {0} in the queue ({1} songs in playlist).</p>'.format (playpos, totalitems+1)
+elif 'songrate' in form:
+   songid = int(form['songkodiid'].value)
+   newrating = int(form['songrating'].value)
+   print (songid)
+   print (newrating)
+   print_escaped (xbmc.AudioLibrary.SetSongDetails (songid = songid, rating = newrating))
+   print_escaped (u'Rating Changed')
+elif 'partyon' in form:
+   if 'error' in xbmc.Player.SetPartymode (partymode=True, playerid=0):
+      xbmc.Player.Open (item={"partymode": "music"})
+elif 'lockon' in form:
+   subprocess.call (['/usr/bin/xscreensaver-command', 'lock'])
 
 
 
 
 
-playlist = xbmc.Playlist.GetItems (playlistid=0, properties=['album', 'albumartist'])['result']['items']
+playlist = get_playlist ()
 #playpos = random.randint (1, totalitems / (1 if 'asap' not in form else 3))
 
 print ('<a name="controls"></a>')
@@ -159,17 +293,20 @@ print ('''
 
 
 print ('<a name="playlist"></a><h1>Playlist</h1>')
-print ("<ol>")
+print ('<ol class="flex_list">')
 for song in playlist:
-   print (u'<li>{} {}'.format(song['albumartist'][0], song['label']).encode('UTF-8'))
-
-   print ('<form method="post" action="admin.cgi" style="display: inline-block">')
-   print ('<button name="songdel" value="{}">del</button>'.format(song['id']))
-   print ('<button name="songup" value="{}">up</button>'.format(song['id']))
-   print ('<button name="songdown" value="{}">down</button>'.format(song['id']))
-   print ('</form>'   )
-   print ("</li>")
+#   print (song._song)
+   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'))
+   print (SongControls (song, ['songup', 'songdown', 'songdel', 'songrate']).controls ())
+   print ("</div></li>")
 print ("</ol>")
 
 print ('<a name="search"></a>')
-show_quick_search (thereal=True)
+Search ().show_quick_search (thereal=True)
+show_menu ()
+
+print ('<form method="post" action="admin.cgi" style="display: inline-block">')
+print ('<button name="partyon" value="true">re-enable party</button>')
+print ('<button name="lockon" value="true">lock em out</button>')
+print ('</form>')
+print ('</body></html>')