party-upload: port to python 3
[clinton/unknownlamer-kodi-addons.git] / party-upload / partyparty.py
CommitLineData
51a600c2
CE
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
18import cgi, cgitb
19from datetime import datetime
20import hashlib
b61367d1 21import html
cfa37db5 22import itertools
51a600c2
CE
23import numbers
24import os
25import random
cfa37db5 26import re
51a600c2 27import subprocess
51a600c2 28import urllib
b61367d1 29from kodijson import Kodi
51a600c2 30from yattag import Doc
b1ddeadc 31import youtube_dl
51a600c2 32
b61367d1 33kodi = None
51a600c2 34
b61367d1
CE
35def connect (_kodi):
36 global kodi
37 kodi = _kodi
38 return kodi
51a600c2 39
4da3fd24 40SONG_PROPERTIES = ['album', 'artist', 'albumartist', 'dateadded', 'userrating', 'displayartist']
51a600c2
CE
41PAGE_SELF = os.environ['SCRIPT_NAME'] if 'SCRIPT_NAME' in os.environ else ''
42
43class 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
b61367d1 62 self.key = hashlib.sha256(str(song['id']).encode('utf-8')).hexdigest()
51a600c2
CE
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.
4da3fd24 66 if 'userrating' in song:
b61367d1 67 libsong = kodi.AudioLibrary.GetSongDetails (songid = song['id'], properties = ['userrating'])
51a600c2
CE
68 #print (libsong)
69 if 'result' in libsong and 'songdetails' in libsong['result']:
4da3fd24 70 song['userrating'] = libsong['result']['songdetails']['userrating']
51a600c2
CE
71 elif 'songid' in song:
72 # search results
b61367d1 73 self.key = hashlib.sha256(str(song['songid']).encode('utf-8')).hexdigest()
51a600c2
CE
74 self.kodi_id = song['songid']
75 else:
b61367d1 76 self.key = hashlib.sha256((song['label'] + self.artist).encode('utf-8')).hexdigest()
51a600c2
CE
77 self.kodi_id = 0
78
79 # videos can still be labeled as songs, but the rating will be a
80 # float...
4da3fd24
CE
81 if 'userrating' in song and isinstance (song['userrating'], numbers.Integral):
82 self.rating = song['userrating']
51a600c2
CE
83 else:
84 self.rating = -1 # might be better to use None here
85
86 self.label = song['label']
87
88def songs(items):
89 '''Convert list of Kodi Items into Song instances'''
90 return [Song(item) for item in items]
91
92def get_playlist (playlistid=0):
b61367d1 93 return songs (kodi.Playlist.GetItems (playlistid=playlistid, properties=SONG_PROPERTIES)['result']['items'])
51a600c2
CE
94
95class 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):
4da3fd24 123 with doc.option (value = i*2):
51a600c2
CE
124 text (str (i))
125 doc.stag ('input', type = 'hidden', name = 'songkodiid', value = self.song.kodi_id)
126
127 return doc.getvalue ()
128
129class Search:
cfa37db5
CE
130 ANY_SEARCH_PROPERTIES = [ 'title', 'album', 'artist' ]
131
51a600c2
CE
132 def __init__ (self, term = '', prop = 'title'):
133 self.term = term
134 self.prop = prop
135 if (term != ''):
cfa37db5
CE
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'):
b61367d1 143 res = kodi.AudioLibrary.GetSongs (filter={'operator': "contains", 'field': self.prop, 'value': self.term}, properties=SONG_PROPERTIES, sort={'order': 'ascending', 'method': 'artist'})['result']
cfa37db5
CE
144 if 'songs' in res:
145 return songs(res['songs'])
146 else:
147 return []
148 else:
b61367d1 149 all_songs = [kodi.AudioLibrary.GetSongs (filter={'operator': "contains", 'field': p, 'value': self.term}, properties=SONG_PROPERTIES, sort={'order': 'ascending', 'method': 'artist'})['result']
cfa37db5
CE
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]))
51a600c2
CE
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'):
cfa37db5 163 for prop in ['title', 'artist', 'album', 'any']:
51a600c2
CE
164 with doc.option (value = prop):
165 text (prop)
166 with tag ('button', type = 'submit', name = 'searchgo', value = '1'):
167 text ('Search')
b61367d1 168 print (doc.getvalue ())
51a600c2
CE
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
182class 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):
b61367d1 207 return songs (kodi.Playlist.GetItems (playlistid=playlistid, properties=SONG_PROPERTIES)['result']['items'])
51a600c2
CE
208
209class 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])
b61367d1 220 subprocess.call (["/usr/local/bin/mp3gain", "-q", "-s", "i", self.filename])
51a600c2
CE
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):
b61367d1 225 fout = open (os.path.join(self.upload_dir, self.fileitem.filename), 'wb')
51a600c2
CE
226 fout.write (self.fileitem.value)
227 fout.close()
228 self.attempt_rpgain ()
433e6204 229 return { 'file': self.filename }
51a600c2 230
b1ddeadc
CE
231class Youtube:
232 upload_dir = '/srv/archive/incoming/youtube-moosic'
233 ydl_opts = {
234 'format': 'bestaudio/best',
433e6204 235 'outtmpl': upload_dir + '/%(title)s-%(id)s.%(ext)s',
b1ddeadc
CE
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)
433e6204 254 filename = re.sub ('\..{3,4}$', '.ogg', self.ydl.prepare_filename (info))
b1ddeadc 255 subprocess.call (["/usr/bin/vorbisgain", "-q", filename])
433e6204 256 return { 'file': filename }
b1ddeadc 257
51a600c2
CE
258
259def css ():
260 doc, tag, text = Doc ().tagtext ()
261 with tag ('style'):
262 text ('''
263input, select, button { font-size: 200%; margin: 0.1em; }
264.horiz-menu li { display: inline; padding-right: 0.5em; font-size: 1.75rem; }
265body { /* background-image: url("fire-under-construction-animation.gif"); */
266 color: white;
267 background-color: black;
268 font-family: sans-serif;
269}
270a { color: #5dfc0a}
271button[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
281ol li:nth-child(even) { background-color: #202020 }
282''')
283 return doc.getvalue ()
284
285def print_escaped (item):
b61367d1 286 print (u"<p>{}</p>".format (html.escape (u"{}".format (item))))
51a600c2
CE
287
288# This is awful
289class 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
b61367d1 300 totalitems = kodi.Playlist.GetItems (playlistid=0)['result']['limits']['total']
51a600c2 301 playpos = random.randint (1, totalitems / divisor + 1)
b61367d1
CE
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))
51a600c2
CE
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)
b61367d1
CE
312 print (u'<p>Deleted {}</p>'.format(html.escape (song.label)))
313 print_escaped (kodi.Playlist.Remove (playlistid=0, position=pos))
51a600c2
CE
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)
b61367d1
CE
318 print (u"<p>Promoted {}</p>".format(html.escape(song.label)))
319 print_escaped (kodi.Playlist.Swap (playlistid=0, position1=pos, position2=pos-1))
51a600c2
CE
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)
b61367d1
CE
324 print (u"<p>Demoted {}</p>".format(html.escape(song.label)))
325 print_escaped (kodi.Playlist.Swap (playlistid=0, position1=pos, position2=pos+1))
51a600c2
CE
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)
b61367d1 330 print (u"<p>Bumped Up {}</p>".format(html.escape(song.label)))
51a600c2 331 for i in range (pos, 1, -1):
b61367d1 332 print_escaped (kodi.Playlist.Swap (playlistid=0, position1=i, position2=i-1))
51a600c2
CE
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)
b61367d1 338 print (u"<p>Banished {}</p>".format(html.escape(song.label)))
51a600c2 339 for i in range (pos, len (playlist), 1):
b61367d1 340 print_escaped (kodi.Playlist.Swap (playlistid=0, position1=i, position2=i+1))
51a600c2 341 elif 'volchange' in form:
b61367d1 342 curvolume = kodi.Application.GetProperties (properties=['volume'])['result']['volume']
51a600c2 343 newvolume = max (0, min (int (form['volchange'].value) + curvolume, 100))
b61367d1 344 print_escaped (kodi.Application.SetVolume (volume=newvolume))
51a600c2 345 elif 'volmute' in form:
b61367d1 346 print_escaped (kodi.Application.SetMute (mute="toggle"))
51a600c2
CE
347 elif 'navigate' in form:
348 action = form['navigate'].value
349 if action == 'prev':
b61367d1 350 print_escaped (kodi.Player.GoTo (to="previous", playerid=0))
51a600c2 351 elif action == 'next':
b61367d1 352 print_escaped (kodi.Player.GoTo (to="next", playerid=0))
51a600c2 353 elif action == 'playpause':
b61367d1 354 print_escaped (kodi.Player.PlayPause (play="toggle", playerid=0))
51a600c2
CE
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)
b61367d1 369 print_escaped (kodi.AudioLibrary.SetSongDetails (songid = songid, userrating = newrating))
51a600c2
CE
370 print_escaped (u'Rating Changed')
371 elif 'browseartists' in form:
b61367d1 372 artists = kodi.AudioLibrary.GetArtists (sort={'order': 'ascending', 'method': 'artist'})['result']['artists']
51a600c2
CE
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')
433e6204
CE
382 item = upload.save ()
383 self.randomqueue (item, 1 if 'asap' not in form else 3)
b1ddeadc
CE
384 elif 'youtubego' in form:
385 youtube = Youtube (form, 'youtubeurl')
433e6204
CE
386 item = youtube.save ()
387 self.randomqueue (item, 1 if 'asap' not in form else 3)
51a600c2 388 elif 'partyon' in form:
b61367d1
CE
389 if 'error' in kodi.Player.SetPartymode (partymode=True, playerid=0):
390 kodi.Player.Open (item={"partymode": "music"})
51a600c2
CE
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'])