Browse files

initial commit.

  • Loading branch information...
simon-weber committed Jan 20, 2012
0 parents commit 880fcb3063e5ed3ba7179f1a9c8fda8c41f1dd6a
Showing with 216 additions and 0 deletions.
  1. +20 −0 README
  2. +174 −0
  3. +22 −0 protocol_info
@@ -0,0 +1,20 @@
A Python API for Google Music.
This is very much a work in progress. It currently supports:
searching for songs
creating playlists
adding songs to playlists
Check protocol-info for more detailed specifications.
There are two classes in GM_API and GM_Communicator.
GM_API is probably what you want to use; it has convenient abstractions of the implemented API calls.
GM_Communicator handles the plumbing for the API. You shouldn't need to use this if everything is working.
Copyright (c) 2012 Simon Weber
Licensed under the GPLv3:
@@ -0,0 +1,174 @@
#!/usr/bin/env python
import mechanize
import cookielib
import exceptions
import urllib
import urllib2
import os
import json
import inspect
from urllib2 import *
from urlparse import *
from functools import wraps
from prompt import prompt #For dropping into a prompt when debugging
#Self explanatory exceptions.
class AlreadyLoggedIn(exceptions.Exception):
class NotLoggedIn(exceptions.Exception):
class GM_API:
""" Contains functional abstractions of API calls."""
def __init__(self):
self.comm = GM_Communicator()
def login(self, email, password):
return self.comm.login(email, password)
def logout(self):
return self.comm.logout()
def api_call(json_builder):
"""Decorator for building API calls."""
@wraps(json_builder) #Preserve docstrings.
def wrapped(self = None, *args):
res = self.comm.make_request(json_builder.__name__, json_builder(*args))
return json.loads(
return wrapped
#API calls.
#Calls added properly here should be automatically supported. The body of the function simply builds the python representation of the json query, and the decorator handles the rest. The name of the function needs to be the same as it will be in the url.
#They should also have params in the docstring, since args (presently) won't be preserved by the decorator. The decorator module fixes this, but I'd rather not introduce another dependency.
def search(query):
"""Search for songs, artists and albums.
query: the search query."""
return {"q": query}
def addplaylist(title):
"""Create a new playlist.
title: the title of the playlist to create."""
return {"title": title}
def addtoplaylist(playlist_id, song_ids):
"""Add songs to a playlist.
playlist_id: id of the playlist to add to.
song_ids: a list of song ids, or a single song id."""
#We require a list. If a string is passed, wrap it in a list.
if isinstance(song_ids, basestring):
song_ids = [song_ids]
return {"playlistId": playlist_id, "songIds": song_ids}
class GM_Communicator:
""" Enables low level communication with Google Music."""
_base_url = ''
_user_agent = "Mozilla/5.0 (X11; U; Linux i686; en-US; rv: Gecko/20061201 Firefox/ (Ubuntu-feisty)"
def __init__(self):
#This cookie jar holds our session.
self._cookie_jar = cookielib.LWPCookieJar()
self.logged_in = False
def login(self, email, password):
if self.logged_in:
raise AlreadyLoggedIn
#Faking Google auth is tricky business, and it's easiest just to emulate a browser; there are fields filled in by javascript when a user submits, for example.
#This code modified from here:
br = mechanize.Browser()
# Browser options
# Follows refresh 0 but doesn't hang on refresh > 0
br.set_handle_refresh(mechanize._http.HTTPRefreshProcessor(), max_time=1)
# Google auth requires a common user-agent.
br.addheaders = [('User-agent', self._user_agent)]
r ='')
auth_page =
self.logged_in = True if self.get_cookie("SID") else False
return self.logged_in
def logout(self):
self._cookie_jar = cookielib.CookieJar()
self.logged_in = False
def make_request(self, call, data):
"""Make a single request to Google Music and return the response for reading.
call: the name of the service, eg 'search'
data: Python representation of the json query"""
if not self.logged_in:
raise NotLoggedIn
xt_val = self.get_cookie("xt").value
#The url passes u=0 and the xt cookie's value. Not sure what the u is for.
url = self._base_url + call + '?u=0&xt=' + xt_val
#GM needs the input to be named json.
encoded_data = "json=" + urllib.quote_plus(json.dumps(data))
return self.open_https_url(url, encoded_data)
def open_https_url(self, target_url, encoded_data = None):
"""Open an https url using our Google session.
target_url: full https url to open
encoded_data: optional, encoded POST data"""
#Code adapted from:
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self._cookie_jar))
opener.addheaders = [('User-agent', self._user_agent)]
response = None
if encoded_data:
response =, encoded_data)
response =
return response
def get_cookie(self, name):
"""Find a cookie by name from the cookie jar."""
for cookie in self._cookie_jar:
if == name:
return cookie
return None
@@ -0,0 +1,22 @@
Protocol Information:
Calls are made to<call name>
Calls also need to send 'u' (I've only seen it =0) and 'xt' (=the same as the cookie) in the url. A sample url:<long number>
== Implemented Calls ==
req: {"q": "<query>"}
res: {"results":{"artists":[<hits>],"albums":[<hits>],"songs":[<hits>]}}
req: {"title": "<name>"}
res: {"id":"<new playlist id>","title":"<name>","success":true}
req: {"playlistId":"<playlist to add to>","songIds":["<songs to add>"]}
res: {"playlistId":"<same as above>","songIds":[{"playlistEntryId":"<new id>","songId":"<same as above>"}]}

0 comments on commit 880fcb3

Please sign in to comment.