more unsucking, plus a year of new stuff
authorClinton Ebadi <clinton@unknownlamer.org>
Sun, 27 Nov 2016 03:10:28 +0000 (22:10 -0500)
committerClinton Ebadi <clinton@unknownlamer.org>
Sun, 27 Nov 2016 03:18:33 +0000 (22:18 -0500)
I was bad and didn't commit anything for ages, so here's an
approximation of what has changed:

 * moved common code into module, converted admin and upload to use it
 * Encapsulated parts of the interface into classes. The code is still
   pretty fugly.
 * New "normals" interface that allows general party goers to
   browse/search your library and throw songs in randomly.
 * Use yattag in more places
 * Layout is now using CSS flex boxes for better alignment and scaling
   for different screen sizes.
 * New commands to rate songs, bump songs to the top or bottom of the
   queue, search improvements, and probably some other things

party-upload/admin.cgi
party-upload/normals.cgi [new file with mode: 0755]
party-upload/partyparty.py [new file with mode: 0644]
party-upload/upload.cgi
party-upload/upload.html

dissimilarity index 79%
index 88fea04..56c523d 100755 (executable)
-#!/usr/bin/python
-
-# ADD COMMAND TO RESTART PARTY MODE
-# (probably should require confirmation)
-# also add undelete link to post-del link just in case (reinsert at old pos)
-
-# Trivial xbmc admin script to view active playlist, control volume,
-# etc.
-
-# I would not recommend putting this online, no attempt is made at
-# being even trivially secure (e.g. form values are passed directly to
-# kodi with zero verification)
-
-# todo
-# any kind of error checking
-
-from __future__ import unicode_literals
-
-# Python is being obnoxious as hell and refusing to .format() utf-8
-# strings. I have no idea. Just hack around it and deal with the
-# actual problem later instead of scattering the code with .encode
-# calls.
-
-import sys
-reload(sys)
-sys.setdefaultencoding ('utf-8')
-
-import cgi, cgitb
-import hashlib
-import numbers
-import os
-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>")
-
-SONG_PROPERTIES = ['album', 'artist', 'albumartist', 'dateadded', 'rating']
-
-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'
-
-      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 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="artist">artist</option>
-<option value="album">album</option>
-</select>
-<button type="submit" name="searchgo" value="1">Search</button>
-</form>
-   ''').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 = 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 = 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 = 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_escaped (xbmc.Application.SetVolume (volume=newvolume))
-elif 'volmute' in form:
-   print_escaped (xbmc.Application.SetMute (mute="toggle"))
-elif 'navigate' in form:
-   action = form['navigate'].value
-   if action == 'prev':
-      print_escaped (xbmc.Player.GoTo (to="previous", playerid=0))
-   elif action == 'next':
-      print_escaped (xbmc.Player.GoTo (to="next", playerid=0))
-   elif action == 'playpause':
-      print_escaped (xbmc.Player.PlayPause (play="toggle",  playerid=0))
-elif 'searchgo' in form:
-   term = form['searchterm'].value
-   field = form['searchfield'].value
-   search = Search (term, field)
-   search.show_quick_search ()
-   search.show_search_results ()
-elif 'randomqueue' in form:
-   songid = int(form['songkodiid'].value)
-   totalitems = xbmc.Playlist.GetItems (playlistid=0)['result']['limits']['total']
-   playpos = random.randint (1, totalitems / 3 + 1)
-   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 = get_playlist ()
-#playpos = random.randint (1, totalitems / (1 if 'asap' not in form else 3))
-
-print ('<a name="controls"></a>')
-print ('<p>Volume: {}%</p>'.format(xbmc.Application.GetProperties (properties=['volume'])['result']['volume']))
-print ('''
-<form method="post" action="admin.cgi" style="display: inline-block">
-<button name="volchange" value="5" type="submit">+5</button>
-<button name="volchange" value="-5" type="submit">-5</button>
-
-<button name="volchange" value="10" type="submit">+10</button>
-<button name="volchange" value="-10" type="submit">-10</button>
-
-<button name="volmute" value="1">Toggle Mute</button>
-
-</form>
-''')
-
-print ('''
-<form method="post" action="admin.cgi" style="display: inline-block">
-<button name="navigate" value="prev" type="submit">&#x23ee;</button>
-<button name="navigate" value="next" type="submit">&#x23ed;</button>
-<button name="navigate" value="playpause" type="submit">&#x23ef;</button>
-</form>
-''')
-
-
-print ('<a name="playlist"></a><h1>Playlist</h1>')
-print ('<ol class="flex_list">')
-for song in playlist:
-#   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>')
-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>')
+#!/usr/bin/python
+# Simple PartyMode Web Console
+# Copyright (c) 2015,2016  Clinton Ebadi <clinton@unknownlamer.org>
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Trivial xbmc admin script to view active playlist, control volume,
+# etc.
+
+# I would not recommend putting this online, no attempt is made at
+# being even trivially secure (e.g. form values are passed directly to
+# kodi with zero verification)
+
+# ADD COMMAND TO RESTART PARTY MODE
+# (probably should require confirmation)
+# also add undelete link to post-del link just in case (reinsert at old pos)
+
+# todo
+# any kind of error checking
+
+from __future__ import unicode_literals
+
+# Python is being obnoxious as hell and refusing to .format() utf-8
+# strings. I have no idea. Just hack around it and deal with the
+# actual problem later instead of scattering the code with .encode
+# calls.
+
+import sys
+reload(sys)
+sys.setdefaultencoding ('utf-8')
+
+import cgi, cgitb
+from datetime import datetime
+import hashlib
+import numbers
+import os
+import random
+import subprocess
+from xbmcjson import XBMC
+from yattag import Doc
+
+import partyparty
+from partyparty import Song, SongControls, Search, Playlist, PartyManager
+
+cgitb.enable()
+PAGE_SELF = os.environ['SCRIPT_NAME'] if 'SCRIPT_NAME' in os.environ else ''
+
+print ("content-type: text/html; charset=utf-8\n\n")
+print ("<!DOCTYPE html>\n<html><head><title>partyparty beb</title></head><body>")
+print (partyparty.css ())
+
+#print (os.environ)
+#print (os.environ['SCRIPT_NAME'])
+
+def show_menu ():
+   doc, tag, text = Doc().tagtext()
+   with tag ('ul', klass = 'horiz-menu flex_row'):
+      for target, description in [('#playlist', 'playlist'),
+                                  ('#controls', 'controls'),
+                                  ('javascript:document.getElementById("quicksearch").focus()', 'search'),
+                                  (PAGE_SELF, 'reload')]:
+         with tag ('li'):
+            with tag ('a', href = target):
+               text (description)
+
+   print (doc.getvalue ())
+
+xbmc = partyparty.connect (XBMC ("http://localhost:8080/jsonrpc"))
+
+def print_escaped (item):
+   print (u"<p>{}</p>".format (cgi.escape (u"{}".format (item))))
+
+show_menu ()
+
+manager = PartyManager (cgi.FieldStorage ())
+manager.process ()
+
+playlist = Playlist()
+#playpos = random.randint (1, totalitems / (1 if 'asap' not in form else 3))
+
+class PlayerControls:
+   def __init__ (self, name='controls'):
+      self.name = name
+
+   def info (self):
+      doc, tag, text = Doc().tagtext()
+      _playtime = xbmc.Player.GetProperties (playerid=0, properties = ['position', 'percentage', 'time', 'totaltime'])
+      pt = _playtime['result'] if 'result' in _playtime else None
+      with tag ('ul', klass = 'horiz-menu'):
+         for infotext in ['Volume {}%'.format(xbmc.Application.GetProperties (properties=['volume'])['result']['volume']),
+                          'Time {:02d}:{:02d} / {:02d}:{:02d} ({:.2f}%) @ {:%H:%M:%S}'.format (pt['time']['hours'] * 60 +  pt['time']['minutes'], pt['time']['seconds'], pt['totaltime']['hours'] * 60 + pt['totaltime']['minutes'], pt['totaltime']['seconds'], pt['percentage'], datetime.now())]:
+            with tag ('li'):
+               text (infotext)
+
+      return doc.getvalue ()
+
+
+
+controls = PlayerControls ()
+print (controls.info ())
+
+print ('<a name="controls"></a>')
+print ('''
+<form method="post" action="{}" style="display: inline-block">
+<button name="volchange" value="5" type="submit">+5</button>
+<button name="volchange" value="-5" type="submit">-5</button>
+
+<button name="volchange" value="10" type="submit">+10</button>
+<button name="volchange" value="-10" type="submit">-10</button>
+
+<button name="volmute" value="1">Toggle Mute</button>
+
+</form>
+'''.format (cgi.escape (PAGE_SELF)))
+
+print ('''
+<form method="post" action="{}" style="display: inline-block">
+<button name="navigate" value="prev" type="submit">&#x23ee;</button>
+<button name="navigate" value="next" type="submit">&#x23ed;</button>
+<button name="navigate" value="playpause" type="submit">&#x23ef;</button>
+</form>
+'''.format (cgi.escape (PAGE_SELF)))
+
+print ('<a name="playlist"></a><h1>Playlist</h1>')
+print (playlist.show ())
+
+
+
+
+print ('<a name="search"></a>')
+Search ().show_quick_search (thereal=True)
+show_menu ()
+
+print ('<form method="post" action="{}" style="display: inline-block">'.format (cgi.escape (PAGE_SELF)))
+print ('<button name="partyon" value="true">re-enable party</button>')
+#print ('<button name="lockon" value="true">lock em out</button>')
+print ('<button name="lights" value="on">lights on</button>')
+print ('<button name="lights" value="off">lights off</button>')
+print ('</form>')
+print ('</body></html>')
diff --git a/party-upload/normals.cgi b/party-upload/normals.cgi
new file mode 100755 (executable)
index 0000000..40de5b8
--- /dev/null
@@ -0,0 +1,68 @@
+#!/usr/bin/python
+
+# Simple PartyMode Web Console
+# Limited interface for normal party goers
+# Copyright (c) 2015,2016  Clinton Ebadi <clinton@unknownlamer.org>
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# limit playlist view to next five
+
+import sys
+reload(sys)
+sys.setdefaultencoding ('utf-8')
+
+import cgi, cgitb
+from datetime import datetime
+import hashlib
+import numbers
+import os
+import random
+import subprocess
+from xbmcjson import XBMC
+from yattag import Doc
+
+import partyparty
+from partyparty import Song, SongControls, Search, Playlist, PartyManager
+
+cgitb.enable()
+
+print ("content-type: text/html; charset=utf-8\n\n")
+sys.stdout.flush ()
+print ("<!DOCTYPE html>\n<html><head><title>partyparty beb</title></head><body>")
+print (partyparty.css ())
+
+print '<p style="font-size: 5rem"><a href="upload.html">Upload A Song</a> | <a href="normals.cgi">home</a> | <a href="normals.cgi?browseartists=1">browse</a></p>'
+
+
+
+class NormalsPlaylist (Playlist):
+    def get_playlist (self):
+        return Playlist.get_playlist (self)[0:6]
+
+class NormalsManager (PartyManager):
+    DEFAULT_QUEUE_DIVISOR = 1    
+
+xbmc = partyparty.connect (XBMC ("http://localhost:8080/jsonrpc"))
+
+manager = NormalsManager (cgi.FieldStorage ())
+manager.process ()
+playlist = NormalsPlaylist ()
+
+print ('<a name="search"></a>')
+Search ().show_quick_search (thereal=True)
+
+print ('<a name="playlist"></a><h1>What\'s Playing</h1>')
+print (playlist.show (controls = []))
+
+print ('</body></html>')
diff --git a/party-upload/partyparty.py b/party-upload/partyparty.py
new file mode 100644 (file)
index 0000000..4d1041c
--- /dev/null
@@ -0,0 +1,344 @@
+# Kodi PartyParty Web Thing Library
+# Part of Simple PartyMode Web Console
+# Copyright (c) 2015 Clinton Ebadi <clinton@unknownlamer.org>
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+# Copyright (c) 2015 Clinton Ebadi <clinton@unknownlamer.org>
+
+import cgi, cgitb
+from datetime import datetime
+import hashlib
+import numbers
+import os
+import random
+import subprocess
+from xbmcjson import XBMC
+import urllib
+from yattag import Doc
+
+xbmc = None
+
+def connect (_xbmc):
+    global xbmc
+    xbmc = _xbmc
+    return xbmc
+
+SONG_PROPERTIES = ['album', 'artist', 'albumartist', 'dateadded', 'rating', 'displayartist']
+PAGE_SELF = os.environ['SCRIPT_NAME'] if 'SCRIPT_NAME' in os.environ else ''
+
+class Song:
+   def __init__ (self, song):
+      self._song = song
+      if 'artist' in song and len(song['artist']) > 0:
+         self.artist = song['artist'][0]
+      if 'displayartist' in song:
+          self.artist = song['displayartist']
+      elif 'albumartist' in song and len(song['albumartist']) > 0:
+         self.artist = song['albumartist'][0]
+      else:
+         self.artist = 'who fucking knows'
+
+      if 'album' in song and 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 get_playlist (playlistid=0):
+   return songs (xbmc.Playlist.GetItems (playlistid=playlistid, properties=SONG_PROPERTIES)['result']['items'])
+
+class SongControls:
+   aactions = {'songup': 'up', 'songdown': 'down', 'songtop': 'next!', 'songbottom': 'banish!',
+               '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 = PAGE_SELF, 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):
+      doc, tag, text = Doc(defaults = {'searchfield': self.prop}).tagtext()
+      with tag ('form', method = 'get', action = PAGE_SELF, style = 'display: inline-block'):
+          if thereal:
+              doc.stag ('input', type = 'text', name = 'searchterm', value = self.term, id = 'quicksearch')
+          else:
+              doc.stag ('input', type = 'text', name = 'searchterm', value = self.term)
+          with doc.select (name = 'searchfield'):
+              for prop in ['title', 'artist', 'album']:
+                  with doc.option (value = prop):
+                      text (prop)
+          with tag ('button', type = 'submit', name = 'searchgo', value = '1'):
+              text ('Search')
+      print doc.getvalue ()
+
+   def show_search_results (self):
+      doc, tag, text = Doc().tagtext()
+      with tag ('h1'):
+         text ('Results')
+      if len (self.results) > 0:
+          doc.asis (Playlist (self.results).show (['randomqueue']))
+      else:
+         with tag ('p'):
+            text ('You are unworthy. No results.')
+
+      print (doc.getvalue ())
+
+class Playlist:
+   default_controls = ['songup', 'songdown', 'songdel', 'songrate', 'songtop', 'songbottom']
+   def __init__ (self, kodi_playlist = None):
+      if kodi_playlist is None:
+         self.playlist = self.get_playlist ()
+      elif (all (isinstance (s, Song) for s in kodi_playlist)):
+         self.playlist = kodi_playlist
+      else:
+          self.playlist = songs (kodi_playlist)
+
+   def show (self, controls = default_controls):
+      doc, tag, text = Doc().tagtext()
+      with tag ('ol', klass = 'flex_list'):
+         for song in self.playlist:
+#            text ("{}".format (song._song))
+            with tag ('li'):
+               with tag ('div', klass = 'flex_row'):
+                  with tag ('p'):
+                     with tag ('a', href = '{1}?searchgo=1&amp;searchterm={0};searchfield=artist'.format(song.artist, PAGE_SELF)):
+                        text (song.artist)
+                     text (' ({}) {}'.format(song.album, song.label))
+                  doc.asis (SongControls (song, controls).controls())
+      return doc.getvalue ()
+
+   def get_playlist (self, playlistid=0):
+      return songs (xbmc.Playlist.GetItems (playlistid=playlistid, properties=SONG_PROPERTIES)['result']['items'])
+
+class Upload:
+    upload_dir = '/srv/archive/incoming/stolen-moosic'
+
+    def __init__ (self, form, field):
+        self.fileitem = form[field]
+        self.filename = '{}/{}'.format (self.upload_dir, self.fileitem.filename)
+
+    # Evil: just run replaygain/mp3gain/metaflac on the file and hope one
+    # works instead of dealing with MIME. For now.
+    def attempt_rpgain (self):
+        subprocess.call (["/usr/bin/vorbisgain", "-q", self.filename])
+        subprocess.call (["/usr/bin/mp3gain", "-q", "-s", "i", self.filename])
+        subprocess.call (["/usr/bin/aacgain", "-q", "-s", "i", self.filename])
+        subprocess.call (["/usr/bin/metaflac", "--add-replay-gain", self.filename])
+
+    def save (self):
+        fout = file (os.path.join(self.upload_dir, self.fileitem.filename), 'wb')
+        fout.write (self.fileitem.value)
+        fout.close()
+        self.attempt_rpgain ()
+        return self.filename
+
+
+def css ():
+    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; font-size: 1.75rem; }
+body {  /* background-image: url("fire-under-construction-animation.gif");  */
+  color: white;
+  background-color: black;
+  font-family: sans-serif;
+}
+a { color: #5dfc0a}
+button[name=songdel] { margin-left: 1em; margin-right: 1em; }
+.flex_row {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.flex_row p { font-size: 175%; max-width: 50%; min-width: 50% }
+
+ol li:nth-child(even) { background-color: #202020 }
+''')
+    return doc.getvalue ()
+
+def print_escaped (item):
+   print (u"<p>{}</p>".format (cgi.escape (u"{}".format (item))))
+
+# This is awful
+class PartyManager:
+    DEFAULT_QUEUE_DIVISOR = 3
+
+    def __init__ (self, form):
+        self.form = form
+
+    def randomqueue (self, item, divisor=None):
+        # randomly queue song somewhere in first (playlist.length /
+        # divisor) songs
+        if divisor is None:
+            divisor = self.DEFAULT_QUEUE_DIVISOR
+        totalitems = xbmc.Playlist.GetItems (playlistid=0)['result']['limits']['total']
+        playpos = random.randint (1, totalitems / divisor + 1)
+        print_escaped (xbmc.Playlist.Insert (playlistid=0, item=item, 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)
+        return (playpos, totalitems+1)
+
+    def process (self):
+        form = self.form
+        if 'songdel' in form:
+            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 = 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 = 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 'songtop' in form:
+            songid = form['songtop'].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>Bumped Up {}</p>".format(cgi.escape(song.label)))
+            for i in range (pos, 1, -1):
+                print_escaped (xbmc.Playlist.Swap (playlistid=0, position1=i, position2=i-1))
+        elif 'songbottom' in form:
+            songid = form['songbottom'].value
+            print (u"<p>{}</p>".format (songid))
+            playlist = get_playlist ()
+            (pos,song) = next ((i,s) for i,s in enumerate(playlist) if s.key == songid)
+            print (u"<p>Banished {}</p>".format(cgi.escape(song.label)))
+            for i in range (pos, len (playlist), 1):
+                print_escaped (xbmc.Playlist.Swap (playlistid=0, position1=i, position2=i+1))
+        elif 'volchange' in form:
+            curvolume = xbmc.Application.GetProperties (properties=['volume'])['result']['volume']
+            newvolume = max (0, min (int (form['volchange'].value) + curvolume, 100))
+            print_escaped (xbmc.Application.SetVolume (volume=newvolume))
+        elif 'volmute' in form:
+            print_escaped (xbmc.Application.SetMute (mute="toggle"))
+        elif 'navigate' in form:
+            action = form['navigate'].value
+            if action == 'prev':
+                print_escaped (xbmc.Player.GoTo (to="previous", playerid=0))
+            elif action == 'next':
+                print_escaped (xbmc.Player.GoTo (to="next", playerid=0))
+            elif action == 'playpause':
+                print_escaped (xbmc.Player.PlayPause (play="toggle",  playerid=0))
+        elif 'searchgo' in form:
+            term = form['searchterm'].value
+            field = form['searchfield'].value
+            search = Search (term, field)
+            search.show_quick_search ()
+            search.show_search_results ()
+        elif 'randomqueue' in form:
+            songid = int(form['songkodiid'].value)
+            self.randomqueue ({"songid": songid})
+        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 'browseartists' in form:
+            artists = xbmc.AudioLibrary.GetArtists (sort={'order': 'ascending', 'method': 'artist'})['result']['artists']
+            doc, tag, text = Doc().tagtext()
+            with tag ('ol', klass='flex_list'):
+                for artist in artists:
+                    with tag ('li', style='padding: 1rem; font-size: x-large'):
+                        with tag ('a', href='{}?searchgo=1&searchterm={}&searchfield=artist'.format (PAGE_SELF, urllib.quote_plus (artist['artist'].encode ('utf-8')).decode ('utf-8'))):
+                            text (artist['label'])
+            print (doc.getvalue ())
+        elif 'uploadgo' in form:
+            upload = Upload (form, 'song')
+            filename = upload.save ()
+            self.randomqueue ({"file": filename}, 1 if 'asap' not in form else 3)
+        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'])
+        elif 'lights' in form:
+            subprocess.call (['/usr/bin/br', '-N' if form['lights'].value == 'on' else '-F'])
index 8ac3918..45a109a 100755 (executable)
@@ -1,7 +1,6 @@
 #!/usr/bin/python
-
-# Simple PartyMode Upload Script
-# Copyright (c) 2015 Clinton Ebadi <clinton@unknownlamer.org>
+# Simple PartyMode Web Console
+# Copyright (c) 2015,2016 Clinton Ebadi <clinton@unknownlamer.org>
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 # the Free Software Foundation, either version 3 of the License, or
 
 # todo:
 # fix weird characters (see erica's song)
+# use shutil.copyfileobj to possibly speed up copying
+# - check if it actually causes chunked reads from the browser
+# - LOL WHAT uploads are like 10s don't bother
+# support multiple files
+# - daemonize replaygain? NOPE it runs in like no time thanks to moar power
 # track uploads (overcomplicated version)
 # - phone ip is hopefully good enough
 # - track # of uses of asap/Next Please
 # - e.g. no one in last PERIOD = go next, fuckton of people = asap just ain't working
 
 
-# integrate better with native queue (if it existed...)
+# integrate better with native queue
+
+
+## crazy idea: if the playlist can be watched with a script... (maybe
+## a json rpc notification? but ... reliability, and upload.cgi runs
+## infrequently enough that it cannot record the playlist and extract
+## a meaningful image of the queue ...).
+
+# diff playlist whenever item is inserted (or song changes, or whatever we can hook on).
+# scan for common items (discarding the head of the old snapshot, since we could just be advancing)
+# when the first different item is found... if it ain't the end of the list it's been queued
+# keep going until we match up again and record the end of the queue position in a file
+# what about when we delete in the list?
+#
 
 # upload.html
 # - show playlist length, # of next/asap available, likely queue position
@@ -42,48 +59,29 @@ import subprocess
 import random
 from xbmcjson import XBMC
 
+import partyparty
+from partyparty import Upload, PartyManager
+
 cgitb.enable()
 
 print "content-type: text/html\n\n"
+sys.stdout.flush ()
 
-music_dir = "/srv/archive/incoming/stolen-moosic"
 form = cgi.FieldStorage ()
 
-# Evil: just run replaygain/mp3gain/metaflac on the file and hope one
-# works instead of dealing with MIME. For now.
-def attempt_rpgain (file_name): 
-   subprocess.call (["/usr/bin/vorbisgain", "-q", file_name])
-   subprocess.call (["/usr/bin/mp3gain", "-q", "-s", "i", file_name])
-   subprocess.call (["/usr/bin/aacgain", "-q", "-s", "i", file_name])
-   subprocess.call (["/usr/bin/metaflac", "--add-replay-gain", file_name])
-
-def save_uploaded_file (form_field, upload_dir):
-    if form_field not in form:
-        print "<p>No file provided.</p>"
-        sys.exit ()
-    fileitem = form[form_field]
-    fout = file (os.path.join(upload_dir, fileitem.filename), 'wb')
-    fout.write (fileitem.value)
-    fout.close()
-    return fileitem.filename
-
-song_name = save_uploaded_file ("song", music_dir)
+manager = PartyManager (form)
 
 print '<p><a href="upload.html">Upload another song</a></p>'
 sys.stdout.flush ()
 
-#with daemon.DaemonContext ():
-attempt_rpgain ("{0}/{1}".format (music_dir, song_name))
-
-xbmc = XBMC ("http://localhost:8080/jsonrpc")
+xbmc = partyparty.connect (XBMC ("http://localhost:8080/jsonrpc"))
+manager.randomqueue ({"file": filename}, 1 if 'asap' not in form else 3)
 
 # todo: use REMOTE_ADDR to limit how many asap requests a person can
 # make in a time period
-totalitems = xbmc.Playlist.GetItems (playlistid=0)['result']['limits']['total']
-playpos = random.randint (1, totalitems / (1 if 'asap' not in form else 3))
-
-print xbmc.Playlist.Insert (playlistid=0, item={"file": "{0}/{1}".format (music_dir, song_name)}, 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)
+#totalitems = xbmc.Playlist.GetItems (playlistid=0)['result']['limits']['total']
+#playpos = random.randint (1, totalitems / (1 if 'asap' not in form else 3))
 
+#print xbmc.Playlist.Insert (playlistid=0, item={"file": filename }, 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)
index 787a17e..f70ecb2 100644 (file)
     </style>
   </head>
   <body>
-    <form action="upload.cgi" method="post" enctype="multipart/form-data">
+    <form action="normals.cgi" method="post" enctype="multipart/form-data">
       <p>
-       <input name="song" type="file" accept="audio/*" required="required" />
+       <input name="song" type="file" accept="audio/*"
+         required="required" />
        <!-- <input type="submit" value="Party On!" /> -->
-       <button name="go" type="submit">Party On!</button>
+       <button name="uploadgo" type="submit">Party On!</button>
        <input type="checkbox" value="faster" name="asap"  />
        <label for="asap">nnooowww</label>
       </p>