6ac2fd3d3ee62c24596282c510161bc038ae8d79
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
28 from xbmcjson
import XBMC
29 from yattag
import Doc
39 SONG_PROPERTIES
= ['album', 'artist', 'albumartist', 'dateadded', 'userrating', 'displayartist']
40 PAGE_SELF
= os
.environ
['SCRIPT_NAME'] if 'SCRIPT_NAME' in os
.environ
else ''
43 def __init__ (self
, song
):
45 if 'artist' in song
and len(song
['artist']) > 0:
46 self
.artist
= song
['artist'][0]
47 if 'displayartist' in song
:
48 self
.artist
= song
['displayartist']
49 elif 'albumartist' in song
and len(song
['albumartist']) > 0:
50 self
.artist
= song
['albumartist'][0]
52 self
.artist
= 'who fucking knows'
54 if 'album' in song
and len(song
['album']) > 0:
55 self
.album
= song
['album']
57 self
.album
= 'album is for losers'
61 self
.key
= hashlib
.sha256(str(song
['id'])).hexdigest()
62 self
.kodi_id
= song
['id']
63 # the playlist will not update things like ratings if we
64 # update via RPC. Just grab it from the library instead.
65 if 'userrating' in song
:
66 libsong
= xbmc
.AudioLibrary
.GetSongDetails (songid
= song
['id'], properties
= ['userrating'])
68 if 'result' in libsong
and 'songdetails' in libsong
['result']:
69 song
['userrating'] = libsong
['result']['songdetails']['userrating']
70 elif 'songid' in song
:
72 self
.key
= hashlib
.sha256(str(song
['songid'])).hexdigest()
73 self
.kodi_id
= song
['songid']
75 self
.key
= hashlib
.sha256(song
['label'] + self
.artist
).hexdigest()
78 # videos can still be labeled as songs, but the rating will be a
80 if 'userrating' in song
and isinstance (song
['userrating'], numbers
.Integral
):
81 self
.rating
= song
['userrating']
83 self
.rating
= -1 # might be better to use None here
85 self
.label
= song
['label']
88 '''Convert list of Kodi Items into Song instances'''
89 return [Song(item
) for item
in items
]
91 def get_playlist (playlistid
=0):
92 return songs (xbmc
.Playlist
.GetItems (playlistid
=playlistid
, properties
=SONG_PROPERTIES
)['result']['items'])
95 aactions
= {'songup': 'up', 'songdown': 'down', 'songtop': 'next!', 'songbottom': 'banish!',
96 'songdel': 'del', 'randomqueue': 'yeh', 'songrate': 'rate'}
98 def __init__ (self
, song
, actions
= ['songup', 'songdown', 'songdel']):
100 self
.actions
= actions
103 doc
, tag
, text
= Doc().tagtext()
104 with
tag ('form', method
= 'post', action
= PAGE_SELF
, klass
= 'song_controls'): #, style = 'display: inline-block'):
105 for action
in self
.actions
:
106 with
tag ('button', name
= action
, value
= self
.song
.key
):
107 text (self
.aactions
[action
])
108 doc
.asis (self
.extra_elements (action
))
109 return doc
.getvalue()
111 def extra_elements (self
, action
):
112 doc
, tag
, text
= Doc().tagtext()
113 if action
== 'randomqueue':
114 doc
.stag ('input', type = 'hidden', name
= 'songkodiid', value
= self
.song
.kodi_id
)
115 elif action
== 'songrate':
116 if self
.song
.rating
> -1:
117 doc
.defaults
= {'songrating': self
.song
.rating
}
118 with doc
.select (name
= 'songrating'):
119 with doc
.option (value
= 0):
121 for i
in range (1,6):
122 with doc
.option (value
= i
*2):
124 doc
.stag ('input', type = 'hidden', name
= 'songkodiid', value
= self
.song
.kodi_id
)
126 return doc
.getvalue ()
129 ANY_SEARCH_PROPERTIES
= [ 'title', 'album', 'artist' ]
131 def __init__ (self
, term
= '', prop
= 'title'):
135 self
.results
= self
.get_search_results ()
137 def get_search_results (self
):
138 if (self
.term
== ''):
141 if (self
.prop
!= 'any'):
142 res
= xbmc
.AudioLibrary
.GetSongs (filter={'operator': "contains", 'field': self
.prop
, 'value': self
.term
}, properties
=SONG_PROPERTIES
, sort
={'order': 'ascending', 'method': 'artist'})['result']
144 return songs(res
['songs'])
148 all_songs
= [xbmc
.AudioLibrary
.GetSongs (filter={'operator': "contains", 'field': p
, 'value': self
.term
}, properties
=SONG_PROPERTIES
, sort
={'order': 'ascending', 'method': 'artist'})['result']
150 in self
.ANY_SEARCH_PROPERTIES
]
151 # does not remove duplicates...
152 return list (itertools
.chain
.from_iterable ([res
['songs'] for res
in all_songs
if 'songs' in res
]))
154 def show_quick_search (self
, thereal
=False):
155 doc
, tag
, text
= Doc(defaults
= {'searchfield': self
.prop
}).tagtext()
156 with
tag ('form', method
= 'get', action
= PAGE_SELF
, style
= 'display: inline-block'):
158 doc
.stag ('input', type = 'text', name
= 'searchterm', value
= self
.term
, id = 'quicksearch')
160 doc
.stag ('input', type = 'text', name
= 'searchterm', value
= self
.term
)
161 with doc
.select (name
= 'searchfield'):
162 for prop
in ['title', 'artist', 'album', 'any']:
163 with doc
.option (value
= prop
):
165 with
tag ('button', type = 'submit', name
= 'searchgo', value
= '1'):
167 print doc
.getvalue ()
169 def show_search_results (self
):
170 doc
, tag
, text
= Doc().tagtext()
173 if len (self
.results
) > 0:
174 doc
.asis (Playlist (self
.results
).show (['randomqueue']))
177 text ('You are unworthy. No results.')
179 print (doc
.getvalue ())
182 default_controls
= ['songup', 'songdown', 'songdel', 'songrate', 'songtop', 'songbottom']
183 def __init__ (self
, kodi_playlist
= None):
184 if kodi_playlist
is None:
185 self
.playlist
= self
.get_playlist ()
186 elif (all (isinstance (s
, Song
) for s
in kodi_playlist
)):
187 self
.playlist
= kodi_playlist
189 self
.playlist
= songs (kodi_playlist
)
191 def show (self
, controls
= default_controls
):
192 doc
, tag
, text
= Doc().tagtext()
193 with
tag ('ol', klass
= 'flex_list'):
194 for song
in self
.playlist
:
195 # text ("{}".format (song._song))
197 with
tag ('div', klass
= 'flex_row'):
199 with
tag ('a', href
= '{1}?searchgo=1&searchterm={0};searchfield=artist'.format(song
.artist
, PAGE_SELF
)):
201 text (' ({}) {}'.format(song
.album
, song
.label
))
202 doc
.asis (SongControls (song
, controls
).controls())
203 return doc
.getvalue ()
205 def get_playlist (self
, playlistid
=0):
206 return songs (xbmc
.Playlist
.GetItems (playlistid
=playlistid
, properties
=SONG_PROPERTIES
)['result']['items'])
209 upload_dir
= '/srv/archive/incoming/stolen-moosic'
211 def __init__ (self
, form
, field
):
212 self
.fileitem
= form
[field
]
213 self
.filename
= '{}/{}'.format (self
.upload_dir
, self
.fileitem
.filename
)
215 # Evil: just run replaygain/mp3gain/metaflac on the file and hope one
216 # works instead of dealing with MIME. For now.
217 def attempt_rpgain (self
):
218 subprocess
.call (["/usr/bin/vorbisgain", "-q", self
.filename
])
219 subprocess
.call (["/usr/bin/mp3gain", "-q", "-s", "i", self
.filename
])
220 subprocess
.call (["/usr/bin/aacgain", "-q", "-s", "i", self
.filename
])
221 subprocess
.call (["/usr/bin/metaflac", "--add-replay-gain", self
.filename
])
224 fout
= file (os
.path
.join(self
.upload_dir
, self
.fileitem
.filename
), 'wb')
225 fout
.write (self
.fileitem
.value
)
227 self
.attempt_rpgain ()
228 return { 'file': self
.filename
}
231 upload_dir
= '/srv/archive/incoming/youtube-moosic'
233 'format': 'bestaudio/best',
234 'outtmpl': upload_dir
+ '/%(title)s-%(id)s.%(ext)s',
238 'key': 'FFmpegMetadata',
241 'key': 'FFmpegExtractAudio',
242 'preferredcodec': 'vorbis',
247 def __init__ (self
, form
, field
):
248 self
.ydl
= youtube_dl
.YoutubeDL(self
.ydl_opts
)
249 self
.url
= form
.getvalue (field
)
252 info
= self
.ydl
.extract_info (self
.url
, download
=True)
253 filename
= re
.sub ('\..{3,4}$', '.ogg', self
.ydl
.prepare_filename (info
))
254 subprocess
.call (["/usr/bin/vorbisgain", "-q", filename
])
255 return { 'file': filename
}
259 doc
, tag
, text
= Doc ().tagtext ()
262 input, select, button { font-size: 200%; margin: 0.1em; }
263 .horiz-menu li { display: inline; padding-right: 0.5em; font-size: 1.75rem; }
264 body { /* background-image: url("fire-under-construction-animation.gif"); */
266 background-color: black;
267 font-family: sans-serif;
270 button[name=songdel] { margin-left: 1em; margin-right: 1em; }
273 flex-flow: row nowrap;
274 justify-content: space-between;
278 .flex_row p { font-size: 175%; max-width: 50%; min-width: 50% }
280 ol li:nth-child(even) { background-color: #202020 }
282 return doc
.getvalue ()
284 def print_escaped (item
):
285 print (u
"<p>{}</p>".format (cgi
.escape (u
"{}".format (item
))))
289 DEFAULT_QUEUE_DIVISOR
= 3
291 def __init__ (self
, form
):
294 def randomqueue (self
, item
, divisor
=None):
295 # randomly queue song somewhere in first (playlist.length /
298 divisor
= self
.DEFAULT_QUEUE_DIVISOR
299 totalitems
= xbmc
.Playlist
.GetItems (playlistid
=0)['result']['limits']['total']
300 playpos
= random
.randint (1, totalitems
/ divisor
+ 1)
301 print_escaped (xbmc
.Playlist
.Insert (playlistid
=0, item
=item
, position
=playpos
))
302 print '<p style="font-size: x-large">Your song is number {0} in the queue ({1} songs in playlist).</p>'.format (playpos
, totalitems
+1)
303 return (playpos
, totalitems
+1)
307 if 'songdel' in form
:
308 songid
= form
['songdel'].value
309 print (u
"<p>{}</p>".format (songid
))
310 (pos
,song
) = next ((i
,s
) for i
,s
in enumerate(get_playlist ()) if s
.key
== songid
)
311 print (u
'<p>Deleted {}</p>'.format(cgi
.escape (song
.label
)))
312 print_escaped (xbmc
.Playlist
.Remove (playlistid
=0, position
=pos
))
313 elif 'songup' in form
:
314 songid
= form
['songup'].value
315 print (u
"<p>{}</p>".format (songid
))
316 (pos
,song
) = next ((i
,s
) for i
,s
in enumerate(get_playlist ()) if s
.key
== songid
)
317 print (u
"<p>Promoted {}</p>".format(cgi
.escape(song
.label
)))
318 print_escaped (xbmc
.Playlist
.Swap (playlistid
=0, position1
=pos
, position2
=pos
-1))
319 elif 'songdown' in form
:
320 songid
= form
['songdown'].value
321 print (u
"<p>{}</p>".format (songid
))
322 (pos
,song
) = next ((i
,s
) for i
,s
in enumerate(get_playlist ()) if s
.key
== songid
)
323 print (u
"<p>Demoted {}</p>".format(cgi
.escape(song
.label
)))
324 print_escaped (xbmc
.Playlist
.Swap (playlistid
=0, position1
=pos
, position2
=pos
+1))
325 elif 'songtop' in form
:
326 songid
= form
['songtop'].value
327 print (u
"<p>{}</p>".format (songid
))
328 (pos
,song
) = next ((i
,s
) for i
,s
in enumerate(get_playlist ()) if s
.key
== songid
)
329 print (u
"<p>Bumped Up {}</p>".format(cgi
.escape(song
.label
)))
330 for i
in range (pos
, 1, -1):
331 print_escaped (xbmc
.Playlist
.Swap (playlistid
=0, position1
=i
, position2
=i
-1))
332 elif 'songbottom' in form
:
333 songid
= form
['songbottom'].value
334 print (u
"<p>{}</p>".format (songid
))
335 playlist
= get_playlist ()
336 (pos
,song
) = next ((i
,s
) for i
,s
in enumerate(playlist
) if s
.key
== songid
)
337 print (u
"<p>Banished {}</p>".format(cgi
.escape(song
.label
)))
338 for i
in range (pos
, len (playlist
), 1):
339 print_escaped (xbmc
.Playlist
.Swap (playlistid
=0, position1
=i
, position2
=i
+1))
340 elif 'volchange' in form
:
341 curvolume
= xbmc
.Application
.GetProperties (properties
=['volume'])['result']['volume']
342 newvolume
= max (0, min (int (form
['volchange'].value
) + curvolume
, 100))
343 print_escaped (xbmc
.Application
.SetVolume (volume
=newvolume
))
344 elif 'volmute' in form
:
345 print_escaped (xbmc
.Application
.SetMute (mute
="toggle"))
346 elif 'navigate' in form
:
347 action
= form
['navigate'].value
349 print_escaped (xbmc
.Player
.GoTo (to
="previous", playerid
=0))
350 elif action
== 'next':
351 print_escaped (xbmc
.Player
.GoTo (to
="next", playerid
=0))
352 elif action
== 'playpause':
353 print_escaped (xbmc
.Player
.PlayPause (play
="toggle", playerid
=0))
354 elif 'searchgo' in form
:
355 term
= form
['searchterm'].value
356 field
= form
['searchfield'].value
357 search
= Search (term
, field
)
358 search
.show_quick_search ()
359 search
.show_search_results ()
360 elif 'randomqueue' in form
:
361 songid
= int(form
['songkodiid'].value
)
362 self
.randomqueue ({"songid": songid
})
363 elif 'songrate' in form
:
364 songid
= int(form
['songkodiid'].value
)
365 newrating
= int(form
['songrating'].value
)
368 print_escaped (xbmc
.AudioLibrary
.SetSongDetails (songid
= songid
, userrating
= newrating
))
369 print_escaped (u
'Rating Changed')
370 elif 'browseartists' in form
:
371 artists
= xbmc
.AudioLibrary
.GetArtists (sort
={'order': 'ascending', 'method': 'artist'})['result']['artists']
372 doc
, tag
, text
= Doc().tagtext()
373 with
tag ('ol', klass
='flex_list'):
374 for artist
in artists
:
375 with
tag ('li', style
='padding: 1rem; font-size: x-large'):
376 with
tag ('a', href
='{}?searchgo=1&searchterm={}&searchfield=artist'.format (PAGE_SELF
, urllib
.quote_plus (artist
['artist'].encode ('utf-8')).decode ('utf-8'))):
377 text (artist
['label'])
378 print (doc
.getvalue ())
379 elif 'uploadgo' in form
:
380 upload
= Upload (form
, 'song')
381 item
= upload
.save ()
382 self
.randomqueue (item
, 1 if 'asap' not in form
else 3)
383 elif 'youtubego' in form
:
384 youtube
= Youtube (form
, 'youtubeurl')
385 item
= youtube
.save ()
386 self
.randomqueue (item
, 1 if 'asap' not in form
else 3)
387 elif 'partyon' in form
:
388 if 'error' in xbmc
.Player
.SetPartymode (partymode
=True, playerid
=0):
389 xbmc
.Player
.Open (item
={"partymode": "music"})
390 elif 'lockon' in form
:
391 subprocess
.call (['/usr/bin/xscreensaver-command', 'lock'])
392 elif 'lights' in form
:
393 subprocess
.call (['/usr/bin/br', '-N' if form
['lights'].value
== 'on' else '-F'])