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
30 from kodijson
import Kodi
31 from yattag
import Doc
41 SONG_PROPERTIES
= ['album', 'artist', 'albumartist', 'title', 'dateadded', 'userrating', 'displayartist']
42 #PAGE_SELF = os.environ['SCRIPT_NAME'] if 'SCRIPT_NAME' in os.environ else ''
43 PAGE_SELF
= os
.environ
['SCRIPT_NAME'].rsplit('/', 1)[-1] if 'SCRIPT_NAME' in os
.environ
else ''
47 def __init__ (self
, 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]
56 self
.artist
= 'who fucking knows'
58 if 'album' in song
and len(song
['album']) > 0:
59 self
.album
= song
['album']
61 self
.album
= 'album is for losers'
65 self
.key
= hashlib
.sha256(str(song
['id']).encode('utf-8')).hexdigest()
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.
69 if 'userrating' in song
:
70 libsong
= kodi
.AudioLibrary
.GetSongDetails (songid
= song
['id'], properties
= ['userrating'])
72 if 'result' in libsong
and 'songdetails' in libsong
['result']:
73 song
['userrating'] = libsong
['result']['songdetails']['userrating']
74 elif 'songid' in song
:
76 self
.key
= hashlib
.sha256(str(song
['songid']).encode('utf-8')).hexdigest()
77 self
.kodi_id
= song
['songid']
79 self
.key
= hashlib
.sha256((song
['label'] + self
.artist
).encode('utf-8')).hexdigest()
82 # videos can still be labeled as songs, but the rating will be a
84 if 'userrating' in song
and isinstance (song
['userrating'], numbers
.Integral
):
85 self
.rating
= song
['userrating']
87 self
.rating
= -1 # might be better to use None here
89 if 'title' in song
and len(song
['title']) > 0:
90 self
.label
= song
['title']
92 self
.label
= song
['label']
95 '''Convert list of Kodi Items into Song instances'''
96 return [Song(item
) for item
in items
]
98 def get_playlist (playlistid
=0):
99 return songs (kodi
.Playlist
.GetItems (playlistid
=playlistid
, properties
=SONG_PROPERTIES
)['result']['items'])
102 aactions
= {'songup': 'up', 'songdown': 'down', 'songtop': 'next!', 'songbottom': 'banish!',
103 'songdel': 'del', 'randomqueue': 'yeh', 'songrate': 'rate'}
105 def __init__ (self
, song
, actions
= ['songup', 'songdown', 'songdel']):
107 self
.actions
= actions
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()
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):
128 for i
in range (1,6):
129 with doc
.option (value
= i
*2):
131 doc
.stag ('input', type = 'hidden', name
= 'songkodiid', value
= self
.song
.kodi_id
)
133 return doc
.getvalue ()
136 ANY_SEARCH_PROPERTIES
= [ 'title', 'album', 'artist' ]
138 def __init__ (self
, term
= '', prop
= 'title'):
142 self
.results
= self
.get_search_results ()
144 def get_search_results (self
):
145 if (self
.term
== ''):
148 if (self
.prop
!= 'any'):
149 res
= kodi
.AudioLibrary
.GetSongs (filter={'operator': "contains", 'field': self
.prop
, 'value': self
.term
}, properties
=SONG_PROPERTIES
, sort
={'order': 'ascending', 'method': 'artist'})['result']
151 return songs(res
['songs'])
155 all_songs
= [kodi
.AudioLibrary
.GetSongs (filter={'operator': "contains", 'field': p
, 'value': self
.term
}, properties
=SONG_PROPERTIES
, sort
={'order': 'ascending', 'method': 'artist'})['result']
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
]))
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'):
165 doc
.stag ('input', type = 'text', name
= 'searchterm', value
= self
.term
, id = 'quicksearch')
167 doc
.stag ('input', type = 'text', name
= 'searchterm', value
= self
.term
)
168 with doc
.select (name
= 'searchfield'):
169 for prop
in ['title', 'artist', 'album', 'any']:
170 with doc
.option (value
= prop
):
172 with
tag ('button', type = 'submit', name
= 'searchgo', value
= '1'):
174 print (doc
.getvalue ())
176 def show_search_results (self
):
177 doc
, tag
, text
= Doc().tagtext()
180 if len (self
.results
) > 0:
181 doc
.asis (Playlist (self
.results
).show (['randomqueue']))
184 text ('You are unworthy. No results.')
186 print (doc
.getvalue ())
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
196 self
.playlist
= songs (kodi_playlist
)
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))
204 with
tag ('div', klass
= 'flex_row'):
206 with
tag ('a', href
= '{1}?searchgo=1&searchterm={0};searchfield=artist'.format(song
.artist
, PAGE_SELF
)):
208 text (' ({}) {}'.format(song
.album
, song
.label
))
209 doc
.asis (SongControls (song
, controls
).controls())
210 return doc
.getvalue ()
212 def get_playlist (self
, playlistid
=0):
213 return songs (kodi
.Playlist
.GetItems (playlistid
=playlistid
, properties
=SONG_PROPERTIES
)['result']['items'])
216 upload_dir
= '/srv/archive/incoming/stolen-moosic'
218 def __init__ (self
, form
, field
):
219 self
.fileitem
= form
[field
]
220 self
.filename
= '{}/{}'.format (self
.upload_dir
, self
.fileitem
.filename
)
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
])
226 subprocess
.call (["/usr/local/bin/mp3gain", "-q", "-s", "i", self
.filename
])
227 subprocess
.call (["/usr/bin/aacgain", "-q", "-s", "i", self
.filename
])
228 subprocess
.call (["/usr/bin/metaflac", "--add-replay-gain", self
.filename
])
231 fout
= open (os
.path
.join(self
.upload_dir
, self
.fileitem
.filename
), 'wb')
232 fout
.write (self
.fileitem
.value
)
234 self
.attempt_rpgain ()
235 return { 'file': self
.filename
}
238 upload_dir
= '/srv/archive/incoming/youtube-moosic'
240 'format': 'bestaudio/best',
241 'outtmpl': upload_dir
+ '/%(title)s-%(id)s.%(ext)s',
245 'key': 'FFmpegMetadata',
248 'key': 'FFmpegExtractAudio',
249 'preferredcodec': 'vorbis',
254 def __init__ (self
, form
, field
):
255 self
.ydl
= youtube_dl
.YoutubeDL(self
.ydl_opts
)
256 self
.url
= form
.getvalue (field
)
259 info
= self
.ydl
.extract_info (self
.url
, download
=True)
260 filename
= re
.sub ('\..{3,4}$', '.ogg', self
.ydl
.prepare_filename (info
))
261 subprocess
.call (["/usr/bin/vorbisgain", "-q", filename
])
262 return { 'file': filename
}
266 doc
, tag
, text
= Doc ().tagtext ()
269 input, select, button { font-size: 200%; margin: 0.1em; }
270 .horiz-menu li { display: inline; padding-right: 0.5em; font-size: 1.75rem; }
271 body { /* background-image: url("fire-under-construction-animation.gif"); */
273 background-color: black;
274 font-family: sans-serif;
277 button[name=songdel] { margin-left: 1em; margin-right: 1em; }
280 flex-flow: row nowrap;
281 justify-content: space-between;
285 .flex_row p { font-size: 175%; max-width: 50%; min-width: 50% }
287 ol li:nth-child(even) { background-color: #202020 }
289 return doc
.getvalue ()
291 def print_escaped (item
):
292 print (u
"<p>{}</p>".format (html
.escape (u
"{}".format (item
))))
296 DEFAULT_QUEUE_DIVISOR
= 3
298 def __init__ (self
, form
):
301 def randomqueue (self
, item
, divisor
=None):
302 # randomly queue song somewhere in first (playlist.length /
305 divisor
= self
.DEFAULT_QUEUE_DIVISOR
306 totalitems
= kodi
.Playlist
.GetItems (playlistid
=0)['result']['limits']['total']
307 playpos
= random
.randint (1, int(totalitems
/ divisor
+ 1))
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))
310 return (playpos
, totalitems
+1)
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
)
318 print (u
'<p>Deleted {}</p>'.format(html
.escape (song
.label
)))
319 print_escaped (kodi
.Playlist
.Remove (playlistid
=0, position
=pos
))
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
)
324 print (u
"<p>Promoted {}</p>".format(html
.escape(song
.label
)))
325 print_escaped (kodi
.Playlist
.Swap (playlistid
=0, position1
=pos
, position2
=pos
-1))
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
)
330 print (u
"<p>Demoted {}</p>".format(html
.escape(song
.label
)))
331 print_escaped (kodi
.Playlist
.Swap (playlistid
=0, position1
=pos
, position2
=pos
+1))
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
)
336 print (u
"<p>Bumped Up {}</p>".format(html
.escape(song
.label
)))
337 for i
in range (pos
, 1, -1):
338 print_escaped (kodi
.Playlist
.Swap (playlistid
=0, position1
=i
, position2
=i
-1))
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
)
344 print (u
"<p>Banished {}</p>".format(html
.escape(song
.label
)))
345 for i
in range (pos
, len (playlist
), 1):
346 print_escaped (kodi
.Playlist
.Swap (playlistid
=0, position1
=i
, position2
=i
+1))
347 elif 'volchange' in form
:
348 curvolume
= kodi
.Application
.GetProperties (properties
=['volume'])['result']['volume']
349 newvolume
= max (0, min (int (form
['volchange'].value
) + curvolume
, 100))
350 print_escaped (kodi
.Application
.SetVolume (volume
=newvolume
))
351 elif 'volmute' in form
:
352 print_escaped (kodi
.Application
.SetMute (mute
="toggle"))
353 elif 'navigate' in form
:
354 action
= form
['navigate'].value
356 print_escaped (kodi
.Player
.GoTo (to
="previous", playerid
=0))
357 elif action
== 'next':
358 print_escaped (kodi
.Player
.GoTo (to
="next", playerid
=0))
359 elif action
== 'playpause':
360 print_escaped (kodi
.Player
.PlayPause (play
="toggle", playerid
=0))
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
)
375 print_escaped (kodi
.AudioLibrary
.SetSongDetails (songid
= songid
, userrating
= newrating
))
376 print_escaped (u
'Rating Changed')
377 elif 'browseartists' in form
:
378 artists
= kodi
.AudioLibrary
.GetArtists (sort
={'order': 'ascending', 'method': 'artist'})['result']['artists']
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'):
383 with
tag ('a', href
='{}?searchgo=1&searchterm={}&searchfield=artist'.format (PAGE_SELF
, urllib
.parse
.quote_plus (artist
['artist']))):
384 text (artist
['label'])
385 print (doc
.getvalue ())
386 elif 'uploadgo' in form
:
387 upload
= Upload (form
, 'song')
388 item
= upload
.save ()
389 self
.randomqueue (item
, 1 if 'asap' not in form
else 3)
390 elif 'youtubego' in form
:
391 youtube
= Youtube (form
, 'youtubeurl')
392 item
= youtube
.save ()
393 self
.randomqueue (item
, 1 if 'asap' not in form
else 3)
394 elif 'partyon' in form
:
395 if 'error' in kodi
.Player
.SetPartymode (partymode
=True, playerid
=0):
396 kodi
.Player
.Open (item
={"partymode": "music"})
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'])