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