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