98d8f9fd937a6924dbf8faaccd3c0ca9a8885d1c
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.
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.
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>
19 from datetime
import datetime
29 from kodijson
import Kodi
30 from yattag
import Doc
40 SONG_PROPERTIES
= ['album', 'artist', 'albumartist', 'dateadded', 'userrating', 'displayartist']
41 PAGE_SELF
= os
.environ
['SCRIPT_NAME'] if 'SCRIPT_NAME' in os
.environ
else ''
44 def __init__ (self
, 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]
53 self
.artist
= 'who fucking knows'
55 if 'album' in song
and len(song
['album']) > 0:
56 self
.album
= song
['album']
58 self
.album
= 'album is for losers'
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'])
69 if 'result' in libsong
and 'songdetails' in libsong
['result']:
70 song
['userrating'] = libsong
['result']['songdetails']['userrating']
71 elif 'songid' in song
:
73 self
.key
= hashlib
.sha256(str(song
['songid']).encode('utf-8')).hexdigest()
74 self
.kodi_id
= song
['songid']
76 self
.key
= hashlib
.sha256((song
['label'] + self
.artist
).encode('utf-8')).hexdigest()
79 # videos can still be labeled as songs, but the rating will be a
81 if 'userrating' in song
and isinstance (song
['userrating'], numbers
.Integral
):
82 self
.rating
= song
['userrating']
84 self
.rating
= -1 # might be better to use None here
86 self
.label
= song
['label']
89 '''Convert list of Kodi Items into Song instances'''
90 return [Song(item
) for item
in items
]
92 def get_playlist (playlistid
=0):
93 return songs (kodi
.Playlist
.GetItems (playlistid
=playlistid
, properties
=SONG_PROPERTIES
)['result']['items'])
96 aactions
= {'songup': 'up', 'songdown': 'down', 'songtop': 'next!', 'songbottom': 'banish!',
97 'songdel': 'del', 'randomqueue': 'yeh', 'songrate': 'rate'}
99 def __init__ (self
, song
, actions
= ['songup', 'songdown', 'songdel']):
101 self
.actions
= actions
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()
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):
122 for i
in range (1,6):
123 with doc
.option (value
= i
*2):
125 doc
.stag ('input', type = 'hidden', name
= 'songkodiid', value
= self
.song
.kodi_id
)
127 return doc
.getvalue ()
130 ANY_SEARCH_PROPERTIES
= [ 'title', 'album', 'artist' ]
132 def __init__ (self
, term
= '', prop
= 'title'):
136 self
.results
= self
.get_search_results ()
138 def get_search_results (self
):
139 if (self
.term
== ''):
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']
145 return songs(res
['songs'])
149 all_songs
= [kodi
.AudioLibrary
.GetSongs (filter={'operator': "contains", 'field': p
, 'value': self
.term
}, properties
=SONG_PROPERTIES
, sort
={'order': 'ascending', 'method': 'artist'})['result']
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
]))
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'):
159 doc
.stag ('input', type = 'text', name
= 'searchterm', value
= self
.term
, id = 'quicksearch')
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
):
166 with
tag ('button', type = 'submit', name
= 'searchgo', value
= '1'):
168 print (doc
.getvalue ())
170 def show_search_results (self
):
171 doc
, tag
, text
= Doc().tagtext()
174 if len (self
.results
) > 0:
175 doc
.asis (Playlist (self
.results
).show (['randomqueue']))
178 text ('You are unworthy. No results.')
180 print (doc
.getvalue ())
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
190 self
.playlist
= songs (kodi_playlist
)
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))
198 with
tag ('div', klass
= 'flex_row'):
200 with
tag ('a', href
= '{1}?searchgo=1&searchterm={0};searchfield=artist'.format(song
.artist
, PAGE_SELF
)):
202 text (' ({}) {}'.format(song
.album
, song
.label
))
203 doc
.asis (SongControls (song
, controls
).controls())
204 return doc
.getvalue ()
206 def get_playlist (self
, playlistid
=0):
207 return songs (kodi
.Playlist
.GetItems (playlistid
=playlistid
, properties
=SONG_PROPERTIES
)['result']['items'])
210 upload_dir
= '/srv/archive/incoming/stolen-moosic'
212 def __init__ (self
, form
, field
):
213 self
.fileitem
= form
[field
]
214 self
.filename
= '{}/{}'.format (self
.upload_dir
, self
.fileitem
.filename
)
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
])
225 fout
= open (os
.path
.join(self
.upload_dir
, self
.fileitem
.filename
), 'wb')
226 fout
.write (self
.fileitem
.value
)
228 self
.attempt_rpgain ()
229 return { 'file': self
.filename
}
232 upload_dir
= '/srv/archive/incoming/youtube-moosic'
234 'format': 'bestaudio/best',
235 'outtmpl': upload_dir
+ '/%(title)s-%(id)s.%(ext)s',
239 'key': 'FFmpegMetadata',
242 'key': 'FFmpegExtractAudio',
243 'preferredcodec': 'vorbis',
248 def __init__ (self
, form
, field
):
249 self
.ydl
= youtube_dl
.YoutubeDL(self
.ydl_opts
)
250 self
.url
= form
.getvalue (field
)
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
}
260 doc
, tag
, text
= Doc ().tagtext ()
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"); */
267 background-color: black;
268 font-family: sans-serif;
271 button[name=songdel] { margin-left: 1em; margin-right: 1em; }
274 flex-flow: row nowrap;
275 justify-content: space-between;
279 .flex_row p { font-size: 175%; max-width: 50%; min-width: 50% }
281 ol li:nth-child(even) { background-color: #202020 }
283 return doc
.getvalue ()
285 def print_escaped (item
):
286 print (u
"<p>{}</p>".format (html
.escape (u
"{}".format (item
))))
290 DEFAULT_QUEUE_DIVISOR
= 3
292 def __init__ (self
, form
):
295 def randomqueue (self
, item
, divisor
=None):
296 # randomly queue song somewhere in first (playlist.length /
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)
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
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
)
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'])