PageRenderTime 27ms CodeModel.GetById 33ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Libraries/Lastfm/Lastfm/LastfmRequest.cs

https://github.com/dufoli/banshee
C# | 376 lines | 276 code | 64 blank | 36 comment | 53 complexity | d3d3027b58a656dd7f5dd25c81949c53 MD5 | raw file
  1. //
  2. // LastfmRequest.cs
  3. //
  4. // Authors:
  5. // Bertrand Lorentz <bertrand.lorentz@gmail.com>
  6. // Phil Trimble <philtrimble@gmail.com>
  7. // Andres G. Aragoneses <knocte@gmail.com>
  8. //
  9. // Copyright (C) 2009 Bertrand Lorentz
  10. // Copyright (C) 2013 Phil Trimble
  11. // Copyright (C) 2013 Andres G. Aragoneses
  12. //
  13. // Permission is hereby granted, free of charge, to any person obtaining
  14. // a copy of this software and associated documentation files (the
  15. // "Software"), to deal in the Software without restriction, including
  16. // without limitation the rights to use, copy, modify, merge, publish,
  17. // distribute, sublicense, and/or sell copies of the Software, and to
  18. // permit persons to whom the Software is furnished to do so, subject to
  19. // the following conditions:
  20. //
  21. // The above copyright notice and this permission notice shall be
  22. // included in all copies or substantial portions of the Software.
  23. //
  24. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  25. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  26. // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  27. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  28. // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  29. // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  30. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  31. //
  32. using System;
  33. using System.Collections.Generic;
  34. using System.Collections.Specialized;
  35. using System.IO;
  36. using System.Net;
  37. using System.Text;
  38. using System.Text.RegularExpressions;
  39. using System.Web;
  40. using Hyena;
  41. using Hyena.Json;
  42. namespace Lastfm
  43. {
  44. public enum RequestType {
  45. Read,
  46. SessionRequest, // Needs the signature, but we don't have the session key yet
  47. AuthenticatedRead,
  48. Write
  49. }
  50. public enum ResponseFormat {
  51. Json,
  52. Raw
  53. }
  54. public delegate void SendRequestHandler ();
  55. public class MaxSizeExceededException : ApplicationException
  56. {
  57. }
  58. internal class WebRequestCreator : IWebRequestCreate
  59. {
  60. public WebRequest Create (Uri uri)
  61. {
  62. return (HttpWebRequest) HttpWebRequest.Create (uri);
  63. }
  64. }
  65. public class LastfmRequest
  66. {
  67. private const string API_ROOT = "http://ws.audioscrobbler.com/2.0/";
  68. private Dictionary<string, string> parameters = new Dictionary<string, string> ();
  69. private Stream response_stream;
  70. private string response_string;
  71. IWebRequestCreate web_request_creator;
  72. public LastfmRequest ()
  73. {}
  74. internal LastfmRequest (string method, RequestType request_type, ResponseFormat response_format, IWebRequestCreate web_request_creator)
  75. : this (method, request_type, response_format)
  76. {
  77. this.web_request_creator = web_request_creator;
  78. }
  79. public LastfmRequest (string method) : this (method, RequestType.Read, ResponseFormat.Json)
  80. {}
  81. public LastfmRequest (string method, RequestType request_type, ResponseFormat response_format)
  82. {
  83. this.method = method;
  84. this.request_type = request_type;
  85. this.response_format = response_format;
  86. if (this.web_request_creator == null) {
  87. this.web_request_creator = new WebRequestCreator ();
  88. }
  89. Init ();
  90. }
  91. private void Init ()
  92. {
  93. this.incremental_data = new StringBuilder ();
  94. if (request_type != RequestType.Write) {
  95. incremental_data.Append (API_ROOT);
  96. }
  97. incremental_data.AppendFormat ("?method={0}", method);
  98. incremental_data.AppendFormat ("&api_key={0}", LastfmCore.ApiKey);
  99. if (request_type == RequestType.AuthenticatedRead || request_type == RequestType.Write) {
  100. AddParameter ("sk", LastfmCore.Account.SessionKey);
  101. }
  102. if (response_format == ResponseFormat.Json) {
  103. AddParameter ("format", "json");
  104. } else if (response_format == ResponseFormat.Raw) {
  105. AddParameter ("raw", "true");
  106. }
  107. }
  108. private string method;
  109. private RequestType request_type;
  110. private ResponseFormat response_format;
  111. private StringBuilder incremental_data;
  112. // This is close to the max based on testing.
  113. private const int MAX_POST_LENGTH = 7000;
  114. public void AddParameter (string param_name, string param_value)
  115. {
  116. var chunk = String.Format ("&{0}={1}",
  117. param_name, param_value != null ? Uri.EscapeDataString (param_value) : null);
  118. CheckSize (chunk.Length);
  119. incremental_data.Append (chunk);
  120. parameters.Add (param_name, param_value);
  121. }
  122. public void AddParameters (NameValueCollection parms)
  123. {
  124. var chunks = new StringBuilder ();
  125. foreach (string key in parms) {
  126. var value = parms [key];
  127. chunks.AppendFormat ("&{0}={1}",
  128. key, value != null ? Uri.EscapeDataString (value) : null);
  129. }
  130. CheckSize (chunks.Length);
  131. incremental_data.Append (chunks.ToString ());
  132. foreach (string key in parms) {
  133. var value = parms [key];
  134. parameters.Add (key, value);
  135. }
  136. }
  137. private void CheckSize (int length)
  138. {
  139. var target_length = incremental_data.Length + length;
  140. if (request_type != RequestType.Read) {
  141. target_length += 9 + 32; // length of &api_sig={GetSignature()}
  142. }
  143. if (target_length > MAX_POST_LENGTH) {
  144. throw new MaxSizeExceededException ();
  145. }
  146. }
  147. public Stream GetResponseStream ()
  148. {
  149. return response_stream;
  150. }
  151. public void Send ()
  152. {
  153. if (method == null) {
  154. throw new InvalidOperationException ("The method name should be set");
  155. }
  156. if (request_type == RequestType.Write) {
  157. response_stream = Post (API_ROOT, BuildPostData ());
  158. } else {
  159. response_stream = Get (BuildGetUrl ());
  160. }
  161. }
  162. public JsonObject GetResponseObject ()
  163. {
  164. if (response_stream == null) {
  165. return null;
  166. }
  167. SetResponseString ();
  168. Deserializer deserializer = new Deserializer (response_string);
  169. object obj = deserializer.Deserialize ();
  170. JsonObject json_obj = obj as Hyena.Json.JsonObject;
  171. if (json_obj == null) {
  172. throw new ApplicationException ("Lastfm invalid response : not a JSON object");
  173. }
  174. return json_obj;
  175. }
  176. public IAsyncResult BeginSend (AsyncCallback callback)
  177. {
  178. return BeginSend (callback, null);
  179. }
  180. private SendRequestHandler send_handler;
  181. public IAsyncResult BeginSend (AsyncCallback callback, object context)
  182. {
  183. send_handler = new SendRequestHandler (Send);
  184. return send_handler.BeginInvoke (callback, context);
  185. }
  186. public void EndSend (IAsyncResult result)
  187. {
  188. send_handler.EndInvoke (result);
  189. }
  190. public StationError GetError ()
  191. {
  192. StationError error = StationError.None;
  193. SetResponseString ();
  194. if (response_string == null) {
  195. return StationError.Unknown;
  196. }
  197. if (response_string.Contains ("<lfm status=\"failed\">")) {
  198. // XML reply indicates an error
  199. Match match = Regex.Match (response_string, "<error code=\"(\\d+)\">");
  200. if (match.Success) {
  201. error = (StationError) Int32.Parse (match.Value);
  202. Log.WarningFormat ("Lastfm error {0}", error);
  203. } else {
  204. error = StationError.Unknown;
  205. }
  206. }
  207. if (response_format == ResponseFormat.Json && response_string.Contains ("\"error\":")) {
  208. // JSON reply indicates an error
  209. Deserializer deserializer = new Deserializer (response_string);
  210. JsonObject json = deserializer.Deserialize () as JsonObject;
  211. if (json != null && json.ContainsKey ("error")) {
  212. error = (StationError) json["error"];
  213. Log.WarningFormat ("Lastfm error {0} : {1}", error, (string)json["message"]);
  214. }
  215. }
  216. return error;
  217. }
  218. private string BuildGetUrl ()
  219. {
  220. if (request_type == RequestType.AuthenticatedRead || request_type == RequestType.SessionRequest) {
  221. incremental_data.AppendFormat ("&api_sig={0}", GetSignature ());
  222. }
  223. return incremental_data.ToString ();
  224. }
  225. private string BuildPostData ()
  226. {
  227. incremental_data.AppendFormat ("&api_sig={0}", GetSignature ());
  228. return incremental_data.ToString ();
  229. }
  230. private string GetSignature ()
  231. {
  232. // We need to have trackNumber[0] before track[0], so we use StringComparer.Ordinal
  233. var sorted_params = new SortedDictionary<string, string> (parameters, StringComparer.Ordinal);
  234. if (!sorted_params.ContainsKey ("api_key")) {
  235. sorted_params.Add ("api_key", LastfmCore.ApiKey);
  236. }
  237. if (!sorted_params.ContainsKey ("method")) {
  238. sorted_params.Add ("method", method);
  239. }
  240. StringBuilder signature = new StringBuilder ();
  241. foreach (var parm in sorted_params) {
  242. if (parm.Key.Equals ("format")) {
  243. continue;
  244. }
  245. signature.Append (parm.Key);
  246. signature.Append (parm.Value);
  247. }
  248. signature.Append (LastfmCore.ApiSecret);
  249. return Hyena.CryptoUtil.Md5Encode (signature.ToString (), Encoding.UTF8);
  250. }
  251. public override string ToString ()
  252. {
  253. StringBuilder sb = new StringBuilder ();
  254. sb.Append (method);
  255. foreach (KeyValuePair<string, string> param in parameters) {
  256. sb.AppendFormat ("\n\t{0}={1}", param.Key, param.Value);
  257. }
  258. return sb.ToString ();
  259. }
  260. private void SetResponseString ()
  261. {
  262. if (response_string == null && response_stream != null) {
  263. using (StreamReader sr = new StreamReader (response_stream)) {
  264. response_string = sr.ReadToEnd ();
  265. }
  266. }
  267. }
  268. #region HTTP helpers
  269. private Stream Get (string uri)
  270. {
  271. return Get (uri, null);
  272. }
  273. private Stream Get (string uri, string accept)
  274. {
  275. var request = (HttpWebRequest)web_request_creator.Create (new Uri (uri));
  276. if (accept != null) {
  277. request.Accept = accept;
  278. }
  279. request.UserAgent = LastfmCore.UserAgent;
  280. request.Timeout = 10000;
  281. request.KeepAlive = false;
  282. request.AllowAutoRedirect = true;
  283. HttpWebResponse response = null;
  284. try {
  285. response = (HttpWebResponse) request.GetResponse ();
  286. } catch (WebException e) {
  287. Log.DebugException (e);
  288. response = (HttpWebResponse)e.Response;
  289. }
  290. return response != null ? response.GetResponseStream () : null;
  291. }
  292. private Stream Post (string uri, string data)
  293. {
  294. // Do not trust docs : it doesn't work if parameters are in the request body
  295. var request = (HttpWebRequest)web_request_creator.Create (new Uri (String.Concat (uri, data)));
  296. request.UserAgent = LastfmCore.UserAgent;
  297. request.Timeout = 10000;
  298. request.Method = "POST";
  299. request.KeepAlive = false;
  300. request.ContentType = "application/x-www-form-urlencoded";
  301. HttpWebResponse response = null;
  302. try {
  303. response = (HttpWebResponse) request.GetResponse ();
  304. } catch (WebException e) {
  305. Log.DebugException (e);
  306. response = (HttpWebResponse)e.Response;
  307. }
  308. return response != null ? response.GetResponseStream () : null;
  309. }
  310. #endregion
  311. }
  312. }