Skip to content
This repository has been archived by the owner on Jan 22, 2022. It is now read-only.

Commit

Permalink
Add support for free radio stations (#561)
Browse files Browse the repository at this point in the history
  • Loading branch information
foreverguest authored and simon-weber committed Jun 29, 2017
1 parent 2d3611f commit 94622f2
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 1 deletion.
2 changes: 2 additions & 0 deletions docs/source/reference/mobileclient.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Note that sometimes they are stored under the ``'nid'`` key, not the ``'id'`` ke
.. automethod:: Mobileclient.increment_song_playcount
.. automethod:: Mobileclient.add_store_track
.. automethod:: Mobileclient.add_store_tracks
.. automethod:: Mobileclient.get_station_track_stream_url

Playlists
---------
Expand Down Expand Up @@ -114,6 +115,7 @@ Search Google Play for information about artists, albums, tracks, and more.
.. automethod:: Mobileclient.get_podcast_episode_info
.. automethod:: Mobileclient.get_podcast_series_info
.. automethod:: Mobileclient.get_track_info
.. automethod:: Mobileclient.get_station_info

Misc
----
Expand Down
120 changes: 120 additions & 0 deletions gmusicapi/clients/mobileclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,28 @@ def get_stream_url(self, song_id, device_id=None, quality='hi'):

return self._make_call(mobileclient.GetStreamUrl, song_id, device_id, quality)

def get_station_track_stream_url(self, song_id, wentry_id, session_token, quality='hi'):
"""Returns a url that will point to an mp3 file.
This is only for use by free accounts, and requires a call to
:func:`get_station_info` first to provide `wentry_id` and `session_token`.
Subscribers should instead use :func:`get_stream_url`.
:param song_id: a single song id
:param wentry_id: a free radio station track entry id (`wentryid` from
:func:`get_station_info`)
:param session_token: a free radio station session token (`sessionToken` from
:func:`get_station_info`)
:param quality: (optional) stream bits per second quality
One of three possible values, hi: 320kbps, med: 160kbps, low: 128kbps.
The default is hi
"""
return self._make_call(mobileclient.GetStationTrackStreamUrl, song_id, wentry_id,
session_token, quality)

def get_all_playlists(self, incremental=False, include_deleted=None, updated_after=None):

"""Returns a list of dictionaries that each represent a playlist.
:param incremental: if True, return a generator that yields lists
Expand Down Expand Up @@ -2071,6 +2092,105 @@ def get_track_info(self, store_track_id):

return self._make_call(mobileclient.GetStoreTrack, store_track_id)

@utils.enforce_id_param
def get_station_info(self, station_id, num_tracks=25):
"""Retrieves information about a station.
:param station_id: a station id
:param include_tracks: when True, create the ``'tracks'`` substructure
:param num_tracks: maximum number of tracks to return
Returns a dict, eg::
{
'kind':'sj#radioStation',
'byline':'By Google Play Music',
'name':'Neo Soul',
'compositeArtRefs':[
{
'url':'http://lh3.googleusercontent.com/Aa-WBVTbKegp6McwqeHlj6KX5EYHKOBag74uwNl4xIHSv1g7Mi-NkMzwig',
'kind':'sj#imageRef',
'aspectRatio':'2'
} ],
'deleted':False,
'enforcementResult':{
'sessionInvalidated':False
},
'lastModifiedTimestamp':'1497410517701000',
'recentTimestamp':'1497410516945000',
'clientId':'9e66e89e-50b0-11e7-aaa3-bc5ff4545c4b',
'sessionToken':'AFTSR9PtB_PbyqZ3jsnl-PFma4upK1MEtlhVnIlxRNynGalctoJF4TpgzaxymOnk0Gv5DQG7gb_W3eLamPU_Mg1cWylhrowQi1EFMBKWHeDDYWzpU1cEOF-D3c_gnwsBRHIuOetph2veY2Fd-dKVzjOkN6mtidE-XPR2VnpR9PG83wRLVRtJq5593-Vvbu6wjCHD9f23ohxg-ki0tyD3fjFW1463zy63YzN5Aa2SpbvOskEWhwhS3u9ASgEoX08lePE-ZZAq1XtmVvLa8DnDMVb7i95Qhp0dM2it1uruKHH85u7tMYnttbAW4022d0rqrp3ULDKOYMvIIouXH44-bkbKLuVIADiqeNavwTVzcoJxWo4mMKjCaxM=',
'tracks':[
{
'albumArtRef':[
{
'url':'http://lh5.ggpht.com/vWRj9DkKZ7cFj-qXoGoBGsv7ngUWdtGNl1SSOdzj2efDwdAs3F0kJ3Xq6zLxKjgv1v3ive5S',
'kind':'sj#imageRef',
'aspectRatio':'1',
'autogen':False
}
],
'artistId':[
'Atmjrctnubes5zhftrey2xjkzl'
],
'composer':'',
'year':1996,
'trackAvailableForSubscription':True,
'trackType':'7',
'album':u"Maxwell's Urban Hang Suite",
'title':u"Ascension (Don't Ever Wonder)",
'albumArtist':'Maxwell',
'trackNumber':4,
'discNumber':1,
'albumAvailableForPurchase':False,
'explicitType':'2',
'trackAvailableForPurchase':True,
'storeId':'T6utayayrlyfmpovgj4ulacpat',
'nid':'T6utayayrlyfmpovgj4ulacpat',
'estimatedSize':'13848059',
'albumId':'Bpwzztxynfjwtnrtgiugem3b56e',
'genre':'Neo-Soul',
'kind':'sj#track',
'primaryVideo':{
'kind':'sj#video',
'id':'D7rm9t5S4uE',
'thumbnails':[
{
'url':'https://i.ytimg.com/vi/D7rm9t5S4uE/mqdefault.jpg',
'width':320,
'height':180
}
]
},
'artist':'Maxwell',
'wentryid':'ec9428eb-2676-4e92-901d-2de9a72fe581',
'durationMillis':'346000'
}
],
'seed':{
'kind':'sj#radioSeed',
'curatedStationId':'L3lu7bpcqtd3e7pa7w37rf7gdu',
'seedType':'9'
},
'skipEventHistory':[
],
'inLibrary':False,
'imageUrls':[
{
'url':'http://lh3.googleusercontent.com/iceDDsQjQ683AD4w21WWlekg115Ixy_kMTivkFJTjo3w7vuW4-SSs3F3KQOaR8qoI-QYVuOQoA',
'kind':'sj#imageRef',
'aspectRatio':'1',
'autogen':False
}
],
'id':'1a9ec96c-6c98-3c43-b123-9e2743203f5d'
}
"""
res = self._make_call(mobileclient.ListStationTracks, station_id, num_tracks, [])

return res.get('data', {'stations': [{}]})['stations'][0]

def get_genres(self, parent_genre_id=None):
"""Retrieves information on Google Music genres.
Expand Down
47 changes: 46 additions & 1 deletion gmusicapi/protocol/mobileclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@
}
}

sj_station_track = sj_track.copy()
sj_station_track['properties']['wentryid'] = {'type': 'string', 'required': False}

sj_station = {
'type': 'object',
'additionalProperties': False,
Expand All @@ -343,14 +346,16 @@
'required': False}, # for public
'clientId': {'type': 'string',
'required': False}, # for public
'sessionToken': {'type': 'string',
'required': False}, # for free radios
'skipEventHistory': {'type': 'array'}, # TODO: What's in this array?
'seed': sj_station_seed,
'stationSeeds': {'type': 'array',
'items': sj_station_seed},
'id': {'type': 'string',
'required': False}, # for public
'description': {'type': 'string', 'required': False},
'tracks': {'type': 'array', 'required': False, 'items': sj_track},
'tracks': {'type': 'array', 'required': False, 'items': sj_station_track},
'imageUrls': {'type': 'array',
'required': False,
'items': sj_image
Expand Down Expand Up @@ -897,6 +902,46 @@ class GetStreamUrl(McStreamCall):
static_url = sj_stream_url + 'mplay'


class GetStationTrackStreamUrl(McStreamCall):
static_method = 'GET'
static_url = sj_stream_url + 'wplay'

@staticmethod
def dynamic_headers(item_id, wentry_id, session_token, quality):
return {'X-Device-ID': ''}

@classmethod
def dynamic_params(cls, song_id, wentry_id, session_token, quality):
sig, salt = cls.get_signature(song_id)

params = {}
if song_id[0] == 'T':
# all access
params['mjck'] = song_id
else:
params['songid'] = song_id

params['sesstok'] = session_token.encode('utf-8')
params['wentryid'] = wentry_id.encode('utf-8')
params['tier'] = 'fr'

params.update(
{'audio_formats': 'mp3',
'opt': quality,
'net': 'mob',
'pt': 'a',
'slt': salt,
'sig': sig,
})

return params

@staticmethod
def parse_response(response):
res = json.loads(response.text)
return res['location']


class ListPlaylists(McListCall):
item_schema = sj_playlist
filter_text = 'playlists'
Expand Down

0 comments on commit 94622f2

Please sign in to comment.