6ac2fd3d3ee62c24596282c510161bc038ae8d79
[clinton/unknownlamer-kodi-addons.git] / party-upload / partyparty.py
1 # Kodi PartyParty Web Thing Library
2 # Part of Simple PartyMode Web Console
3 # Copyright (c) 2015 Clinton Ebadi <clinton@unknownlamer.org>
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 # Copyright (c) 2015 Clinton Ebadi <clinton@unknownlamer.org>
17
18 import cgi, cgitb
19 from datetime import datetime
20 import hashlib
21 import itertools
22 import numbers
23 import os
24 import random
25 import re
26 import subprocess
27 import urllib
28 from xbmcjson import XBMC
29 from yattag import Doc
30 import youtube_dl
31
32 xbmc = None
33
34 def connect (_xbmc):
35 global xbmc
36 xbmc = _xbmc
37 return xbmc
38
39 SONG_PROPERTIES = ['album', 'artist', 'albumartist', 'dateadded', 'userrating', 'displayartist']
40 PAGE_SELF = os.environ['SCRIPT_NAME'] if 'SCRIPT_NAME' in os.environ else ''
41
42 class Song:
43 def __init__ (self, song):
44 self._song = song
45 if 'artist' in song and len(song['artist']) > 0:
46 self.artist = song['artist'][0]
47 if 'displayartist' in song:
48 self.artist = song['displayartist']
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 'album' in song and 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 'userrating' in song:
66 libsong = xbmc.AudioLibrary.GetSongDetails (songid = song['id'], properties = ['userrating'])
67 #print (libsong)
68 if 'result' in libsong and 'songdetails' in libsong['result']:
69 song['userrating'] = libsong['result']['songdetails']['userrating']
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 'userrating' in song and isinstance (song['userrating'], numbers.Integral):
81 self.rating = song['userrating']
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', 'songtop': 'next!', 'songbottom': 'banish!',
96 'songdel': 'del', 'randomqueue': 'yeh', 'songrate': 'rate'}
97
98 def __init__ (self, song, actions = ['songup', 'songdown', 'songdel']):
99 self.song = song
100 self.actions = actions
101
102 def controls (self):
103 doc, tag, text = Doc().tagtext()
104 with tag ('form', method = 'post', action = PAGE_SELF, klass = 'song_controls'): #, style = 'display: inline-block'):
105 for action in self.actions:
106 with tag ('button', name = action, value = self.song.key):
107 text (self.aactions[action])
108 doc.asis (self.extra_elements (action))
109 return doc.getvalue()
110
111 def extra_elements (self, action):
112 doc, tag, text = Doc().tagtext()
113 if action == 'randomqueue':
114 doc.stag ('input', type = 'hidden', name = 'songkodiid', value = self.song.kodi_id)
115 elif action == 'songrate':
116 if self.song.rating > -1:
117 doc.defaults = {'songrating': self.song.rating}
118 with doc.select (name = 'songrating'):
119 with doc.option (value = 0):
120 text ('na')
121 for i in range (1,6):
122 with doc.option (value = i*2):
123 text (str (i))
124 doc.stag ('input', type = 'hidden', name = 'songkodiid', value = self.song.kodi_id)
125
126 return doc.getvalue ()
127
128 class Search:
129 ANY_SEARCH_PROPERTIES = [ 'title', 'album', 'artist' ]
130
131 def __init__ (self, term = '', prop = 'title'):
132 self.term = term
133 self.prop = prop
134 if (term != ''):
135 self.results = self.get_search_results ()
136
137 def get_search_results (self):
138 if (self.term == ''):
139 return {}
140
141 if (self.prop != 'any'):
142 res = xbmc.AudioLibrary.GetSongs (filter={'operator': "contains", 'field': self.prop, 'value': self.term}, properties=SONG_PROPERTIES, sort={'order': 'ascending', 'method': 'artist'})['result']
143 if 'songs' in res:
144 return songs(res['songs'])
145 else:
146 return []
147 else:
148 all_songs = [xbmc.AudioLibrary.GetSongs (filter={'operator': "contains", 'field': p, 'value': self.term}, properties=SONG_PROPERTIES, sort={'order': 'ascending', 'method': 'artist'})['result']
149 for p
150 in self.ANY_SEARCH_PROPERTIES]
151 # does not remove duplicates...
152 return list (itertools.chain.from_iterable ([res['songs'] for res in all_songs if 'songs' in res]))
153
154 def show_quick_search (self, thereal=False):
155 doc, tag, text = Doc(defaults = {'searchfield': self.prop}).tagtext()
156 with tag ('form', method = 'get', action = PAGE_SELF, style = 'display: inline-block'):
157 if thereal:
158 doc.stag ('input', type = 'text', name = 'searchterm', value = self.term, id = 'quicksearch')
159 else:
160 doc.stag ('input', type = 'text', name = 'searchterm', value = self.term)
161 with doc.select (name = 'searchfield'):
162 for prop in ['title', 'artist', 'album', 'any']:
163 with doc.option (value = prop):
164 text (prop)
165 with tag ('button', type = 'submit', name = 'searchgo', value = '1'):
166 text ('Search')
167 print doc.getvalue ()
168
169 def show_search_results (self):
170 doc, tag, text = Doc().tagtext()
171 with tag ('h1'):
172 text ('Results')
173 if len (self.results) > 0:
174 doc.asis (Playlist (self.results).show (['randomqueue']))
175 else:
176 with tag ('p'):
177 text ('You are unworthy. No results.')
178
179 print (doc.getvalue ())
180
181 class Playlist:
182 default_controls = ['songup', 'songdown', 'songdel', 'songrate', 'songtop', 'songbottom']
183 def __init__ (self, kodi_playlist = None):
184 if kodi_playlist is None:
185 self.playlist = self.get_playlist ()
186 elif (all (isinstance (s, Song) for s in kodi_playlist)):
187 self.playlist = kodi_playlist
188 else:
189 self.playlist = songs (kodi_playlist)
190
191 def show (self, controls = default_controls):
192 doc, tag, text = Doc().tagtext()
193 with tag ('ol', klass = 'flex_list'):
194 for song in self.playlist:
195 # text ("{}".format (song._song))
196 with tag ('li'):
197 with tag ('div', klass = 'flex_row'):
198 with tag ('p'):
199 with tag ('a', href = '{1}?searchgo=1&amp;searchterm={0};searchfield=artist'.format(song.artist, PAGE_SELF)):
200 text (song.artist)
201 text (' ({}) {}'.format(song.album, song.label))
202 doc.asis (SongControls (song, controls).controls())
203 return doc.getvalue ()
204
205 def get_playlist (self, playlistid=0):
206 return songs (xbmc.Playlist.GetItems (playlistid=playlistid, properties=SONG_PROPERTIES)['result']['items'])
207
208 class Upload:
209 upload_dir = '/srv/archive/incoming/stolen-moosic'
210
211 def __init__ (self, form, field):
212 self.fileitem = form[field]
213 self.filename = '{}/{}'.format (self.upload_dir, self.fileitem.filename)
214
215 # Evil: just run replaygain/mp3gain/metaflac on the file and hope one
216 # works instead of dealing with MIME. For now.
217 def attempt_rpgain (self):
218 subprocess.call (["/usr/bin/vorbisgain", "-q", self.filename])
219 subprocess.call (["/usr/bin/mp3gain", "-q", "-s", "i", self.filename])
220 subprocess.call (["/usr/bin/aacgain", "-q", "-s", "i", self.filename])
221 subprocess.call (["/usr/bin/metaflac", "--add-replay-gain", self.filename])
222
223 def save (self):
224 fout = file (os.path.join(self.upload_dir, self.fileitem.filename), 'wb')
225 fout.write (self.fileitem.value)
226 fout.close()
227 self.attempt_rpgain ()
228 return { 'file': self.filename }
229
230 class Youtube:
231 upload_dir = '/srv/archive/incoming/youtube-moosic'
232 ydl_opts = {
233 'format': 'bestaudio/best',
234 'outtmpl': upload_dir + '/%(title)s-%(id)s.%(ext)s',
235 'quiet': True,
236 'postprocessors': [
237 {
238 'key': 'FFmpegMetadata',
239 },
240 {
241 'key': 'FFmpegExtractAudio',
242 'preferredcodec': 'vorbis',
243 }],
244
245 }
246
247 def __init__ (self, form, field):
248 self.ydl = youtube_dl.YoutubeDL(self.ydl_opts)
249 self.url = form.getvalue (field)
250
251 def save (self):
252 info = self.ydl.extract_info (self.url, download=True)
253 filename = re.sub ('\..{3,4}$', '.ogg', self.ydl.prepare_filename (info))
254 subprocess.call (["/usr/bin/vorbisgain", "-q", filename])
255 return { 'file': filename }
256
257
258 def css ():
259 doc, tag, text = Doc ().tagtext ()
260 with tag ('style'):
261 text ('''
262 input, select, button { font-size: 200%; margin: 0.1em; }
263 .horiz-menu li { display: inline; padding-right: 0.5em; font-size: 1.75rem; }
264 body { /* background-image: url("fire-under-construction-animation.gif"); */
265 color: white;
266 background-color: black;
267 font-family: sans-serif;
268 }
269 a { color: #5dfc0a}
270 button[name=songdel] { margin-left: 1em; margin-right: 1em; }
271 .flex_row {
272 display: flex;
273 flex-flow: row nowrap;
274 justify-content: space-between;
275 align-items: center;
276 }
277
278 .flex_row p { font-size: 175%; max-width: 50%; min-width: 50% }
279
280 ol li:nth-child(even) { background-color: #202020 }
281 ''')
282 return doc.getvalue ()
283
284 def print_escaped (item):
285 print (u"<p>{}</p>".format (cgi.escape (u"{}".format (item))))
286
287 # This is awful
288 class PartyManager:
289 DEFAULT_QUEUE_DIVISOR = 3
290
291 def __init__ (self, form):
292 self.form = form
293
294 def randomqueue (self, item, divisor=None):
295 # randomly queue song somewhere in first (playlist.length /
296 # divisor) songs
297 if divisor is None:
298 divisor = self.DEFAULT_QUEUE_DIVISOR
299 totalitems = xbmc.Playlist.GetItems (playlistid=0)['result']['limits']['total']
300 playpos = random.randint (1, totalitems / divisor + 1)
301 print_escaped (xbmc.Playlist.Insert (playlistid=0, item=item, position=playpos))
302 print '<p style="font-size: x-large">Your song is number {0} in the queue ({1} songs in playlist).</p>'.format (playpos, totalitems+1)
303 return (playpos, totalitems+1)
304
305 def process (self):
306 form = self.form
307 if 'songdel' in form:
308 songid = form['songdel'].value
309 print (u"<p>{}</p>".format (songid))
310 (pos,song) = next ((i,s) for i,s in enumerate(get_playlist ()) if s.key == songid)
311 print (u'<p>Deleted {}</p>'.format(cgi.escape (song.label)))
312 print_escaped (xbmc.Playlist.Remove (playlistid=0, position=pos))
313 elif 'songup' in form:
314 songid = form['songup'].value
315 print (u"<p>{}</p>".format (songid))
316 (pos,song) = next ((i,s) for i,s in enumerate(get_playlist ()) if s.key == songid)
317 print (u"<p>Promoted {}</p>".format(cgi.escape(song.label)))
318 print_escaped (xbmc.Playlist.Swap (playlistid=0, position1=pos, position2=pos-1))
319 elif 'songdown' in form:
320 songid = form['songdown'].value
321 print (u"<p>{}</p>".format (songid))
322 (pos,song) = next ((i,s) for i,s in enumerate(get_playlist ()) if s.key == songid)
323 print (u"<p>Demoted {}</p>".format(cgi.escape(song.label)))
324 print_escaped (xbmc.Playlist.Swap (playlistid=0, position1=pos, position2=pos+1))
325 elif 'songtop' in form:
326 songid = form['songtop'].value
327 print (u"<p>{}</p>".format (songid))
328 (pos,song) = next ((i,s) for i,s in enumerate(get_playlist ()) if s.key == songid)
329 print (u"<p>Bumped Up {}</p>".format(cgi.escape(song.label)))
330 for i in range (pos, 1, -1):
331 print_escaped (xbmc.Playlist.Swap (playlistid=0, position1=i, position2=i-1))
332 elif 'songbottom' in form:
333 songid = form['songbottom'].value
334 print (u"<p>{}</p>".format (songid))
335 playlist = get_playlist ()
336 (pos,song) = next ((i,s) for i,s in enumerate(playlist) if s.key == songid)
337 print (u"<p>Banished {}</p>".format(cgi.escape(song.label)))
338 for i in range (pos, len (playlist), 1):
339 print_escaped (xbmc.Playlist.Swap (playlistid=0, position1=i, position2=i+1))
340 elif 'volchange' in form:
341 curvolume = xbmc.Application.GetProperties (properties=['volume'])['result']['volume']
342 newvolume = max (0, min (int (form['volchange'].value) + curvolume, 100))
343 print_escaped (xbmc.Application.SetVolume (volume=newvolume))
344 elif 'volmute' in form:
345 print_escaped (xbmc.Application.SetMute (mute="toggle"))
346 elif 'navigate' in form:
347 action = form['navigate'].value
348 if action == 'prev':
349 print_escaped (xbmc.Player.GoTo (to="previous", playerid=0))
350 elif action == 'next':
351 print_escaped (xbmc.Player.GoTo (to="next", playerid=0))
352 elif action == 'playpause':
353 print_escaped (xbmc.Player.PlayPause (play="toggle", playerid=0))
354 elif 'searchgo' in form:
355 term = form['searchterm'].value
356 field = form['searchfield'].value
357 search = Search (term, field)
358 search.show_quick_search ()
359 search.show_search_results ()
360 elif 'randomqueue' in form:
361 songid = int(form['songkodiid'].value)
362 self.randomqueue ({"songid": songid})
363 elif 'songrate' in form:
364 songid = int(form['songkodiid'].value)
365 newrating = int(form['songrating'].value)
366 print (songid)
367 print (newrating)
368 print_escaped (xbmc.AudioLibrary.SetSongDetails (songid = songid, userrating = newrating))
369 print_escaped (u'Rating Changed')
370 elif 'browseartists' in form:
371 artists = xbmc.AudioLibrary.GetArtists (sort={'order': 'ascending', 'method': 'artist'})['result']['artists']
372 doc, tag, text = Doc().tagtext()
373 with tag ('ol', klass='flex_list'):
374 for artist in artists:
375 with tag ('li', style='padding: 1rem; font-size: x-large'):
376 with tag ('a', href='{}?searchgo=1&searchterm={}&searchfield=artist'.format (PAGE_SELF, urllib.quote_plus (artist['artist'].encode ('utf-8')).decode ('utf-8'))):
377 text (artist['label'])
378 print (doc.getvalue ())
379 elif 'uploadgo' in form:
380 upload = Upload (form, 'song')
381 item = upload.save ()
382 self.randomqueue (item, 1 if 'asap' not in form else 3)
383 elif 'youtubego' in form:
384 youtube = Youtube (form, 'youtubeurl')
385 item = youtube.save ()
386 self.randomqueue (item, 1 if 'asap' not in form else 3)
387 elif 'partyon' in form:
388 if 'error' in xbmc.Player.SetPartymode (partymode=True, playerid=0):
389 xbmc.Player.Open (item={"partymode": "music"})
390 elif 'lockon' in form:
391 subprocess.call (['/usr/bin/xscreensaver-command', 'lock'])
392 elif 'lights' in form:
393 subprocess.call (['/usr/bin/br', '-N' if form['lights'].value == 'on' else '-F'])