An Update on Nisme
I've been extremely busy lately starting up APPCON, and I've neglected Nisme. I'm sorry about this, and with the recent launch of Google and Lala's partnership, I see an even greater need for an awesome desktop player for Lala.
I started Nisme almost a year ago, and back then it was pretty much what it is today. A pet project aimed at increasing my knowledge of C#. I have started thinking more and more about what Nisme can and cannot be. I've invariably gotten thousands (yes, litterally THOUSANDS) of emails asking me to make an app that either downloads mp3's, or some how circumvents the protection that Lala has put in place. I'll state this one time and one time only.
NO.
Now that that is out of the way, let's move on to what I have planned for Nisme. My goal over the next few months is to clean up the code and make the application more stable. I've never had the intention of adding the market into the player, as I think lala has already done an amazing job of that, and I have no intention of reinventing the wheel. Nisme was meant to be an enriched player for your music that lala hosts. Nothing more. I think it has done a wonderful job of doing that already, and I also think it has quite a ways to go before I consider it "complete".
Up until now I have never really defined what Nisme was and was not. I am going to start doing that, and you will soon see several blog posts in the near future detailing what new features I have in store for Nisme.
One final thing.
Alot of people have asked me what Nisme means. Nisme is the Sumerian word for "Hear", cleverly thought of by a close friend of mine, Adenansu (http://adenansu.com/). I hope I didn't crush any dreams of it meaning something more mystical, but hey, you get what you pay for, amirite?
A look inside Nisme: Part 2
When I first started develolping Nisme, there was no clearly defined goal. Really my whole idea was to simply learn how the site functioned and to learn the proper use of JSON in the mean time.
It took me down a very slippery slope.
In this post, I'm going to talk about a small chunk of how I actually came across the raw MP3 stream that Lala sends to it's flash player. I started with the simple idea (that I stand behind today):
If I can do it in a browser, I can do it progmatically.
This took me to follow the request through lala's many layers of Javascript, and backend calls. I first found that there seemed to be something missing. It seemed that there was one call to:
playUrl
that didn't really go anywhere. It just executed. So I took my time, and after all options were exahusted, I looked in the unlikeliest of places. I figured that the flash player was simply a dummy control for the javascript behind it. I was wrong.
Inside the .swf was the actual player. They were hashing a JSON value called playToken along with an extremely long and foreboding "don't copy this stream or we'll cut off your neck and spit down your throat" copyright warning. Once I found that, I started passing it along to another URL that sent back a play URL. THe problem with these play URL's was that they were only good for one play. That's right, the second time you requested them you got a nice little 404 error.
Secondly, and this wasn't added until later, they check for the presence of your login cookie as well. This small fact has been the cause of many headaches in the Nisme development tree. It's extremely simple to play an MP3 url from a remote source on lots of platforms, however, sending a header value first proved difficult to say the least.
As far as source code goes, here is the actual retrieval of the playLink:
public String PlayLink
{
get
{
string hash = this.PlayToken + "Warning: Unauthorized reproduction, capture or distribution of this stream can result in civil and criminal liability. This stream and its content is owned by la la media, inc. and/or its licensors, and is protected by applicable intellectual property and other laws, including but not limited to copyright. To prevent unauthorized and infringing uses of this stream, generation of an MD5 digest with this text is required and will be used for monitoring and tracking unauthorized and infringing activity." + "abs123";
System.Security.Cryptography.MD5CryptoServiceProvider x = new System.Security.Cryptography.MD5CryptoServiceProvider();
byte[] data = System.Text.Encoding.ASCII.GetBytes(hash);
data = x.ComputeHash(data);
string ret = "";
for (int i = 0; i < data.Length; i++)
ret += data[i].ToString("x2").ToLower();
string md5t = ret;
string T = this.PlayToken;
T = T.Replace("-", "%2D").Replace("=", "%3D").Replace("+", "%2B").Replace("/", "%2F");
string TS = this.Timestamp;
string URL = "http://www.lala.com/api/Player/getPlaybackUrl?T=" + T + "&webSrc=lala&md5T=" + md5t + "&flash=true&TS=" + TS + "&debugPagePath=bigdPlayer&xml=true";
string xml = API.HTTPRequests.GetData(URL);
XmlDocument xdoc = new XmlDocument();
xdoc.LoadXml(xml);
XmlNodeList result = xdoc.GetElementsByTagName("url");
return result[0].InnerText;
}
}
Of course all of this is online at the repo locate at http://nisme.googlecode.com/
A Look Inside Nisme: Part 1
For those of you who don't know, Nisme is being developed with out any offical support from Lala. They don't have an official API, and the ones that I'm using are the exact same ones their site uses to provide the service.
This does pose a very large problem. What happens if Lala changes up their API? Well that has come up in my development brainstorming sessions, however, I think I've come up with quite a few "good-enough" ideas.
This leads me to my next topic. I am going to start a small mini-series on key components of Nisme. That way, you can see how I've interacted with Lala's data, and also give pointers if you see any glaring issues! So, for Part 1, I'm going to show you how I keep the authentication with lala through out the session.
public static bool GetLoginCookie(string username, string password)
{
string URL = "https://www.lala.com/api/User/signin/" + API.Functions.CurrentLalaVersion() + "?email=" + username + "&userpwd=" + password + "&xml=true";
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(URL);
request.CookieContainer = new CookieContainer();
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
string plate = "uiFc=homeNoAuthBSignin; ";
foreach (Cookie cook in response.Cookies)
{
plate = plate + cook.ToString() + "; ";
}
Stream dataStream = response.GetResponseStream();
StreamReader reader = new StreamReader(dataStream);
string responseFromServer = reader.ReadToEnd();
reader.Close();
dataStream.Close();
response.Close();
XmlDocument xDoc = new XmlDocument();
xDoc.LoadXml(responseFromServer);
XmlNodeList id = xDoc.GetElementsByTagName("userToken");
XmlNodeList err = xDoc.GetElementsByTagName("errorCode");
string UserID = id[0].InnerText;
string ErrorCode = err[0].InnerText;
if (ErrorCode != String.Empty)
{
return false;
}
else
{
API.Constants.Cookie = plate;
API.Constants.UserID = UserID;
return true;
}
}
Let's pull this apart for a second.
string URL = "https://www.lala.com/api/User/signin/" + API.Functions.CurrentLalaVersion() + "?email=" + username + "&userpwd=" + password + "&xml=true"; HttpWebRequest request = (HttpWebRequest)WebRequest.Create(URL); request.CookieContainer = new CookieContainer(); HttpWebResponse response = (HttpWebResponse)request.GetResponse();
These lines are pretty simple. They setup a RESTful API call to lala's User.signIn module. It's important to note that Lala by default will return JSON formatted values. While talking with a developer at Lala, I was informed that by appending <pre>&xml=true</pre> to the end of the query string, I can
get an xml formated value back.
I've flopped between JSON and XML through out Nisme's lifecycle, and the code shows it. In some of my methods, I use extremely hacked together raw string parsing to get my values, later on, I switched to XML, and with the WPF version, I'm back to JSON. More than likely I'll be sticking with JSON, simply because I've found a library that I'm comfortable working with, and JSON is just more lightweight in my opinion.
string plate = "uiFc=homeNoAuthBSignin; ";
foreach (Cookie cook in response.Cookies)
{
plate = plate + cook.ToString() + "; ";
}
For some reason, and don't ask me why, the <pre>plate</pre> has to be appended manually. I couldn't find in what part of the stack lala set this cookie, but it was always set to <pre>homeNoAuthBSignin</pre> so I figured it wouldn't hurt to manually force it. The next couple of lines are pretty self explanitory, as they simply loop through all the retrieved cookies and append them to <pre>plate</pre>.
Stream dataStream = response.GetResponseStream(); StreamReader reader = new StreamReader(dataStream); string responseFromServer = reader.ReadToEnd(); reader.Close(); dataStream.Close(); response.Close();
Here we are retrieving the actual data of the response. Up until now, we've only been parsing header values. We'll do a bit of garbage collection by closing all the uneeded resources as well.
XmlDocument xDoc = new XmlDocument();
xDoc.LoadXml(responseFromServer);
XmlNodeList id = xDoc.GetElementsByTagName("userToken");
XmlNodeList err = xDoc.GetElementsByTagName("errorCode");
string UserID = id[0].InnerText;
string ErrorCode = err[0].InnerText;
Again, pretty simple. We create a new <pre>XmlDocument</pre> and parse out the <pre>userToken</pre> and <pre>errorCode</pre> fields. The <pre>userToken</pre> field is only used currently for the naming of the Nisme Library File (.NLF). The <pre>errorCode</pre> is only used to detect if we had a successful login.
I guess in hind sight, it doesn't make sense to parse the headers if the login wasn't successful, now does it? (This will get changed in a later version)
if (ErrorCode != String.Empty)
{
return false;
}
else
{
API.Constants.Cookie = plate;
API.Constants.UserID = UserID;
return true;
}
Here's the actual logic check, and the assignment to the API.Constants singletons', plate and UserID.
So that's it! Each call that is made to lala after this, has the API.Constants.Cookie sent within it's header, and the rest is the magic of HTTP!