Fix localization.
[clinton/xbmc-groove.git] / default.py
CommitLineData
2d388879 1# Copyright 2011 Stephen Denham
2
3# This file is part of xbmc-groove.
4#
5# xbmc-groove is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# xbmc-groove is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with xbmc-groove. If not, see <http://www.gnu.org/licenses/>.
17
18
f95afae7 19import urllib, sys, os, shutil, re, pickle, time, tempfile, xbmcaddon, xbmcplugin, xbmcgui, xbmc
2d388879 20
b26b96e6 21__addon__ = xbmcaddon.Addon('plugin.audio.groove')
2d388879 22__addonname__ = __addon__.getAddonInfo('name')
23__cwd__ = __addon__.getAddonInfo('path')
24__author__ = __addon__.getAddonInfo('author')
25__version__ = __addon__.getAddonInfo('version')
26__language__ = __addon__.getLocalizedString
27
8817bb2e 28MODE_SEARCH_SONGS = 1
29MODE_SEARCH_ALBUMS = 2
30MODE_SEARCH_ARTISTS = 3
86f629ea 31MODE_SEARCH_ARTISTS_ALBUMS = 4
32MODE_SEARCH_PLAYLISTS = 5
97289139 33MODE_ARTIST_POPULAR = 6
34MODE_POPULAR_SONGS = 7
35MODE_FAVORITES = 8
36MODE_PLAYLISTS = 9
37MODE_ALBUM = 10
38MODE_ARTIST = 11
39MODE_PLAYLIST = 12
40MODE_SONG_PAGE = 13
052028f1 41MODE_SIMILAR_ARTISTS = 14
42MODE_SONG = 15
43MODE_FAVORITE = 16
44MODE_UNFAVORITE = 17
45MODE_MAKE_PLAYLIST = 18
46MODE_REMOVE_PLAYLIST = 19
47MODE_RENAME_PLAYLIST = 20
48MODE_REMOVE_PLAYLIST_SONG = 21
49MODE_ADD_PLAYLIST_SONG = 22
7ea6f166 50
38df1fa5 51ACTION_MOVE_LEFT = 1
7ea6f166 52ACTION_MOVE_UP = 3
53ACTION_MOVE_DOWN = 4
54ACTION_PAGE_UP = 5
55ACTION_PAGE_DOWN = 6
56ACTION_SELECT_ITEM = 7
57ACTION_PREVIOUS_MENU = 10
8817bb2e 58
86f629ea 59# Formats for track labels
60ARTIST_ALBUM_NAME_LABEL = 0
61NAME_ALBUM_ARTIST_LABEL = 1
62
f95afae7 63# Stream marking time (seconds)
64STREAM_MARKING_TIME = 30
65
66songMarkTime = 0
67player = xbmc.Player()
68playTimer = None
69
2d388879 70baseDir = __cwd__
6ae708d0 71resDir = xbmc.translatePath(os.path.join(baseDir, 'resources'))
8817bb2e 72libDir = xbmc.translatePath(os.path.join(resDir, 'lib'))
73imgDir = xbmc.translatePath(os.path.join(resDir, 'img'))
2d388879 74cacheDir = os.path.join(xbmc.translatePath('special://masterprofile/addon_data/'), os.path.basename(baseDir))
7ce01be6 75thumbDirName = 'thumb'
2d388879 76thumbDir = os.path.join('special://masterprofile/addon_data/', os.path.basename(baseDir), thumbDirName)
4be42357 77
78baseModeUrl = 'plugin://plugin.audio.groove/'
e278f474 79playlistUrl = baseModeUrl + '?mode=' + str(MODE_PLAYLIST)
4be42357 80playlistsUrl = baseModeUrl + '?mode=' + str(MODE_PLAYLISTS)
81favoritesUrl = baseModeUrl + '?mode=' + str(MODE_FAVORITES)
82
b26b96e6 83searchArtistsAlbumsName = __language__(30006)
3d95dfcb 84
7ce01be6 85thumbDef = os.path.join(imgDir, 'default.tbn')
052028f1 86listBackground = os.path.join(imgDir, 'listbackground.png')
8817bb2e 87
88sys.path.append (libDir)
7ce01be6 89from GroovesharkAPI import GrooveAPI
f95afae7 90from threading import Event, Thread
7ce01be6 91
a3ad8f73 92try:
93 groovesharkApi = GrooveAPI()
7ce01be6 94 if groovesharkApi.pingService() != True:
b26b96e6 95 raise StandardError(__language__(30007))
a3ad8f73 96except:
b26b96e6 97 dialog = xbmcgui.Dialog(__language__(30008),__language__(30009),__language__(30010))
2d388879 98 dialog.ok()
a3ad8f73 99 sys.exit(-1)
100
f95afae7 101# Mark song as playing or played
102def markSong(songid, duration):
103 global songMarkTime
104 global playTimer
105 global player
106 if player.isPlayingAudio():
107 tNow = player.getTime()
108 if tNow >= STREAM_MARKING_TIME and songMarkTime == 0:
109 groovesharkApi.markStreamKeyOver30Secs()
110 songMarkTime = tNow
111 elif duration > tNow and duration - tNow < 2 and songMarkTime >= STREAM_MARKING_TIME:
112 playTimer.cancel()
113 songMarkTime = 0
114 groovesharkApi.markSongComplete(songid)
115 else:
116 playTimer.cancel()
117 songMarkTime = 0
118
8817bb2e 119class _Info:
120 def __init__( self, *args, **kwargs ):
121 self.__dict__.update( kwargs )
e278f474 122
052028f1 123# Window dialog to select a grooveshark playlist
124class GroovesharkPlaylistSelect(xbmcgui.WindowDialog):
125
126 def __init__(self, items=[]):
127 gap = int(self.getHeight()/100)
128 w = int(self.getWidth()*0.5)
129 h = self.getHeight()-30*gap
130 rw = self.getWidth()
131 rh = self.getHeight()
132 x = rw/2 - w/2
133 y = rh/2 -h/2
134
135 self.imgBg = xbmcgui.ControlImage(x+gap, 5*gap+y, w-2*gap, h-5*gap, listBackground)
136 self.addControl(self.imgBg)
137
138 self.playlistControl = xbmcgui.ControlList(2*gap+x, y+3*gap+30, w-4*gap, h-10*gap, textColor='0xFFFFFFFF', selectedColor='0xFFFF4242', itemTextYOffset=0, itemHeight=50, alignmentY = 0)
139 self.addControl(self.playlistControl)
140
141 self.lastPos = 0
142 self.isSelecting = False
143 self.selected = -1
144 listitems = []
145 for playlist in items:
146 listitems.append(xbmcgui.ListItem(playlist[0]))
b26b96e6 147 listitems.append(xbmcgui.ListItem(__language__(30011)))
052028f1 148 self.playlistControl.addItems(listitems)
149 self.setFocus(self.playlistControl)
150 self.playlistControl.selectItem(0)
151 item = self.playlistControl.getListItem(self.lastPos)
152 item.select(True)
153
154 # Highlight selected item
155 def setHighlight(self):
156 if self.isSelecting:
157 return
158 else:
159 self.isSelecting = True
160
161 pos = self.playlistControl.getSelectedPosition()
162 if pos >= 0:
163 item = self.playlistControl.getListItem(self.lastPos)
164 item.select(False)
165 item = self.playlistControl.getListItem(pos)
166 item.select(True)
167 self.lastPos = pos
168 self.isSelecting = False
169
170 # Control - select
171 def onControl(self, control):
172 if control == self.playlistControl:
173 self.selected = self.playlistControl.getSelectedPosition()
174 self.close()
175
176 # Action - close or up/down
177 def onAction(self, action):
178 if action == ACTION_PREVIOUS_MENU:
179 self.selected = -1
180 self.close()
181 elif action == ACTION_MOVE_UP or action == ACTION_MOVE_DOWN or action == ACTION_PAGE_UP or action == ACTION_PAGE_DOWN == 6:
182 self.setFocus(self.playlistControl)
183 self.setHighlight()
f95afae7 184
185
186class PlayTimer(Thread):
187 # interval -- floating point number specifying the number of seconds to wait before executing function
188 # function -- the function (or callable object) to be executed
189
190 # iterations -- integer specifying the number of iterations to perform
191 # args -- list of positional arguments passed to function
192 # kwargs -- dictionary of keyword arguments passed to function
193
194 def __init__(self, interval, function, iterations=0, args=[], kwargs={}):
195 Thread.__init__(self)
196 self.interval = interval
197 self.function = function
198 self.iterations = iterations
199 self.args = args
200 self.kwargs = kwargs
201 self.finished = Event()
202
203 def run(self):
204 count = 0
205 while not self.finished.isSet() and (self.iterations <= 0 or count < self.iterations):
206 self.finished.wait(self.interval)
207 if not self.finished.isSet():
208 self.function(*self.args, **self.kwargs)
209 count += 1
210
211 def cancel(self):
212 self.finished.set()
213
214 def setIterations(self, iterations):
215 self.iterations = iterations
216
217
218 def getTime(self):
219 return self.iterations * self.interval
220
221
8817bb2e 222class Groveshark:
973b4c6c 223
7ea6f166 224 albumImg = xbmc.translatePath(os.path.join(imgDir, 'album.png'))
225 artistImg = xbmc.translatePath(os.path.join(imgDir, 'artist.png'))
86f629ea 226 artistsAlbumsImg = xbmc.translatePath(os.path.join(imgDir, 'artistsalbums.png'))
7ea6f166 227 favoritesImg = xbmc.translatePath(os.path.join(imgDir, 'favorites.png'))
228 playlistImg = xbmc.translatePath(os.path.join(imgDir, 'playlist.png'))
86f629ea 229 usersplaylistsImg = xbmc.translatePath(os.path.join(imgDir, 'usersplaylists.png'))
7ea6f166 230 popularSongsImg = xbmc.translatePath(os.path.join(imgDir, 'popularSongs.png'))
97289139 231 popularSongsArtistImg = xbmc.translatePath(os.path.join(imgDir, 'popularSongsArtist.png'))
7ea6f166 232 songImg = xbmc.translatePath(os.path.join(imgDir, 'song.png'))
233 defImg = xbmc.translatePath(os.path.join(imgDir, 'default.tbn'))
2254a6b5 234 fanImg = xbmc.translatePath(os.path.join(baseDir, 'fanart.png'))
8817bb2e 235
2254a6b5 236 settings = xbmcaddon.Addon(id='plugin.audio.groove')
7ce01be6 237 songsearchlimit = int(settings.getSetting('songsearchlimit'))
238 albumsearchlimit = int(settings.getSetting('albumsearchlimit'))
239 artistsearchlimit = int(settings.getSetting('artistsearchlimit'))
97289139 240 songspagelimit = int(settings.getSetting('songspagelimit'))
2254a6b5 241 username = settings.getSetting('username')
242 password = settings.getSetting('password')
38df1fa5 243 userid = 0
4be42357 244
8817bb2e 245 def __init__( self ):
246 self._handle = int(sys.argv[1])
7ce01be6 247 if os.path.isdir(cacheDir) == False:
248 os.makedirs(cacheDir)
b26b96e6 249 xbmc.log(__language__(30012) + " " + cacheDir)
164e42d8 250 artDir = xbmc.translatePath(thumbDir)
251 if os.path.isdir(artDir) == False:
3a794693 252 os.makedirs(artDir)
b26b96e6 253 xbmc.log(__language__(30012) + " " + artDir)
052028f1 254
e278f474 255 # Top-level menu
8817bb2e 256 def categories(self):
2254a6b5 257
38df1fa5 258 self.userid = self._get_login()
b738088f 259
260 # Setup
6ae708d0 261 xbmcplugin.setPluginFanart(int(sys.argv[1]), self.fanImg)
6ae708d0 262
b26b96e6 263 self._add_dir(__language__(30013), '', MODE_SEARCH_SONGS, self.songImg, 0)
264 self._add_dir(__language__(30014), '', MODE_SEARCH_ALBUMS, self.albumImg, 0)
265 self._add_dir(__language__(30015), '', MODE_SEARCH_ARTISTS, self.artistImg, 0)
3d95dfcb 266 self._add_dir(searchArtistsAlbumsName, '', MODE_SEARCH_ARTISTS_ALBUMS, self.artistsAlbumsImg, 0)
f95afae7 267 # Not supported by key
268 #self._add_dir("Search for user's playlists...", '', MODE_SEARCH_PLAYLISTS, self.usersplaylistsImg, 0)
b26b96e6 269 self._add_dir(__language__(30016), '', MODE_ARTIST_POPULAR, self.popularSongsArtistImg, 0)
270 self._add_dir(__language__(30017), '', MODE_POPULAR_SONGS, self.popularSongsImg, 0)
38df1fa5 271 if (self.userid != 0):
b26b96e6 272 self._add_dir(__language__(30018), '', MODE_FAVORITES, self.favoritesImg, 0)
273 self._add_dir(__language__(30019), '', MODE_PLAYLISTS, self.playlistImg, 0)
e278f474 274
275 # Search for songs
8817bb2e 276 def searchSongs(self):
b26b96e6 277 query = self._get_keyboard(default="", heading=__language__(30020))
7ea6f166 278 if (query != ''):
7ce01be6 279 songs = groovesharkApi.getSongSearchResults(query, limit = self.songsearchlimit)
8817bb2e 280 if (len(songs) > 0):
6ae708d0 281 self._add_songs_directory(songs)
8817bb2e 282 else:
283 dialog = xbmcgui.Dialog()
b26b96e6 284 dialog.ok(__language__(30008), __language__(30021))
8817bb2e 285 self.categories()
7ea6f166 286 else:
287 self.categories()
8817bb2e 288
e278f474 289 # Search for albums
8817bb2e 290 def searchAlbums(self):
b26b96e6 291 query = self._get_keyboard(default="", heading=__language__(30022))
7ea6f166 292 if (query != ''):
7ce01be6 293 albums = groovesharkApi.getAlbumSearchResults(query, limit = self.albumsearchlimit)
8817bb2e 294 if (len(albums) > 0):
6ae708d0 295 self._add_albums_directory(albums)
8817bb2e 296 else:
297 dialog = xbmcgui.Dialog()
b26b96e6 298 dialog.ok(__language__(30008), __language__(30023))
8817bb2e 299 self.categories()
7ea6f166 300 else:
301 self.categories()
8817bb2e 302
e278f474 303 # Search for artists
8817bb2e 304 def searchArtists(self):
b26b96e6 305 query = self._get_keyboard(default="", heading=__language__(30024))
7ea6f166 306 if (query != ''):
7ce01be6 307 artists = groovesharkApi.getArtistSearchResults(query, limit = self.artistsearchlimit)
8817bb2e 308 if (len(artists) > 0):
6ae708d0 309 self._add_artists_directory(artists)
8817bb2e 310 else:
311 dialog = xbmcgui.Dialog()
b26b96e6 312 dialog.ok(__language__(30008), __language__(30025))
8817bb2e 313 self.categories()
7ea6f166 314 else:
315 self.categories()
86f629ea 316
317 # Search for playlists
318 def searchPlaylists(self):
b26b96e6 319 query = self._get_keyboard(default="", heading=__language__(30026))
86f629ea 320 if (query != ''):
f95afae7 321 playlists = groovesharkApi.getUserPlaylistsByUsername(query)
86f629ea 322 if (len(playlists) > 0):
323 self._add_playlists_directory(playlists)
324 else:
325 dialog = xbmcgui.Dialog()
b26b96e6 326 dialog.ok(__language__(30008), __language__(30027))
86f629ea 327 self.categories()
328 else:
329 self.categories()
330
331 # Search for artists albums
3d95dfcb 332 def searchArtistsAlbums(self, artistName = None):
333 if artistName == None or artistName == searchArtistsAlbumsName:
b26b96e6 334 query = self._get_keyboard(default="", heading=__language__(30028))
99f72740 335 else:
336 query = artistName
86f629ea 337 if (query != ''):
338 artists = groovesharkApi.getArtistSearchResults(query, limit = self.artistsearchlimit)
339 if (len(artists) > 0):
340 artist = artists[0]
341 artistID = artist[1]
342 xbmc.log("Found " + artist[0] + "...")
343 albums = groovesharkApi.getArtistAlbums(artistID, limit = self.albumsearchlimit)
344 if (len(albums) > 0):
052028f1 345 self._add_albums_directory(albums, artistID)
86f629ea 346 else:
347 dialog = xbmcgui.Dialog()
b26b96e6 348 dialog.ok(__language__(30008), __language__(30029))
86f629ea 349 self.categories()
350 else:
351 dialog = xbmcgui.Dialog()
b26b96e6 352 dialog.ok(__language__(30008), __language__(30030))
86f629ea 353 self.categories()
354 else:
355 self.categories()
356
e278f474 357 # Get my favorites
8817bb2e 358 def favorites(self):
36cc00d7 359 userid = self._get_login()
360 if (userid != 0):
7ce01be6 361 favorites = groovesharkApi.getUserFavoriteSongs()
36cc00d7 362 if (len(favorites) > 0):
052028f1 363 self._add_songs_directory(favorites, isFavorites=True)
36cc00d7 364 else:
365 dialog = xbmcgui.Dialog()
b26b96e6 366 dialog.ok(__language__(30008), __language__(30031))
36cc00d7 367 self.categories()
8817bb2e 368
e278f474 369 # Get popular songs
36cc00d7 370 def popularSongs(self):
7ce01be6 371 popular = groovesharkApi.getPopularSongsToday(limit = self.songsearchlimit)
8817bb2e 372 if (len(popular) > 0):
6ae708d0 373 self._add_songs_directory(popular)
8817bb2e 374 else:
375 dialog = xbmcgui.Dialog()
b26b96e6 376 dialog.ok(__language__(30008), __language__(30032))
8817bb2e 377 self.categories()
36cc00d7 378
e278f474 379 # Get my playlists
8817bb2e 380 def playlists(self):
381 userid = self._get_login()
382 if (userid != 0):
7ce01be6 383 playlists = groovesharkApi.getUserPlaylists()
8817bb2e 384 if (len(playlists) > 0):
6ae708d0 385 self._add_playlists_directory(playlists)
8817bb2e 386 else:
387 dialog = xbmcgui.Dialog()
b26b96e6 388 dialog.ok(__language__(30008), __language__(30033))
8817bb2e 389 self.categories()
7ea6f166 390 else:
391 dialog = xbmcgui.Dialog()
b26b96e6 392 dialog.ok(__language__(30008), __language__(30034), __language__(30035))
7ea6f166 393
e278f474 394 # Make songs a favorite
8817bb2e 395 def favorite(self, songid):
396 userid = self._get_login()
397 if (userid != 0):
406ab447 398 xbmc.log("Favorite song: " + str(songid))
7ce01be6 399 groovesharkApi.addUserFavoriteSong(songID = songid)
b26b96e6 400 xbmc.executebuiltin('XBMC.Notification(' + __language__(30008) + ', ' + __language__(30036) + ', 1000, ' + thumbDef + ')')
8817bb2e 401 else:
402 dialog = xbmcgui.Dialog()
b26b96e6 403 dialog.ok(__language__(30008), __language__(30034), __language__(30037))
052028f1 404
405 # Remove song from favorites
406 def unfavorite(self, songid, prevMode=0):
f95afae7 407 userid = self._get_login()
052028f1 408 if (userid != 0):
409 xbmc.log("Unfavorite song: " + str(songid) + ', previous mode was ' + str(prevMode))
f95afae7 410 groovesharkApi.removeUserFavoriteSongs(songIDs = songid)
b26b96e6 411 xbmc.executebuiltin('XBMC.Notification(' + __language__(30008) + ', ' + __language__(30038) + ', 1000, ' + thumbDef + ')')
052028f1 412 # Refresh to remove item from directory
413 if (int(prevMode) == MODE_FAVORITES):
414 xbmc.executebuiltin("Container.Refresh(" + favoritesUrl + ")")
415 else:
416 dialog = xbmcgui.Dialog()
b26b96e6 417 dialog.ok(__language__(30008), __language__(30034), __language__(30039))
052028f1 418
e278f474 419
420 # Show selected album
36cc00d7 421 def album(self, albumid):
7ce01be6 422 album = groovesharkApi.getAlbumSongs(albumid, limit = self.songsearchlimit)
86f629ea 423 self._add_songs_directory(album, trackLabelFormat=NAME_ALBUM_ARTIST_LABEL)
e278f474 424
425 # Show selected artist
8817bb2e 426 def artist(self, artistid):
7ce01be6 427 albums = groovesharkApi.getArtistAlbums(artistid, limit = self.albumsearchlimit)
052028f1 428 self._add_albums_directory(albums, artistid)
8817bb2e 429
e278f474 430 # Show selected playlist
e6f8730b 431 def playlist(self, playlistid, playlistname):
8817bb2e 432 userid = self._get_login()
433 if (userid != 0):
e6f8730b 434 songs = groovesharkApi.getPlaylistSongs(playlistid)
052028f1 435 self._add_songs_directory(songs, trackLabelFormat=NAME_ALBUM_ARTIST_LABEL, playlistid=playlistid, playlistname=playlistname)
8817bb2e 436 else:
437 dialog = xbmcgui.Dialog()
b26b96e6 438 dialog.ok(__language__(30008), __language__(30034), __language__(30040))
8817bb2e 439
97289139 440 # Show popular songs of the artist
441 def artistPopularSongs(self):
b26b96e6 442 query = self._get_keyboard(default="", heading=__language__(30041))
97289139 443 if (query != ''):
444 artists = groovesharkApi.getArtistSearchResults(query, limit = self.artistsearchlimit)
445 if (len(artists) > 0):
446 artist = artists[0]
447 artistID = artist[1]
448 xbmc.log("Found " + artist[0] + "...")
449 songs = groovesharkApi.getArtistPopularSongs(artistID, limit = self.songsearchlimit)
450 if (len(songs) > 0):
451 self._add_songs_directory(songs, trackLabelFormat=NAME_ALBUM_ARTIST_LABEL)
452 else:
453 dialog = xbmcgui.Dialog()
b26b96e6 454 dialog.ok(__language__(30008), __language__(30042))
97289139 455 self.categories()
456 else:
457 dialog = xbmcgui.Dialog()
b26b96e6 458 dialog.ok(__language__(30008), __language__(30043))
97289139 459 self.categories()
460 else:
461 self.categories()
462
e278f474 463 # Play a song
6ae708d0 464 def playSong(self, item):
f95afae7 465 global playTimer
466 global player
467 if item != None:
468 songid = item.getProperty('songid')
469 stream = groovesharkApi.getSubscriberStreamKey(songid)
470 url = stream['url']
471 item.setPath(url)
472 xbmc.log("Grooveshark playing: " + url)
7ce01be6 473 xbmcplugin.setResolvedUrl(handle=int(sys.argv[1]), succeeded=True, listitem=item)
f95afae7 474 # Wait for play then start time
475 seconds = 0
476 while seconds < STREAM_MARKING_TIME:
477 try:
478 if player.isPlayingAudio() == True:
479 if playTimer != None:
480 playTimer.cancel()
481 songMarkTime = 0
482 duration = int(item.getProperty('duration'))
483 playTimer = PlayTimer(1, markSong, duration, [songid, duration])
484 playTimer.start()
485 break
486 except: pass
487 time.sleep(1)
488 seconds = seconds + 1
7ce01be6 489 else:
b26b96e6 490 xbmc.executebuiltin('XBMC.Notification(' + __language__(30008) + ', ' + __language__(30044) + ', 1000, ' + thumbDef + ')')
f95afae7 491
e278f474 492 # Make a song directory item
86f629ea 493 def songItem(self, songid, name, album, artist, coverart, trackLabelFormat=ARTIST_ALBUM_NAME_LABEL):
7ce01be6 494 songImg = self._get_icon(coverart, 'song-' + str(songid) + "-image")
2cb26bea 495 if int(trackLabelFormat) == NAME_ALBUM_ARTIST_LABEL:
86f629ea 496 trackLabel = name + " - " + album + " - " + artist
497 else:
498 trackLabel = artist + " - " + album + " - " + name
f95afae7 499 duration = self._getSongDuration(songid)
86f629ea 500 item = xbmcgui.ListItem(label = trackLabel, thumbnailImage=songImg, iconImage=songImg)
f95afae7 501 item.setInfo( type="music", infoLabels={ "title": name, "album": album, "artist": artist, "duration": duration} )
2254a6b5 502 item.setProperty('mimetype', 'audio/mpeg')
503 item.setProperty("IsPlayable", "true")
7ce01be6 504 item.setProperty('songid', str(songid))
505 item.setProperty('coverart', songImg)
506 item.setProperty('title', name)
507 item.setProperty('album', album)
508 item.setProperty('artist', artist)
f95afae7 509 item.setProperty('duration', str(duration))
7ce01be6 510
6ae708d0 511 return item
2254a6b5 512
97289139 513 # Next page of songs
052028f1 514 def songPage(self, page, trackLabelFormat, playlistid = 0, playlistname = ''):
515 self._add_songs_directory([], trackLabelFormat, page, playlistid = playlistid, playlistname = playlistname)
516
517 # Make a playlist from an album
518 def makePlaylist(self, albumid, name):
f95afae7 519 userid = self._get_login()
052028f1 520 if (userid != 0):
521 re.split(' - ',name,1)
522 nameTokens = re.split(' - ',name,1) # suggested name
b26b96e6 523 name = self._get_keyboard(default=nameTokens[0], heading=__language__(30045))
052028f1 524 if name != '':
525 album = groovesharkApi.getAlbumSongs(albumid, limit = self.songsearchlimit)
526 songids = []
527 for song in album:
528 songids.append(song[1])
f95afae7 529 if groovesharkApi.createPlaylist(name, songids) == 0:
052028f1 530 dialog = xbmcgui.Dialog()
b26b96e6 531 dialog.ok(__language__(30008), __language__(30046), name)
052028f1 532 else:
b26b96e6 533 xbmc.executebuiltin('XBMC.Notification(' + __language__(30008) + ',' + __language__(30047)+ ', 1000, ' + thumbDef + ')')
052028f1 534 else:
535 dialog = xbmcgui.Dialog()
b26b96e6 536 dialog.ok(__language__(30008), __language__(30034), __language__(30048))
052028f1 537
538 # Rename a playlist
539 def renamePlaylist(self, playlistid, name):
f95afae7 540 userid = self._get_login()
052028f1 541 if (userid != 0):
b26b96e6 542 newname = self._get_keyboard(default=name, heading=__language__(30049))
052028f1 543 if newname == '':
544 return
f95afae7 545 elif groovesharkApi.playlistRename(playlistid, newname) == 0:
052028f1 546 dialog = xbmcgui.Dialog()
b26b96e6 547 dialog.ok(__language__(30008), __language__(30050), name)
052028f1 548 else:
549 # Refresh to show new item name
550 xbmc.executebuiltin("Container.Refresh")
551 else:
552 dialog = xbmcgui.Dialog()
b26b96e6 553 dialog.ok(__language__(30008), __language__(30034), __language__(30051))
052028f1 554
555 # Remove a playlist
556 def removePlaylist(self, playlistid, name):
557 dialog = xbmcgui.Dialog()
b26b96e6 558 if dialog.yesno(__language__(30008), name, __language__(30052)) == True:
f95afae7 559 userid = self._get_login()
052028f1 560 if (userid != 0):
f95afae7 561 if groovesharkApi.playlistDelete(playlistid) == 0:
052028f1 562 dialog = xbmcgui.Dialog()
b26b96e6 563 dialog.ok(__language__(30008), __language__(30053), name)
052028f1 564 else:
565 # Refresh to remove item from directory
566 xbmc.executebuiltin("Container.Refresh(" + playlistsUrl + ")")
567 else:
568 dialog = xbmcgui.Dialog()
b26b96e6 569 dialog.ok(__language__(30008), __language__(30034), __language__(30054))
052028f1 570
571 # Add song to playlist
572 def addPlaylistSong(self, songid):
f95afae7 573 userid = self._get_login()
052028f1 574 if (userid != 0):
575 playlists = groovesharkApi.getUserPlaylists()
576 if (len(playlists) > 0):
577 ret = 0
578 # Select the playlist
579 playlistSelect = GroovesharkPlaylistSelect(items=playlists)
580 playlistSelect.setFocus(playlistSelect.playlistControl)
581 playlistSelect.doModal()
582 i = playlistSelect.selected
583 del playlistSelect
584 if i > -1:
585 # Add a new playlist
586 if i >= len(playlists):
b26b96e6 587 name = self._get_keyboard(default='', heading=__language__(30055))
052028f1 588 if name != '':
589 songIds = []
590 songIds.append(songid)
f95afae7 591 if groovesharkApi.createPlaylist(name, songIds) == 0:
052028f1 592 dialog = xbmcgui.Dialog()
b26b96e6 593 dialog.ok(__language__(30008), __language__(30056), name)
052028f1 594 else:
b26b96e6 595 xbmc.executebuiltin('XBMC.Notification(' + __language__(30008) + ',' + __language__(30057) + ', 1000, ' + thumbDef + ')')
052028f1 596 # Existing playlist
597 else:
598 playlist = playlists[i]
599 playlistid = playlist[1]
600 xbmc.log("Add song " + str(songid) + " to playlist " + str(playlistid))
f95afae7 601 songIDs=[]
602 songs = groovesharkApi.getPlaylistSongs(playlistid)
603 for song in songs:
604 songIDs.append(song[1])
605 songIDs.append(songid)
606 ret = groovesharkApi.setPlaylistSongs(playlistid, songIDs)
607 if ret == False:
052028f1 608 dialog = xbmcgui.Dialog()
b26b96e6 609 dialog.ok(__language__(30008), __language__(30058))
052028f1 610 else:
b26b96e6 611 xbmc.executebuiltin('XBMC.Notification(' + __language__(30008) + ',' + __language__(30059) + ', 1000, ' + thumbDef + ')')
052028f1 612 else:
613 dialog = xbmcgui.Dialog()
b26b96e6 614 dialog.ok(__language__(30008), __language__(30060))
052028f1 615 self.categories()
616 else:
617 dialog = xbmcgui.Dialog()
b26b96e6 618 dialog.ok(__language__(30008), __language__(30034), __language__(30061))
052028f1 619
620 # Remove song from playlist
f95afae7 621 def removePlaylistSong(self, playlistid, playlistname, songid):
e6f8730b 622 dialog = xbmcgui.Dialog()
b26b96e6 623 if dialog.yesno(__language__(30008), __language__(30062), __language__(30063)) == True:
f95afae7 624 userid = self._get_login()
052028f1 625 if (userid != 0):
f95afae7 626 songs = groovesharkApi.getPlaylistSongs(playlistID)
627 songIDs=[]
628 for song in songs:
629 if (song[1] != songid):
630 songIDs.append(song[1])
631 ret = groovesharkApi.setPlaylistSongs(playlistID, songIDs)
632 if ret == False:
052028f1 633 dialog = xbmcgui.Dialog()
b26b96e6 634 dialog.ok(__language__(30008), __language__(30064), __language__(30065))
052028f1 635 else:
636 # Refresh to remove item from directory
b26b96e6 637 xbmc.executebuiltin('XBMC.Notification(' + __language__(30008) + ',' + __language__(30066)+ ', 1000, ' + thumbDef + ')')
e6f8730b 638 xbmc.executebuiltin("Container.Update(" + playlistUrl + "&id="+str(playlistid) + "&name=" + playlistname + ")")
052028f1 639 else:
640 dialog = xbmcgui.Dialog()
b26b96e6 641 dialog.ok(__language__(30008), __language__(30034), __language__(30067))
052028f1 642
643 # Find similar artists to searched artist
644 def similarArtists(self, artistId):
f95afae7 645 similar = groovesharkApi.getSimilarArtists(artistId, limit = self.artistsearchlimit)
052028f1 646 if (len(similar) > 0):
647 self._add_artists_directory(similar)
648 else:
649 dialog = xbmcgui.Dialog()
b26b96e6 650 dialog.ok(__language__(30008), __language__(30068))
052028f1 651 self.categories()
97289139 652
e278f474 653 # Get keyboard input
8817bb2e 654 def _get_keyboard(self, default="", heading="", hidden=False):
3cfead3c 655 kb = xbmc.Keyboard(default, heading, hidden)
656 kb.doModal()
657 if (kb.isConfirmed()):
658 return unicode(kb.getText(), "utf-8")
659 return ''
8817bb2e 660
e278f474 661 # Login to grooveshark
f95afae7 662 def _get_login(self):
2254a6b5 663 if (self.username == "" or self.password == ""):
8817bb2e 664 dialog = xbmcgui.Dialog()
b26b96e6 665 dialog.ok(__language__(30008), __language__(30069), __language__(30070))
8817bb2e 666 return 0
667 else:
38df1fa5 668 if self.userid == 0:
f95afae7 669 uid = groovesharkApi.login(self.username, self.password)
38df1fa5 670 if (uid != 0):
38df1fa5 671 return uid
8817bb2e 672 else:
673 dialog = xbmcgui.Dialog()
b26b96e6 674 dialog.ok(__language__(30008), __language__(30069), __language__(30070))
8817bb2e 675 return 0
676
e278f474 677 # Get a song directory item
86f629ea 678 def _get_song_item(self, song, trackLabelFormat):
6ae708d0 679 name = song[0]
7ce01be6 680 songid = song[1]
681 album = song[2]
682 artist = song[4]
683 coverart = song[6]
86f629ea 684 return self.songItem(songid, name, album, artist, coverart, trackLabelFormat)
6ae708d0 685
686 # File download
7ce01be6 687 def _get_icon(self, url, songid):
688 if url != 'None':
689 localThumb = os.path.join(xbmc.translatePath(os.path.join(thumbDir, str(songid)))) + '.tbn'
690 try:
691 if os.path.isfile(localThumb) == False:
692 loc = urllib.URLopener()
693 loc.retrieve(url, localThumb)
694 except:
695 shutil.copy2(thumbDef, localThumb)
696 return os.path.join(os.path.join(thumbDir, str(songid))) + '.tbn'
697 else:
698 return thumbDef
e278f474 699
700 # Add songs to directory
052028f1 701 def _add_songs_directory(self, songs, trackLabelFormat=ARTIST_ALBUM_NAME_LABEL, page=0, playlistid=0, playlistname='', isFavorites=False):
97289139 702
703 totalSongs = len(songs)
704 page = int(page)
705
706 # No pages needed
707 if page == 0 and totalSongs <= self.songspagelimit:
708 xbmc.log("Found " + str(totalSongs) + " songs...")
709 # Pages
710 else:
711 # Cache all songs
712 if page == 0:
713 self._setSavedSongs(songs)
714 else:
715 songs = self._getSavedSongs()
716 totalSongs = len(songs)
717
718 if totalSongs > 0:
719 start = page * self.songspagelimit
720 end = start + self.songspagelimit
721 songs = songs[start:end]
722
052028f1 723 id = 0
97289139 724 for song in songs:
86f629ea 725 item = self._get_song_item(song, trackLabelFormat)
7ce01be6 726 coverart = item.getProperty('coverart')
6ae708d0 727 songname = song[0]
728 songid = song[1]
7ce01be6 729 songalbum = song[2]
730 songartist = song[4]
f95afae7 731
732
e278f474 733 u=sys.argv[0]+"?mode="+str(MODE_SONG)+"&name="+urllib.quote_plus(songname)+"&id="+str(songid) \
6ae708d0 734 +"&album="+urllib.quote_plus(songalbum) \
735 +"&artist="+urllib.quote_plus(songartist) \
7ce01be6 736 +"&coverart="+urllib.quote_plus(coverart)
e278f474 737 fav=sys.argv[0]+"?mode="+str(MODE_FAVORITE)+"&name="+urllib.quote_plus(songname)+"&id="+str(songid)
052028f1 738 unfav=sys.argv[0]+"?mode="+str(MODE_UNFAVORITE)+"&name="+urllib.quote_plus(songname)+"&id="+str(songid)+"&prevmode="
6ae708d0 739 menuItems = []
052028f1 740 if isFavorites == True:
741 unfav = unfav +str(MODE_FAVORITES)
742 else:
b26b96e6 743 menuItems.append((__language__(30071), "XBMC.RunPlugin("+fav+")"))
744 menuItems.append((__language__(30072), "XBMC.RunPlugin("+unfav+")"))
f95afae7 745 if playlistid > 0:
746 rmplaylstsong=sys.argv[0]+"?playlistid="+str(playlistid)+"&id="+str(songid)+"&mode="+str(MODE_REMOVE_PLAYLIST_SONG)+"&name="+playlistname
b26b96e6 747 menuItems.append((__language__(30073), "XBMC.RunPlugin("+rmplaylstsong+")"))
f95afae7 748 else:
749 addplaylstsong=sys.argv[0]+"?id="+str(songid)+"&mode="+str(MODE_ADD_PLAYLIST_SONG)
b26b96e6 750 menuItems.append((__language__(30074), "XBMC.RunPlugin("+addplaylstsong+")"))
6ae708d0 751 item.addContextMenuItems(menuItems, replaceItems=False)
97289139 752 xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),url=u,listitem=item,isFolder=False, totalItems=len(songs))
052028f1 753 id = id + 1
97289139 754
755 page = page + 1
756 if totalSongs > page * self.songspagelimit:
052028f1 757 u=sys.argv[0]+"?mode="+str(MODE_SONG_PAGE)+"&id=playlistid"+"&page="+str(page)+"&label="+str(trackLabelFormat)+"&name="+playlistname
b26b96e6 758 self._add_dir(__language__(30075) + '...', u, MODE_SONG_PAGE, self.songImg, 0, totalSongs - (page * self.songspagelimit))
cb06c186 759
8817bb2e 760 xbmcplugin.setContent(self._handle, 'songs')
31731635 761 xbmcplugin.setPluginFanart(int(sys.argv[1]), self.fanImg)
e278f474 762
763 # Add albums to directory
052028f1 764 def _add_albums_directory(self, albums, artistid=0):
31731635 765 n = len(albums)
766 xbmc.log("Found " + str(n) + " albums...")
8817bb2e 767 i = 0
31731635 768 while i < n:
8817bb2e 769 album = albums[i]
770 albumArtistName = album[0]
771 albumName = album[2]
772 albumID = album[3]
2254a6b5 773 albumImage = self._get_icon(album[4], 'album-' + str(albumID))
31731635 774 self._add_dir(albumName + " - " + albumArtistName, '', MODE_ALBUM, albumImage, albumID, n)
8817bb2e 775 i = i + 1
f95afae7 776 # Not supported by key
777 #if artistid > 0:
778 # self._add_dir('Similar artists...', '', MODE_SIMILAR_ARTISTS, self.artistImg, artistid)
8817bb2e 779 xbmcplugin.setContent(self._handle, 'albums')
780 xbmcplugin.addSortMethod(self._handle, xbmcplugin.SORT_METHOD_ALBUM_IGNORE_THE)
31731635 781 xbmcplugin.setPluginFanart(int(sys.argv[1]), self.fanImg)
e278f474 782
783 # Add artists to directory
6ae708d0 784 def _add_artists_directory(self, artists):
31731635 785 n = len(artists)
786 xbmc.log("Found " + str(n) + " artists...")
8817bb2e 787 i = 0
31731635 788 while i < n:
8817bb2e 789 artist = artists[i]
790 artistName = artist[0]
791 artistID = artist[1]
31731635 792 self._add_dir(artistName, '', MODE_ARTIST, self.artistImg, artistID, n)
8817bb2e 793 i = i + 1
794 xbmcplugin.setContent(self._handle, 'artists')
795 xbmcplugin.addSortMethod(self._handle, xbmcplugin.SORT_METHOD_ARTIST_IGNORE_THE)
31731635 796 xbmcplugin.setPluginFanart(int(sys.argv[1]), self.fanImg)
e278f474 797
798 # Add playlists to directory
6ae708d0 799 def _add_playlists_directory(self, playlists):
31731635 800 n = len(playlists)
801 xbmc.log("Found " + str(n) + " playlists...")
8817bb2e 802 i = 0
31731635 803 while i < n:
8817bb2e 804 playlist = playlists[i]
805 playlistName = playlist[0]
806 playlistID = playlist[1]
86f629ea 807 dir = self._add_dir(playlistName, '', MODE_PLAYLIST, self.playlistImg, playlistID, n)
8817bb2e 808 i = i + 1
809 xbmcplugin.setContent(self._handle, 'files')
86f629ea 810 xbmcplugin.addSortMethod(self._handle, xbmcplugin.SORT_METHOD_LABEL)
31731635 811 xbmcplugin.setPluginFanart(int(sys.argv[1]), self.fanImg)
3fcef5ba 812
e278f474 813 # Add whatever directory
31731635 814 def _add_dir(self, name, url, mode, iconimage, id, items=1):
052028f1 815
97289139 816 if url == '':
817 u=sys.argv[0]+"?mode="+str(mode)+"&name="+urllib.quote_plus(name)+"&id="+str(id)
818 else:
819 u = url
8817bb2e 820 dir=xbmcgui.ListItem(name, iconImage=iconimage, thumbnailImage=iconimage)
b738088f 821 dir.setInfo( type="Music", infoLabels={ "title": name } )
052028f1 822
823 # Custom menu items
f95afae7 824 menuItems = []
825 if mode == MODE_ALBUM:
826 mkplaylst=sys.argv[0]+"?mode="+str(MODE_MAKE_PLAYLIST)+"&name="+name+"&id="+str(id)
b26b96e6 827 menuItems.append((__language__(30076), "XBMC.RunPlugin("+mkplaylst+")"))
f95afae7 828 # Broken rename/delete are broken in API
829 if mode == MODE_PLAYLIST:
830 rmplaylst=sys.argv[0]+"?mode="+str(MODE_REMOVE_PLAYLIST)+"&name="+urllib.quote_plus(name)+"&id="+str(id)
b26b96e6 831 menuItems.append((__language__(30077), "XBMC.RunPlugin("+rmplaylst+")"))
f95afae7 832 mvplaylst=sys.argv[0]+"?mode="+str(MODE_RENAME_PLAYLIST)+"&name="+urllib.quote_plus(name)+"&id="+str(id)
b26b96e6 833 menuItems.append((__language__(30078), "XBMC.RunPlugin("+mvplaylst+")"))
f95afae7 834 dir.addContextMenuItems(menuItems, replaceItems=False)
052028f1 835
31731635 836 return xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),url=u,listitem=dir,isFolder=True, totalItems=items)
97289139 837
838 def _getSavedSongs(self):
839 path = os.path.join(cacheDir, 'songs.dmp')
840 try:
841 f = open(path, 'rb')
842 songs = pickle.load(f)
843 f.close()
844 except:
845 songs = []
846 pass
847 return songs
848
849 def _setSavedSongs(self, songs):
850 try:
851 # Create the 'data' directory if it doesn't exist.
852 if not os.path.exists(cacheDir):
853 os.makedirs(cacheDir)
854 path = os.path.join(cacheDir, 'songs.dmp')
855 f = open(path, 'wb')
856 pickle.dump(songs, f, protocol=pickle.HIGHEST_PROTOCOL)
857 f.close()
858 except:
859 xbmc.log("An error occurred saving songs")
860 pass
f95afae7 861
862 def _getSongDuration(self, songid):
863 path = os.path.join(cacheDir, 'duration.dmp')
864 id = int(songid)
865 durations = []
866 duration = -1
867
868 # Try cache first
869 try:
870 f = open(path, 'rb')
871 durations = pickle.load(f)
872 for song in durations:
873 if song[0] == id:
874 duration = song[1]
875 f.close()
876 except:
877 pass
878
879 if duration < 0:
880 stream = groovesharkApi.getSubscriberStreamKey(songid)
881 usecs = stream['uSecs']
882 if usecs < 60000000:
883 usecs = usecs * 10 # Some durations are 10x to small
884 duration = usecs / 1000000
885 song = [id, duration]
886 durations.append(song)
887 self._setSongDuration(durations)
888
889 return duration
890
891 def _setSongDuration(self, durations):
892 try:
893 # Create the 'data' directory if it doesn't exist.
894 if not os.path.exists(cacheDir):
895 os.makedirs(cacheDir)
896 path = os.path.join(cacheDir, 'duration.dmp')
897 f = open(path, 'wb')
898 pickle.dump(durations, f, protocol=pickle.HIGHEST_PROTOCOL)
899 f.close()
900 except:
901 xbmc.log("An error occurred saving durations")
902 pass
903
97289139 904
e278f474 905# Parse URL parameters
8817bb2e 906def get_params():
907 param=[]
908 paramstring=sys.argv[2]
e278f474 909 xbmc.log(paramstring)
8817bb2e 910 if len(paramstring)>=2:
911 params=sys.argv[2]
912 cleanedparams=params.replace('?','')
913 if (params[len(params)-1]=='/'):
914 params=params[0:len(params)-2]
915 pairsofparams=cleanedparams.split('&')
916 param={}
917 for i in range(len(pairsofparams)):
918 splitparams={}
919 splitparams=pairsofparams[i].split('=')
920 if (len(splitparams))==2:
921 param[splitparams[0]]=splitparams[1]
8817bb2e 922 return param
923
e278f474 924# Main
8817bb2e 925grooveshark = Groveshark();
e278f474 926
8817bb2e 927params=get_params()
8817bb2e 928mode=None
8817bb2e 929try: mode=int(params["mode"])
930except: pass
7ce01be6 931id=0
932try: id=int(params["id"])
933except: pass
052028f1 934name = None
935try: name=urllib.unquote_plus(params["name"])
936except: pass
e278f474 937
938# Call function for URL
8817bb2e 939if mode==None:
940 grooveshark.categories()
941
942elif mode==MODE_SEARCH_SONGS:
943 grooveshark.searchSongs()
944
945elif mode==MODE_SEARCH_ALBUMS:
946 grooveshark.searchAlbums()
947
948elif mode==MODE_SEARCH_ARTISTS:
949 grooveshark.searchArtists()
86f629ea 950
951elif mode==MODE_SEARCH_ARTISTS_ALBUMS:
a2e75b14 952 grooveshark.searchArtistsAlbums(name)
86f629ea 953
954elif mode==MODE_SEARCH_PLAYLISTS:
955 grooveshark.searchPlaylists()
8817bb2e 956
36cc00d7 957elif mode==MODE_POPULAR_SONGS:
958 grooveshark.popularSongs()
97289139 959
960elif mode==MODE_ARTIST_POPULAR:
a2e75b14 961 grooveshark.artistPopularSongs()
8817bb2e 962
8817bb2e 963elif mode==MODE_FAVORITES:
964 grooveshark.favorites()
965
e278f474 966elif mode==MODE_PLAYLISTS:
967 grooveshark.playlists()
97289139 968
969elif mode==MODE_SONG_PAGE:
970 try: page=urllib.unquote_plus(params["page"])
971 except: pass
972 try: label=urllib.unquote_plus(params["label"])
973 except: pass
052028f1 974 grooveshark.songPage(page, label, id, name)
e278f474 975
8817bb2e 976elif mode==MODE_SONG:
8817bb2e 977 try: album=urllib.unquote_plus(params["album"])
978 except: pass
979 try: artist=urllib.unquote_plus(params["artist"])
980 except: pass
7ce01be6 981 try: coverart=urllib.unquote_plus(params["coverart"])
8817bb2e 982 except: pass
7ce01be6 983 song = grooveshark.songItem(id, name, album, artist, coverart)
3fcef5ba 984 grooveshark.playSong(song)
8817bb2e 985
986elif mode==MODE_ARTIST:
4be42357 987 grooveshark.artist(id)
8817bb2e 988
989elif mode==MODE_ALBUM:
4be42357 990 grooveshark.album(id)
8817bb2e 991
992elif mode==MODE_PLAYLIST:
e6f8730b 993 grooveshark.playlist(id, name)
8817bb2e 994
995elif mode==MODE_FAVORITE:
4be42357 996 grooveshark.favorite(id)
97289139 997
052028f1 998elif mode==MODE_UNFAVORITE:
999 try: prevMode=int(urllib.unquote_plus(params["prevmode"]))
1000 except:
1001 prevMode = 0
1002 grooveshark.unfavorite(id, prevMode)
1003
1004elif mode==MODE_SIMILAR_ARTISTS:
1005 grooveshark.similarArtists(id)
1006
1007elif mode==MODE_MAKE_PLAYLIST:
1008 grooveshark.makePlaylist(id, name)
1009
1010elif mode==MODE_REMOVE_PLAYLIST:
1011 grooveshark.removePlaylist(id, name)
1012
1013elif mode==MODE_RENAME_PLAYLIST:
1014 grooveshark.renamePlaylist(id, name)
1015
1016elif mode==MODE_REMOVE_PLAYLIST_SONG:
1017 try: playlistID=urllib.unquote_plus(params["playlistid"])
1018 except: pass
1019 grooveshark.removePlaylistSong(playlistID, name, id)
1020
1021elif mode==MODE_ADD_PLAYLIST_SONG:
1022 grooveshark.addPlaylistSong(id)
1023
e278f474 1024if mode < MODE_SONG:
8817bb2e 1025 xbmcplugin.endOfDirectory(int(sys.argv[1]))