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', 'title', '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 if 'title' in song
and len(song
['title']) > 0:
87 self
.label
= song
['title']
89 self
.label
= song
['label']
92 '''Convert list of Kodi Items into Song instances'''
93 return [Song(item
) for item
in items
]
95 def get_playlist (playlistid
=0):
96 return songs (kodi
.Playlist
.GetItems (playlistid
=playlistid
, properties
=SONG_PROPERTIES
)['result']['items'])
99 aactions
= {'songup': 'up', 'songdown': 'down', 'songtop': 'next!', 'songbottom': 'banish!',
100 'songdel': 'del', 'randomqueue': 'yeh', 'songrate': 'rate'}
102 def __init__ (self
, song
, actions
= ['songup', 'songdown', 'songdel']):
104 self
.actions
= actions
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()
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):
125 for i
in range (1,6):
126 with doc
.option (value
= i
*2):
128 doc
.stag ('input', type = 'hidden', name
= 'songkodiid', value
= self
.song
.kodi_id
)
130 return doc
.getvalue ()
133 ANY_SEARCH_PROPERTIES
= [ 'title', 'album', 'artist' ]
135 def __init__ (self
, term
= '', prop
= 'title'):
139 self
.results
= self
.get_search_results ()
141 def get_search_results (self
):
142 if (self
.term
== ''):
145 if (self
.prop
!= 'any'):
146 res
= kodi
.AudioLibrary
.GetSongs (filter={'operator': "contains", 'field': self
.prop
, 'value': self
.term
}, properties
=SONG_PROPERTIES
, sort
={'order': 'ascending', 'method': 'artist'})['result']
148 return songs(res
['songs'])
152 all_songs
= [kodi
.AudioLibrary
.GetSongs (filter={'operator': "contains", 'field': p
, 'value': self
.term
}, properties
=SONG_PROPERTIES
, sort
={'order': 'ascending', 'method': 'artist'})['result']
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
]))
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'):
162 doc
.stag ('input', type = 'text', name
= 'searchterm', value
= self
.term
, id = 'quicksearch')
164 doc
.stag ('input', type = 'text', name
= 'searchterm', value
= self
.term
)
165 with doc
.select (name
= 'searchfield'):
166 for prop
in ['title', 'artist', 'album', 'any']:
167 with doc
.option (value
= prop
):
169 with
tag ('button', type = 'submit', name
= 'searchgo', value
= '1'):
171 print (doc
.getvalue ())
173 def show_search_results (self
):
174 doc
, tag
, text
= Doc().tagtext()
177 if len (self
.results
) > 0:
178 doc
.asis (Playlist (self
.results
).show (['randomqueue']))
181 text ('You are unworthy. No results.')
183 print (doc
.getvalue ())
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
193 self
.playlist
= songs (kodi_playlist
)
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))
201 with
tag ('div', klass
= 'flex_row'):
203 with
tag ('a', href
= '{1}?searchgo=1&searchterm={0};searchfield=artist'.format(song
.artist
, PAGE_SELF
)):
205 text (' ({}) {}'.format(song
.album
, song
.label
))
206 doc
.asis (SongControls (song
, controls
).controls())
207 return doc
.getvalue ()
209 def get_playlist (self
, playlistid
=0):
210 return songs (kodi
.Playlist
.GetItems (playlistid
=playlistid
, properties
=SONG_PROPERTIES
)['result']['items'])
213 upload_dir
= '/srv/archive/incoming/stolen-moosic'
215 def __init__ (self
, form
, field
):
216 self
.fileitem
= form
[field
]
217 self
.filename
= '{}/{}'.format (self
.upload_dir
, self
.fileitem
.filename
)
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
])
223 subprocess
.call (["/usr/local/bin/mp3gain", "-q", "-s", "i", self
.filename
])
224 subprocess
.call (["/usr/bin/aacgain", "-q", "-s", "i", self
.filename
])
225 subprocess
.call (["/usr/bin/metaflac", "--add-replay-gain", self
.filename
])
228 fout
= open (os
.path
.join(self
.upload_dir
, self
.fileitem
.filename
), 'wb')
229 fout
.write (self
.fileitem
.value
)
231 self
.attempt_rpgain ()
232 return { 'file': self
.filename
}
235 upload_dir
= '/srv/archive/incoming/youtube-moosic'
237 'format': 'bestaudio/best',
238 'outtmpl': upload_dir
+ '/%(title)s-%(id)s.%(ext)s',
242 'key': 'FFmpegMetadata',
245 'key': 'FFmpegExtractAudio',
246 'preferredcodec': 'vorbis',
251 def __init__ (self
, form
, field
):
252 self
.ydl
= youtube_dl
.YoutubeDL(self
.ydl_opts
)
253 self
.url
= form
.getvalue (field
)
256 info
= self
.ydl
.extract_info (self
.url
, download
=True)
257 filename
= re
.sub ('\..{3,4}$', '.ogg', self
.ydl
.prepare_filename (info
))
258 subprocess
.call (["/usr/bin/vorbisgain", "-q", filename
])
259 return { 'file': filename
}
263 doc
, tag
, text
= Doc ().tagtext ()
266 input, select, button { font-size: 200%; margin: 0.1em; }
267 .horiz-menu li { display: inline; padding-right: 0.5em; font-size: 1.75rem; }
268 body { /* background-image: url("fire-under-construction-animation.gif"); */
270 background-color: black;
271 font-family: sans-serif;
274 button[name=songdel] { margin-left: 1em; margin-right: 1em; }
277 flex-flow: row nowrap;
278 justify-content: space-between;
282 .flex_row p { font-size: 175%; max-width: 50%; min-width: 50% }
284 ol li:nth-child(even) { background-color: #202020 }
286 return doc
.getvalue ()
288 def print_escaped (item
):
289 print (u
"<p>{}</p>".format (html
.escape (u
"{}".format (item
))))
293 DEFAULT_QUEUE_DIVISOR
= 3
295 def __init__ (self
, form
):
298 def randomqueue (self
, item
, divisor
=None):
299 # randomly queue song somewhere in first (playlist.length /
302 divisor
= self
.DEFAULT_QUEUE_DIVISOR
303 totalitems
= kodi
.Playlist
.GetItems (playlistid
=0)['result']['limits']['total']
304 playpos
= random
.randint (1, totalitems
/ divisor
+ 1)
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))
307 return (playpos
, totalitems
+1)
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
)
315 print (u
'<p>Deleted {}</p>'.format(html
.escape (song
.label
)))
316 print_escaped (kodi
.Playlist
.Remove (playlistid
=0, position
=pos
))
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
)
321 print (u
"<p>Promoted {}</p>".format(html
.escape(song
.label
)))
322 print_escaped (kodi
.Playlist
.Swap (playlistid
=0, position1
=pos
, position2
=pos
-1))
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
)
327 print (u
"<p>Demoted {}</p>".format(html
.escape(song
.label
)))
328 print_escaped (kodi
.Playlist
.Swap (playlistid
=0, position1
=pos
, position2
=pos
+1))
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
)
333 print (u
"<p>Bumped Up {}</p>".format(html
.escape(song
.label
)))
334 for i
in range (pos
, 1, -1):
335 print_escaped (kodi
.Playlist
.Swap (playlistid
=0, position1
=i
, position2
=i
-1))
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
)
341 print (u
"<p>Banished {}</p>".format(html
.escape(song
.label
)))
342 for i
in range (pos
, len (playlist
), 1):
343 print_escaped (kodi
.Playlist
.Swap (playlistid
=0, position1
=i
, position2
=i
+1))
344 elif 'volchange' in form
:
345 curvolume
= kodi
.Application
.GetProperties (properties
=['volume'])['result']['volume']
346 newvolume
= max (0, min (int (form
['volchange'].value
) + curvolume
, 100))
347 print_escaped (kodi
.Application
.SetVolume (volume
=newvolume
))
348 elif 'volmute' in form
:
349 print_escaped (kodi
.Application
.SetMute (mute
="toggle"))
350 elif 'navigate' in form
:
351 action
= form
['navigate'].value
353 print_escaped (kodi
.Player
.GoTo (to
="previous", playerid
=0))
354 elif action
== 'next':
355 print_escaped (kodi
.Player
.GoTo (to
="next", playerid
=0))
356 elif action
== 'playpause':
357 print_escaped (kodi
.Player
.PlayPause (play
="toggle", playerid
=0))
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
)
372 print_escaped (kodi
.AudioLibrary
.SetSongDetails (songid
= songid
, userrating
= newrating
))
373 print_escaped (u
'Rating Changed')
374 elif 'browseartists' in form
:
375 artists
= kodi
.AudioLibrary
.GetArtists (sort
={'order': 'ascending', 'method': 'artist'})['result']['artists']
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')
385 item
= upload
.save ()
386 self
.randomqueue (item
, 1 if 'asap' not in form
else 3)
387 elif 'youtubego' in form
:
388 youtube
= Youtube (form
, 'youtubeurl')
389 item
= youtube
.save ()
390 self
.randomqueue (item
, 1 if 'asap' not in form
else 3)
391 elif 'partyon' in form
:
392 if 'error' in kodi
.Player
.SetPartymode (partymode
=True, playerid
=0):
393 kodi
.Player
.Open (item
={"partymode": "music"})
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'])