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