PageRenderTime 57ms CodeModel.GetById 10ms app.highlight 36ms RepoModel.GetById 1ms app.codeStats 0ms

/Application/Core/ServiceManager.cs

http://yet-another-music-application.googlecode.com/
C# | 1416 lines | 1031 code | 136 blank | 249 comment | 169 complexity | 64318425198c85b7792c7a577a0de005 MD5 | raw file
   1/**
   2 * ServiceManager.cs
   3 * 
   4 * Communicates with the Stoffi Services.
   5 * 
   6 * * * * * * * * *
   7 * 
   8 * Copyright 2012 Simplare
   9 * 
  10 * This code is part of the Stoffi Music Player Project.
  11 * Visit our website at: stoffiplayer.com
  12 *
  13 * This program is free software; you can redistribute it and/or
  14 * modify it under the terms of the GNU General Public License
  15 * as published by the Free Software Foundation; either version
  16 * 3 of the License, or (at your option) any later version.
  17 * 
  18 * See stoffiplayer.com/license for more information.
  19 **/
  20
  21using System;
  22using System.Collections;
  23using System.Collections.Generic;
  24using System.Collections.ObjectModel;
  25using System.Collections.Specialized;
  26using System.IO;
  27using System.Linq;
  28using System.Globalization;
  29using System.Runtime.InteropServices;
  30using System.Net;
  31using System.Text;
  32using System.Threading;
  33using System.Xml;
  34using System.Web;
  35using Newtonsoft.Json.Linq;
  36
  37namespace Stoffi
  38{
  39	/// <summary>
  40	/// Represents a manager that handles communication with
  41	/// the Stoffi services.
  42	/// </summary>
  43	public static class ServiceManager
  44	{
  45		#region Members
  46
  47		private static OAuth.Manager oauth = new OAuth.Manager();
  48		private static string oauth_request_token = "";
  49		private static string oauth_key = "baAito0V8WXtdpfjrE4GfUhld4IvFMd9Ud5EYw8i";
  50		private static string oauth_secret = "cU1esKuX0VruYYVhU5Mrry4SukK5yL9uHcYoHip1";
  51		private static string domain = "http://alpha.stoffiplayer.com";
  52		private static string deviceName = "";
  53		private static Dictionary<string, string> toBeSynced = new Dictionary<string,string>();
  54		private static Timer syncDelay = null;
  55		private static int userID = -1;
  56		private static string lang = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
  57
  58		#endregion
  59
  60		#region Properties
  61
  62		/// <summary>
  63		/// Gets whether the client is linked to a service account or not
  64		/// </summary>
  65		public static bool Linked
  66		{
  67			get
  68			{
  69				return SettingsManager.OAuthToken != null &&
  70					   SettingsManager.OAuthToken != "" &&
  71					   SettingsManager.OAuthSecret != null &&
  72					   SettingsManager.OAuthSecret != "";
  73			}
  74		}
  75
  76		/// <summary>
  77		/// Gets the URL for authorizing the application with OAuth
  78		/// </summary>
  79		public static string RequestURL
  80		{
  81			get
  82			{
  83				return String.Format("{0}/{1}/oauth/authorize?oauth_token={2}",
  84					domain, 
  85					lang, 
  86					oauth_request_token);
  87			}
  88		}
  89
  90		/// <summary>
  91		/// Gets the URL for logging out
  92		/// </summary>
  93		public static string LogoutURL { get { return String.Format("{0}/{1}/logout", domain, lang); } }
  94
  95		/// <summary>
  96		/// Gets the Identity representing the currently linked user.
  97		/// </summary>
  98		public static CloudIdentity Identity
  99		{
 100			get
 101			{
 102				return SettingsManager.GetCloudIdentity(userID);
 103			}
 104		}
 105
 106		/// <summary>
 107		/// Gets the callback URL for OAuth
 108		/// </summary>
 109		public static string CallbackURL
 110		{
 111			get { return String.Format("{0}/controls", domain); }
 112		}
 113
 114		/// <summary>
 115		/// Gets the callback URL with parameters to authenticate 
 116		/// </summary>
 117		public static string CallbackURLWithAuthParams
 118		{
 119			get
 120			{
 121				return String.Format("{0}/{1}/controls?callback=stoffi&oauth_token={2}&oauth_secret_token={3}",
 122					domain,
 123					SettingsManager.Culture.TwoLetterISOLanguageName,
 124					SettingsManager.OAuthToken,
 125					SettingsManager.OAuthSecret);
 126			}
 127		}
 128
 129		/// <summary>
 130		/// Gets or sets the name of the device
 131		/// </summary>
 132		public static string DeviceName
 133		{
 134			get { return deviceName; }
 135			set
 136			{
 137				object old = deviceName;
 138				deviceName = value;
 139				DispatchPropertyChanged("DeviceName", old, value, true);
 140
 141				if (old != value as object)
 142				{
 143					U.L(LogLevel.Debug, "SERVICE", "Changing device name");
 144					string url = String.Format("/devices/{0}.json", Identity.DeviceID);
 145					string query = String.Format("?device[name]={0}&device[version]={1}",
 146						OAuth.Manager.UrlEncode(value),
 147						OAuth.Manager.UrlEncode(SettingsManager.Release));
 148					var response = SendRequest(url, "PUT", query);
 149					if (response == null || response.StatusCode != HttpStatusCode.OK)
 150					{
 151						U.L(LogLevel.Error, "SERVICE", "There was a problem changing device name");
 152						U.L(LogLevel.Error, "SERVICE", response);
 153					}
 154					else
 155					{
 156						U.L(LogLevel.Debug, "SERVICE", "Device name changed successfully");
 157					}
 158					if (response != null) response.Close();
 159				}
 160			}
 161		}
 162
 163		/// <summary>
 164		/// Gets the synchronization profiles.
 165		/// </summary>
 166		public static Dictionary<int, string> SyncProfiles { get; private set; }
 167
 168		#endregion
 169
 170		/// <summary>
 171		/// Creates an instance of the ServiceManager class.
 172		/// </summary>
 173		static ServiceManager()
 174		{
 175			SyncProfiles = new Dictionary<int, string>();
 176		}
 177
 178		#region Methods
 179
 180		#region Public
 181
 182		/// <summary>
 183		/// Initializes the manager.
 184		/// </summary>
 185		public static void Initialize()
 186		{
 187			ServicePointManager.DefaultConnectionLimit = 1000;
 188
 189			ThreadStart initThread = delegate()
 190			{
 191				InitOAuth();
 192				SyncProfiles = new Dictionary<int, string>();
 193				SettingsManager.PropertyChanged += SettingsManager_PropertyChanged;
 194				DispatchInitialized();
 195			};
 196			Thread init_thread = new Thread(initThread);
 197			init_thread.Name = "Service Initializer thread";
 198			init_thread.Priority = ThreadPriority.Lowest;
 199			init_thread.Start();
 200		}
 201
 202		/// <summary>
 203		/// Shares a track.
 204		/// </summary>
 205		/// <param name="track">The track to share</param>
 206		public static void ShareSong(TrackData track)
 207		{
 208			ThreadStart shareThread = delegate()
 209			{
 210				string query = String.Format("?title={0}&artist={1}&path={2}&object={3}",
 211						OAuth.Manager.UrlEncode(track.Title),
 212						OAuth.Manager.UrlEncode(track.Artist),
 213						OAuth.Manager.UrlEncode(track.Path),
 214						"song");
 215				var response = SendRequest("/shares.json", "POST", query);
 216
 217				if (response == null || response.StatusCode != HttpStatusCode.OK)
 218				{
 219					U.L(LogLevel.Error, "SERVICE" ,"There was a problem sharing song");
 220					U.L(LogLevel.Error, "SERVICE", response);
 221				}
 222				else
 223				{
 224					U.L(LogLevel.Information, "SERVICE", "Shared song successfully");
 225				}
 226				if (response != null) response.Close();
 227			};
 228			Thread sharethread = new Thread(shareThread);
 229			sharethread.Name = "Share thread";
 230			sharethread.Priority = ThreadPriority.Lowest;
 231			sharethread.Start();
 232		}
 233
 234		/// <summary>
 235		/// Shares a playlist.
 236		/// </summary>
 237		/// <param name="pl">The playlist to share</param>
 238		public static void SharePlaylist(PlaylistData pl)
 239		{
 240			ThreadStart shareThread = delegate()
 241			{
 242				//string query = String.Format("?title={0}&artist={1}&path={2}&type={3}",
 243				//        OAuth.Manager.UrlEncode(track.Title),
 244				//        OAuth.Manager.UrlEncode(track.Artist),
 245				//        OAuth.Manager.UrlEncode(track.Path),
 246				//        "song");
 247				//var response = SendRequest("/share.json", "POST", query);
 248
 249				//if (response == null || response.StatusCode != HttpStatusCode.OK)
 250				//{
 251				//    U.L(LogLevel.Error, "SERVICE", "There was a problem sharing song");
 252				//    U.L(LogLevel.Error, "SERVICE", response);
 253				//}
 254				//else
 255				//{
 256				//    U.L(LogLevel.Information, "SERVICE", "Shared playlist successfully");
 257				//}
 258				//if (response != null) response.Close();
 259			};
 260			Thread sharethread = new Thread(shareThread);
 261			sharethread.Name = "Share thread";
 262			sharethread.Priority = ThreadPriority.Lowest;
 263			sharethread.Start();
 264		}
 265
 266		/// <summary>
 267		/// Submits that a track was listened to.
 268		/// </summary>
 269		/// <param name="track">The track that was listened to</param>
 270		/// <param name="playlist">The playlist from which the song was played</param>
 271		public static void Listen(TrackData track, PlaylistData playlist = null)
 272		{
 273			if (!SettingsManager.SubmitSongs) return;
 274
 275			ThreadStart listenThread = delegate()
 276			{
 277				string query = String.Format("?title={0}&artist={1}&path={2}&playlist={3}",
 278						OAuth.Manager.UrlEncode(track.Title),
 279						OAuth.Manager.UrlEncode(track.Artist),
 280						OAuth.Manager.UrlEncode(track.Path),
 281						playlist == null ? "" : playlist.Name);
 282				var response = SendRequest("/listens.json", "POST", query);
 283
 284				if (response == null || response.StatusCode != HttpStatusCode.Created)
 285				{
 286					U.L(LogLevel.Error, "SERVICE", "There was a problem submitting song");
 287					U.L(LogLevel.Error, "SERVICE", response);
 288				}
 289				else
 290				{
 291					U.L(LogLevel.Information, "SERVICE", "Submitted song successfully");
 292				}
 293				if (response != null) response.Close();
 294			};
 295			Thread l_thread = new Thread(listenThread);
 296			l_thread.Name = "Submission thread";
 297			l_thread.Priority = ThreadPriority.Lowest;
 298			l_thread.Start();
 299		}
 300
 301		/// <summary>
 302		/// Link Stoffi to an account
 303		/// </summary>
 304		/// <param name="url">The full callback URL (including parameters) where OAuth sent us after authorization</param>
 305		public static void Link(string url)
 306		{
 307			U.L(LogLevel.Information, "SERVICE", "Linking to service account");
 308			Dictionary<string,string> p = U.GetParams(U.GetQuery(url));
 309			string token = p["oauth_token"];
 310			string verifier = p["oauth_verifier"];
 311			var access_url = domain + "/oauth/access_token";
 312			try
 313			{
 314				OAuth.OAuthResponse accessToken = oauth.AcquireAccessToken(access_url, "POST", verifier);
 315				string[] tokens = accessToken.AllText.Split('&');
 316				SettingsManager.OAuthToken = tokens[0].Split('=')[1];
 317				SettingsManager.OAuthSecret = tokens[1].Split('=')[1];
 318				U.L(LogLevel.Information, "SERVICE", "Service account has been linked");
 319				RetrieveUserData();
 320			}
 321			catch (Exception e)
 322			{
 323				U.L(LogLevel.Warning, "SERVICE", "Problem linking with account: " + e.Message);
 324				DispatchDisconnected();
 325			}
 326		}
 327
 328		/// <summary>
 329		/// Remove the current link if there is one
 330		/// </summary>
 331		public static void Delink()
 332		{
 333			if (!Linked) return;
 334			SettingsManager.OAuthToken = null;
 335			SettingsManager.OAuthSecret = null;
 336			try
 337			{
 338				InitOAuth();
 339				DispatchPropertyChanged("Linked", true, false);
 340			}
 341			catch (Exception e)
 342			{
 343				DispatchDisconnected();
 344			}
 345		}
 346
 347		/// <summary>
 348		/// Update the values of an object.
 349		/// </summary>
 350		/// <remarks>
 351		/// Use this when an object is changed from outside the application (ie async call from server)
 352		/// in order to prevent the manager to send the update back to the server.
 353		/// </remarks>
 354		/// <param name="objectType">The name of the type of object</param>
 355		/// <param name="objectID">The object ID</param>
 356		/// <param name="updatedProperties">The updated properties of the object encoded in JSON</param>
 357		public static void UpdateObject(string objectType, int objectID, string updatedProperties)
 358		{
 359			JObject o = JObject.Parse(updatedProperties);
 360			switch (objectType)
 361			{
 362				case "device":
 363					if (objectID == Identity.ConfigurationID)
 364					{
 365						if (o["name"] != null)
 366						{
 367							string name = U.UnescapeHTML((string)o["name"]);
 368							deviceName = name;
 369							DeviceName = name;
 370						}
 371					}
 372					break;
 373
 374				case "configuration":
 375					if (objectID == Identity.ConfigurationID)
 376						SyncProfileUpdated(o);
 377					else
 378					{
 379						object old = SyncProfiles;
 380						string name = (string)o["name"];
 381						if (SyncProfiles.ContainsKey(objectID))
 382							SyncProfiles[objectID] = name;
 383						else
 384							SyncProfiles.Add(objectID, name);
 385						DispatchPropertyChanged("SyncProfiles", old, SyncProfiles as object, true);
 386					}
 387					break;
 388
 389				case "list_config":
 390					break;
 391
 392				case "column":
 393					break;
 394
 395				case "column_sort":
 396					break;
 397
 398				case "equalizer_profile":
 399					break;
 400
 401				case "keyboard_shortcut_profile":
 402					break;
 403
 404				case "keyboard_shortcut":
 405					break;
 406
 407				case "song":
 408					break;
 409
 410				case "album":
 411					break;
 412
 413				case "artist":
 414					break;
 415
 416				case "user":
 417					break;
 418			}
 419		}
 420
 421		/// <summary>
 422		/// Creates a new object.
 423		/// </summary>
 424		/// <remarks>
 425		/// Use this when an object is changed from outside the application (ie async call from server)
 426		/// in order to prevent the manager to send the update back to the server.
 427		/// </remarks>
 428		/// <param name="objectType">The type of the object</param>
 429		/// <param name="objectJSON">The object encoded in JSON</param>
 430		public static void CreateObject(string objectType, string objectJSON)
 431		{
 432			JObject o = JObject.Parse(objectJSON);
 433			switch (objectType)
 434			{
 435				case "device":
 436					break;
 437
 438				case "configuration":
 439					object old = SyncProfiles;
 440					int id = Convert.ToInt32((string)o["id"]);
 441					string name = (string)o["name"];
 442					if (!SyncProfiles.ContainsKey(id))
 443						SyncProfiles.Add(id, name);
 444					DispatchPropertyChanged("SyncProfiles", old, SyncProfiles as object, true);
 445					break;
 446
 447				case "list_config":
 448					break;
 449
 450				case "column":
 451					break;
 452
 453				case "column_sort":
 454					break;
 455
 456				case "equalizer_profile":
 457					break;
 458
 459				case "keyboard_shortcut_profile":
 460					break;
 461
 462				case "keyboard_shortcut":
 463					break;
 464
 465				case "song":
 466					break;
 467
 468				case "album":
 469					break;
 470
 471				case "artist":
 472					break;
 473
 474				case "user":
 475					break;
 476			}
 477		}
 478
 479		/// <summary>
 480		/// Deletes an object.
 481		/// </summary>
 482		/// <remarks>
 483		/// Use this when an object is changed from outside the application (ie async call from server)
 484		/// in order to prevent the manager to send the update back to the server.
 485		/// </remarks>
 486		/// <param name="objectType">The name of the type of object</param>
 487		/// <param name="objectID">The object ID</param>
 488		public static void DeleteObject(string objectType, int objectID)
 489		{
 490			switch (objectType)
 491			{
 492				case "device":
 493					if (objectID == Identity.DeviceID)
 494					{
 495
 496						Identity.DeviceID = -1;
 497						RegisterDevice();
 498					}
 499					break;
 500
 501				case "configuration":
 502					if (objectID == Identity.ConfigurationID)
 503					{
 504						Identity.ConfigurationID = -1;
 505					}
 506					else
 507					{
 508						object old = SyncProfiles;
 509						if (SyncProfiles.ContainsKey(objectID))
 510							SyncProfiles.Remove(objectID);
 511						DispatchPropertyChanged("SyncProfiles", old, SyncProfiles as object, true);
 512					}
 513					break;
 514
 515				case "list_config":
 516					break;
 517
 518				case "column":
 519					break;
 520
 521				case "column_sort":
 522					break;
 523
 524				case "equalizer_profile":
 525					break;
 526
 527				case "keyboard_shortcut_profile":
 528					break;
 529
 530				case "keyboard_shortcut":
 531					break;
 532
 533				case "song":
 534					break;
 535
 536				case "album":
 537					break;
 538
 539				case "artist":
 540					break;
 541
 542				case "user":
 543					break;
 544			}
 545		}
 546
 547		/// <summary>
 548		/// Executes a command.
 549		/// </summary>
 550		/// <param name="command">The name of the command</param>
 551		/// <param name="configID">The ID of the config</param>
 552		public static void ExecuteCommand(string command, string configID)
 553		{
 554			if (Identity.ConfigurationID != Convert.ToInt32(configID))
 555				return;
 556
 557			switch (command)
 558			{
 559				case "next":
 560					MediaManager.Next(true, true);
 561					break;
 562
 563				case "previous":
 564					MediaManager.Previous();
 565					break;
 566			}
 567		}
 568
 569		/// <summary>
 570		/// Adds a new cloud configuration.
 571		/// </summary>
 572		/// <param name="name">The name of the new configuration.</param>
 573		/// <param name="use">Switch to use the new configuration after it has been created.</param>
 574		public static void AddCloudConfiguration(string name, bool use = false)
 575		{
 576			ThreadStart thread = delegate()
 577			{
 578				U.L(LogLevel.Debug, "SERVICE", "Adding cloud configuration named '" + name + "'");
 579				string url = String.Format("/configurations.json");
 580
 581				// set all parameters
 582				string mediaState = SettingsManager.MediaState.ToString();
 583				string repeat = SettingsManager.Repeat.ToString();
 584				string shuffle = SettingsManager.Shuffle ? "Random" : "Off";
 585				string volume = SettingsManager.Volume.ToString();
 586
 587				// create query string
 588				string query = String.Format("?configuration[name]={0}" +
 589											 "&configuration[media_state]={1}" +
 590											 "&configuration[repeat]={2}" +
 591											 "&configuration[shuffle]={3}" +
 592											 "&configuration[volume]={4}",
 593					OAuth.Manager.UrlEncode(name),
 594					OAuth.Manager.UrlEncode(mediaState),
 595					OAuth.Manager.UrlEncode(repeat),
 596					OAuth.Manager.UrlEncode(shuffle),
 597					OAuth.Manager.UrlEncode(volume));
 598
 599
 600				// send post request to server
 601				var response = SendRequest(url, "POST", query);
 602				if (response == null || response.StatusCode != HttpStatusCode.Created)
 603				{
 604					U.L(LogLevel.Error, "SERVICE", "There was a problem creating configuration");
 605					U.L(LogLevel.Error, "SERVICE", response);
 606				}
 607				else
 608				{
 609					U.L(LogLevel.Debug, "SERVICE", "Configuration was created successfully");
 610					if (use)
 611					{
 612						JObject o = ParseResponse(response);
 613						if (o != null)
 614							Identity.ConfigurationID = (int)o["id"];
 615					}
 616				}
 617				if (response != null) response.Close();
 618			};
 619			Thread th = new Thread(thread);
 620			th.Name = "Service thread";
 621			th.Priority = ThreadPriority.Lowest;
 622			th.Start();
 623		}
 624
 625		/// <summary>
 626		/// Removes a cloud configuration.
 627		/// </summary>
 628		/// <param name="id">The ID of the configuration to remove</param>
 629		public static void RemoveCloudConfiguration(int id)
 630		{
 631			ThreadStart thread = delegate()
 632			{
 633				U.L(LogLevel.Debug, "SERVICE", "Removing cloud configuration with ID " + id);
 634				string url = String.Format("/configurations/{0}.json", id);
 635				var response = SendRequest(url, "DELETE");
 636				if (response == null || response.StatusCode != HttpStatusCode.OK)
 637				{
 638					U.L(LogLevel.Error, "SERVICE", "There was a problem removing configuration");
 639					U.L(LogLevel.Error, "SERVICE", response);
 640				}
 641				else
 642				{
 643					if (id == Identity.ConfigurationID)
 644						Identity.ConfigurationID = -1;
 645					U.L(LogLevel.Debug, "SERVICE", "Configuration was removed successfully");
 646				}
 647			};
 648			Thread th = new Thread(thread);
 649			th.Name = "Service thread";
 650			th.Priority = ThreadPriority.Lowest;
 651			th.Start();
 652		}
 653
 654		/// <summary>
 655		/// Renames a cloud configuration.
 656		/// </summary>
 657		/// <param name="id">The ID of the configuration to rename</param>
 658		/// <param name="newName">The new name of the configuration</param>
 659		public static void RenameCloudConfiguration(int id, string newName)
 660		{
 661			ThreadStart thread = delegate()
 662			{
 663				U.L(LogLevel.Debug, "SERVICE", "Renaming cloud configuration with ID " + id + " to '" + newName + "'");
 664				string url = String.Format("/configurations/{0}.json", id);
 665				string query = String.Format("?configuration[name]={0}", OAuth.Manager.UrlEncode(newName));
 666				var response = SendRequest(url, "PUT", query);
 667				if (response == null || response.StatusCode != HttpStatusCode.Created)
 668				{
 669					U.L(LogLevel.Error, "SERVICE", "There was a problem renaming configuration");
 670					U.L(LogLevel.Error, "SERVICE", response);
 671				}
 672				else
 673					U.L(LogLevel.Debug, "SERVICE", "Configuration was renamed successfully");
 674				if (response != null) response.Close();
 675			};
 676			Thread th = new Thread(thread);
 677			th.Name = "Service thread";
 678			th.Priority = ThreadPriority.Lowest;
 679			th.Start();
 680		}
 681
 682		/// <summary>
 683		/// Synchronize the application with a given cloud configuration.
 684		/// Will download all information and apply it to the application.
 685		/// 
 686		/// Set to 0 to turn off.
 687		/// </summary>
 688		/// <param name="cloudConfig">The ID of the cloud configuration</param>
 689		public static void Sync(int cloudConfig)
 690		{
 691			if (cloudConfig <= 0)
 692			{
 693				U.L(LogLevel.Debug, "SERVICE", "Turning off synchronization");
 694				Identity.ConfigurationID = 0;
 695				return;
 696			}
 697			ThreadStart thread = delegate()
 698			{
 699				U.L(LogLevel.Debug, "SERVICE", "Retrieving cloud configuration with ID " + cloudConfig);
 700				string url = String.Format("/configurations/{0}.json", cloudConfig);
 701				var response = SendRequest(url, "GET");
 702				if (response == null || response.StatusCode != HttpStatusCode.OK)
 703				{
 704					U.L(LogLevel.Error, "SERVICE", "There was a problem retrieving configuration");
 705					U.L(LogLevel.Error, "SERVICE", response);
 706				}
 707				else
 708				{
 709					JObject config = ParseResponse(response);
 710					int id = (int)config["id"];
 711					Identity.ConfigurationID = id;
 712					SyncProfileUpdated(config);
 713
 714					U.L(LogLevel.Debug, "SERVICE", "Tell server we're using this profile");
 715					url = String.Format("/devices/{0}.json", Identity.DeviceID);
 716					string query = String.Format("?device[configuration_id]={0}",
 717						OAuth.Manager.UrlEncode(Identity.ConfigurationID.ToString()));
 718					response.Close();
 719					response = SendRequest(url, "PUT", query);
 720					if (response == null || response.StatusCode != HttpStatusCode.OK)
 721					{
 722						U.L(LogLevel.Error, "SERVICE", "There was a problem updating device data");
 723						U.L(LogLevel.Error, "SERVICE", response);
 724					}
 725					else
 726					{
 727						U.L(LogLevel.Debug, "SERVICE", "Successfully told server of our new sync profile.");
 728					}
 729					if (response != null) response.Close();
 730				}
 731				if (response != null) response.Close();
 732			};
 733			Thread th = new Thread(thread);
 734			th.Name = "Service thread";
 735			th.Priority = ThreadPriority.Lowest;
 736			th.Start();
 737		}
 738
 739		/// <summary>
 740		/// Initializes the oauth manager and acquires a request token if needed
 741		/// </summary>
 742		public static void InitOAuth()
 743		{
 744			oauth = new OAuth.Manager();
 745			oauth["consumer_key"] = oauth_key;
 746			oauth["consumer_secret"] = oauth_secret;
 747			if (Linked)
 748			{
 749				oauth["token"] = SettingsManager.OAuthToken;
 750				oauth["token_secret"] = SettingsManager.OAuthSecret;
 751				RetrieveUserData();
 752			}
 753			else
 754			{
 755				try
 756				{
 757					oauth.AcquireRequestToken(domain + "/oauth/request_token", "POST");
 758					oauth_request_token = oauth["token"];
 759				}
 760				catch (Exception e)
 761				{
 762					U.L(LogLevel.Warning, "SERVICE", "Problem linking with account: " + e.Message);
 763					DispatchDisconnected();
 764				}
 765			}
 766		}
 767
 768		/// <summary>
 769		/// Checks if a given URL works.
 770		/// If not it will go to disconnected mode.
 771		/// </summary>
 772		/// <param name="url">The URL to check</param>
 773		public static void Ping(Uri url)
 774		{
 775			ThreadStart thread = delegate()
 776			{
 777				try
 778				{
 779					U.L(LogLevel.Debug, "SERVICE", "Trying to ping: " + url.Host);
 780					var request = (HttpWebRequest)WebRequest.Create(url.Scheme + "://" + url.Host);
 781					request.Timeout = 10000;
 782					request.KeepAlive = false;
 783					HttpWebResponse resp = (HttpWebResponse)request.GetResponse();
 784					if (resp != null)
 785					{
 786						// check status codes?
 787					}
 788					else
 789					{
 790						U.L(LogLevel.Error, "SERVICE", "No response from host: " + url.Host);
 791						DispatchDisconnected();
 792					}
 793					resp.Close();
 794				}
 795				catch (WebException e)
 796				{
 797					U.L(LogLevel.Error, "SERVICE", "Error while pinging URL: " + e.Message);
 798					DispatchDisconnected();
 799				}
 800			};
 801			Thread th = new Thread(thread);
 802			th.Name = "Ping thread";
 803			th.Priority = ThreadPriority.Lowest;
 804			th.Start();
 805		}
 806		
 807		#endregion
 808
 809		#region Private
 810
 811		/// <summary>
 812		/// Retrieves the data of the currently linked user.
 813		/// </summary>
 814		public static void RetrieveUserData()
 815		{
 816			if (!Linked) return;
 817
 818			ThreadStart cloudThread = delegate()
 819			{
 820				string name = U.Capitalize(Environment.MachineName);
 821				U.L(LogLevel.Debug, "SERVICE", "Fetching user data");
 822				string url = String.Format("/me.json");
 823				var response = SendRequest(url, "GET", "");
 824				if (response == null || response.StatusCode != HttpStatusCode.OK)
 825				{
 826					U.L(LogLevel.Error, "SERVICE", "There was a problem retrieving user data");
 827					U.L(LogLevel.Error, "SERVICE", response);
 828					Delink();
 829				}
 830				else
 831				{
 832					JObject o = ParseResponse(response);
 833					userID = (int)o["id"];
 834					Console.WriteLine("user id: " + userID);
 835					if (!SettingsManager.HasCloudIdentity(userID))
 836						SettingsManager.CloudIdentities.Add(new CloudIdentity { UserID = userID });
 837					RegisterDevice();
 838				}
 839				if (response != null) response.Close();
 840			};
 841			Thread cl_thread = new Thread(cloudThread);
 842			cl_thread.Name = "Cloud user thread";
 843			cl_thread.Priority = ThreadPriority.Lowest;
 844			cl_thread.Start();
 845		}
 846
 847		/// <summary>
 848		/// Registers the device with the server
 849		/// </summary>
 850		private static void RegisterDevice()
 851		{
 852			ThreadStart cloudThread = delegate()
 853			{
 854				if (Identity != null && Identity.DeviceID > 0)
 855				{
 856					U.L(LogLevel.Debug, "SERVICE", "Device already registered, verifying ID");
 857					string url = String.Format("/devices/{0}.json", Identity.DeviceID);
 858					var response = SendRequest(url, "GET");
 859					if (response == null || response.StatusCode != HttpStatusCode.OK)
 860					{
 861						Identity.DeviceID = -1;
 862						U.L(LogLevel.Debug, "SERVICE", "Previous registered ID was invalid");
 863						RegisterDevice();
 864					}
 865					else
 866					{
 867						Stream s = response.GetResponseStream();
 868						StreamReader sr = new StreamReader(s);
 869						string str = sr.ReadToEnd();
 870						JObject o = JObject.Parse(str);
 871						string name = (string)o["name"];
 872						U.L(LogLevel.Debug, "SERVICE", "ID verified, device name is " + name);
 873						deviceName = name;
 874						DeviceName = name;
 875						DispatchPropertyChanged("Linked", false, true);
 876						RetrieveCloudConfigurations();
 877					}
 878					if (response != null) response.Close();
 879				}
 880				else
 881				{
 882					string name = U.Capitalize(Environment.MachineName);
 883					U.L(LogLevel.Debug, "SERVICE", "Need to register the device");
 884					string url = String.Format("/devices.json");
 885					string query = String.Format("?device[name]={0}&device[version]={1}",
 886						OAuth.Manager.UrlEncode(name),
 887						OAuth.Manager.UrlEncode(SettingsManager.Release));
 888					var response = SendRequest(url, "POST", query);
 889					if (response == null || response.StatusCode != HttpStatusCode.Created)
 890					{
 891						U.L(LogLevel.Error, "SERVICE", "There was a problem registering the device");
 892						U.L(LogLevel.Error, "SERVICE", response);
 893						Delink();
 894					}
 895					else
 896					{
 897						Stream s = response.GetResponseStream();
 898						StreamReader sr = new StreamReader(s);
 899						string str = sr.ReadToEnd();
 900						JObject o = JObject.Parse(str);
 901						Identity.DeviceID = (int)o["id"];
 902						U.L(LogLevel.Debug, "SERVICE", "Device has been registered with ID " + Identity.DeviceID.ToString());
 903						deviceName = name;
 904						DeviceName = name;
 905						DispatchPropertyChanged("Linked", false, true);
 906						RetrieveCloudConfigurations();
 907					}
 908					if (response != null) response.Close();
 909				}
 910			};
 911			Thread cl_thread = new Thread(cloudThread);
 912			cl_thread.Name = "Cloud register thread";
 913			cl_thread.Priority = ThreadPriority.Lowest;
 914			cl_thread.Start();
 915		}
 916
 917		/// <summary>
 918		/// Retrieves all cloud configurations from the server.
 919		/// </summary>
 920		private static void RetrieveCloudConfigurations()
 921		{
 922			ThreadStart configThread = delegate()
 923			{
 924				U.L(LogLevel.Debug, "SERVICE", "Retrieving cloud configurations");
 925				string url = String.Format("/configurations.json");
 926				var response = SendRequest(url, "GET");
 927				if (response == null || response.StatusCode != HttpStatusCode.OK)
 928				{
 929					U.L(LogLevel.Error, "SERVICE", "There was a problem retrieving cloud configurations");
 930					U.L(LogLevel.Error, "SERVICE", response);
 931				}
 932				else
 933				{
 934					Stream s = response.GetResponseStream();
 935					StreamReader sr = new StreamReader(s);
 936					string str = sr.ReadToEnd();
 937					Console.WriteLine("response: " + str);
 938					JArray a = JArray.Parse(str);
 939					object old = SyncProfiles;
 940					SyncProfiles.Clear();
 941					foreach (JObject config in a)
 942					{
 943						string name = (string)config["name"];
 944						int id = (int)config["id"];
 945						if (SyncProfiles.ContainsKey(id))
 946							SyncProfiles[id] = name;
 947						else
 948							SyncProfiles.Add(id, name);
 949					}
 950					DispatchPropertyChanged("SyncProfiles", old, SyncProfiles as object, true);
 951				}
 952				if (response != null) response.Close();
 953			};
 954			Thread config_thread = new Thread(configThread);
 955			config_thread.Name = "Cloud configuration thread";
 956			config_thread.Priority = ThreadPriority.Lowest;
 957			config_thread.Start();
 958		}
 959
 960		/// <summary>
 961		/// Will update Stoffi's current configuration according to the
 962		/// state of the synchronization profile.
 963		/// </summary>
 964		/// <param name="updatedParams">The updated parameters of the configuration.</param>
 965		private static void SyncProfileUpdated(JObject updatedParams)
 966		{
 967			SettingsManager.PropertyChanged -= SettingsManager_PropertyChanged;
 968
 969			#region Media state
 970			if (updatedParams["media_state"] != null)
 971			{
 972				try
 973				{
 974					switch ((string)updatedParams["media_state"])
 975					{
 976						case "Playing":
 977							MediaManager.Play();
 978							break;
 979
 980						case "Paused":
 981							Console.WriteLine("PAUSE!");
 982							MediaManager.Pause();
 983							break;
 984
 985						case "Stopped":
 986							Console.WriteLine("STOP!");
 987							MediaManager.Stop();
 988							break;
 989					}
 990				}
 991				catch (Exception e)
 992				{
 993					U.L(LogLevel.Error, "SERVICE", "Problem synchronizing media state: " + e.Message);
 994				}
 995			}
 996			#endregion
 997
 998			#region Shuffle
 999			if (updatedParams["shuffle"] != null)
1000			{
1001				try
1002				{
1003					switch ((string)updatedParams["shuffle"])
1004					{
1005						case "NoShuffle":
1006							SettingsManager.Shuffle = false;
1007							break;
1008
1009						case "Random":
1010							SettingsManager.Shuffle = true;
1011							break;
1012					}
1013				}
1014				catch (Exception e)
1015				{
1016					U.L(LogLevel.Error, "SERVICE", "Problem synchronizing shuffle mode: " + e.Message);
1017				}
1018			}
1019			#endregion
1020
1021			#region Repeat
1022			if (updatedParams["repeat"] != null)
1023			{
1024				try
1025				{
1026					switch ((string)updatedParams["repeat"])
1027					{
1028						case "NoRepeat":
1029							SettingsManager.Repeat = RepeatState.NoRepeat;
1030							break;
1031
1032						case "RepeatAll":
1033							SettingsManager.Repeat = RepeatState.RepeatAll;
1034							break;
1035
1036						case "RepeatOne":
1037							SettingsManager.Repeat = RepeatState.RepeatOne;
1038							break;
1039					}
1040				}
1041				catch (Exception e)
1042				{
1043					U.L(LogLevel.Error, "SERVICE", "Problem synchronizing repeat mode: " + e.Message);
1044				}
1045			}
1046			#endregion
1047
1048			#region Volume
1049			if (updatedParams["volume"] != null)
1050			{
1051				try
1052				{
1053					float vol = (float)updatedParams["volume"];
1054					SettingsManager.Volume = Convert.ToDouble(vol);
1055				}
1056				catch (Exception e)
1057				{
1058					try
1059					{
1060						string vol = (string)updatedParams["volume"];
1061						SettingsManager.Volume = Convert.ToDouble(vol);
1062					}
1063					catch (Exception ee)
1064					{
1065						U.L(LogLevel.Error, "SERVICE", "Problem synchronizing volume");
1066						U.L(LogLevel.Error, "SERVICE", e.Message);
1067						U.L(LogLevel.Error, "SERVICE", ee.Message);
1068					}
1069				}
1070			}
1071			#endregion
1072
1073			#region Current track
1074			if (updatedParams["current_track"] != null)
1075			{
1076				try
1077				{
1078					string path = (string)updatedParams["current_track"]["path"];
1079					if (path != null)
1080					{
1081						// TODO:
1082						// Create function SettingsManager.FindTrack()
1083						// It should then either find a local track fast
1084						// or convert the path into a youtube track.
1085						if (YouTubeManager.IsYouTube(path))
1086						{
1087							TrackData track = YouTubeManager.CreateTrack(YouTubeManager.GetYouTubeID(path));
1088							if (track != null)
1089								SettingsManager.CurrentTrack = track;
1090						}
1091						else
1092						{
1093							foreach (TrackData track in SettingsManager.FileTracks)
1094							{
1095								if (track.Path == path)
1096								{
1097									SettingsManager.CurrentTrack = track;
1098									break;
1099								}
1100							}
1101						}
1102					}
1103				}
1104				catch (Exception e)
1105				{
1106					U.L(LogLevel.Error, "SERVICE", "Could not read current track from server");
1107					U.L(LogLevel.Error, "SERVICE", e.Message);
1108				}
1109			}
1110			#endregion
1111
1112			SettingsManager.PropertyChanged += SettingsManager_PropertyChanged;
1113		}
1114
1115		/// <summary>
1116		/// Sends a request to the server
1117		/// </summary>
1118		/// <param name="url">The base url</param>
1119		/// <param name="method">The HTTP method to use</param>
1120		/// <param name="query">The query string, encoded properly</param>
1121		/// <returns>The response from the server</returns>
1122		private static HttpWebResponse SendRequest(string url, string method, string query = "")
1123		{
1124			if (SettingsManager.HasCloudIdentity(userID))
1125			{
1126				if (query == "") query = "?";
1127				else query += "&";
1128				query += "device_id=" + Identity.DeviceID.ToString();
1129			}
1130
1131			string fullUrl = domain + url + query;
1132			U.L(LogLevel.Debug, "SERVICE", "Sending " + method + " request to " + fullUrl);
1133			var authzHeader = oauth.GenerateAuthzHeader(fullUrl, method);
1134			var request = (HttpWebRequest)WebRequest.Create(fullUrl);
1135			request.Method = method;
1136			request.PreAuthenticate = true;
1137			request.AllowWriteStreamBuffering = true;
1138			request.Headers.Add("Authorization", authzHeader);
1139			request.Timeout = 30000;
1140			request.KeepAlive = false;
1141			try
1142			{
1143				HttpWebResponse resp = (HttpWebResponse)request.GetResponse();
1144				return resp;
1145			}
1146			catch (WebException exc)
1147			{
1148				U.L(LogLevel.Error, "SERVICE", "Error while contacting cloud server: " + exc.Message);
1149				return null;
1150			}
1151		}
1152
1153		/// <summary>
1154		/// Parses a web response encoded in JSON into an object.
1155		/// </summary>
1156		/// <param name="response">The JSON web response.</param>
1157		/// <returns>A JSON object.</returns>
1158		private static JObject ParseResponse(HttpWebResponse response)
1159		{
1160			try
1161			{
1162				Stream s = response.GetResponseStream();
1163				StreamReader sr = new StreamReader(s);
1164				string str = sr.ReadToEnd();
1165				JObject o = JObject.Parse(str);
1166				return o;
1167			}
1168			catch (Exception e)
1169			{
1170				U.L(LogLevel.Error, "SERVICE", "Could not parse response: " + e.Message);
1171				return null;
1172			}
1173		}
1174
1175		/// <summary>
1176		/// Encodes a track object into HTTP parameters.
1177		/// </summary>
1178		/// <param name="track">The track to encode</param>
1179		/// <param name="prefix">An optional prefix to put on each property</param>
1180		/// <returns>The track encoded as "title=X&artist=Y&genre=Z..."</returns>
1181		private static List<string> EncodeTrack(TrackData track, string prefix = "")
1182		{
1183			List<string> paras = new List<string>();
1184			if (track != null)
1185			{
1186				paras.Add(U.CreateParam("title", track.Title, prefix));
1187				paras.Add(U.CreateParam("artist", track.Artist, prefix));
1188				paras.Add(U.CreateParam("album", track.Album, prefix));
1189				paras.Add(U.CreateParam("length", track.RawLength, prefix));
1190				paras.Add(U.CreateParam("genre", track.Genre, prefix));
1191				paras.Add(U.CreateParam("track", track.Track, prefix));
1192				paras.Add(U.CreateParam("path", track.Path, prefix));
1193				paras.Add(U.CreateParam("views", track.RawViews, prefix));
1194				paras.Add(U.CreateParam("bitrate", track.Bitrate, prefix));
1195				paras.Add(U.CreateParam("codecs", track.Codecs, prefix));
1196				paras.Add(U.CreateParam("channels", track.Channels, prefix));
1197				paras.Add(U.CreateParam("sample_rate", track.SampleRate, prefix));
1198				paras.Add(U.CreateParam("year", track.Year, prefix));
1199			}
1200			return paras;
1201		}
1202
1203		#endregion
1204
1205		#region Event handlers
1206
1207		/// <summary>
1208		/// Invoked when a property of the settings manager is changed
1209		/// </summary>
1210		/// <param name="sender">The sender of the event</param>
1211		/// <param name="e">The event data</param>
1212		private static void SettingsManager_PropertyChanged(object sender, PropertyChangedWithValuesEventArgs e)
1213		{
1214			if (Identity == null || Identity.ConfigurationID < 1) return;
1215
1216			// create query string
1217			switch (e.PropertyName)
1218			{
1219				case "Volume":
1220					string vol = Convert.ToString(SettingsManager.Volume);
1221					toBeSynced["configuration[volume]"] = OAuth.Manager.UrlEncode(vol);
1222					break;
1223
1224				case "Repeat":
1225					toBeSynced["configuration[repeat]"] = OAuth.Manager.UrlEncode(SettingsManager.Repeat.ToString());
1226					break;
1227
1228				case "Shuffle":
1229					toBeSynced["configuration[shuffle]"] = OAuth.Manager.UrlEncode(SettingsManager.Shuffle ? "Random" : "Off");
1230					break;
1231
1232				case "MediaState":
1233					toBeSynced["configuration[media_state]"] = OAuth.Manager.UrlEncode(SettingsManager.MediaState.ToString());
1234					break;
1235
1236				case "CurrentTrack":
1237					List<string> paras = EncodeTrack(SettingsManager.CurrentTrack, "configurations[current_track]");
1238					foreach (string para in paras)
1239					{
1240						if (para != null)
1241						{
1242							string[] p = para.Split(new char[] { '=' }, 2);
1243							if (p[0] != "" && p[1] != "")
1244								toBeSynced[p[0]] = p[1];
1245						}
1246					}
1247					break;
1248
1249				default:
1250					return;
1251			}
1252
1253			if (syncDelay != null)
1254				syncDelay.Dispose();
1255			syncDelay = new Timer(PerformSync, null, 400, 3600);
1256		}
1257
1258		/// <summary>
1259		/// Sends requests to the server updating the properties stored in
1260		/// toBeSynced
1261		/// </summary>
1262		/// <param name="state">The state (not used)</param>
1263		private static void PerformSync(object state)
1264		{
1265			syncDelay.Dispose();
1266			syncDelay = null;
1267
1268			int id = Identity.ConfigurationID;
1269			if (id < 0 || toBeSynced.Count == 0) return;
1270
1271			ThreadStart thread = delegate()
1272			{
1273				U.L(LogLevel.Debug, "SERVICE", "Sending parameters to sync server");
1274				string url = String.Format("/configurations/{0}.json", id);				
1275				string query = "";
1276				foreach (KeyValuePair<string, string> p in toBeSynced)
1277					query += "&" + p.Key + "=" + p.Value;
1278				query = "?" + query.Substring(1);
1279
1280				U.L(LogLevel.Debug, "SERVICE", "Updating cloud configuration with ID " + id);
1281
1282				// send put request to server
1283				var response = SendRequest(url, "PUT", query);
1284				if (response == null || response.StatusCode != HttpStatusCode.OK)
1285				{
1286					U.L(LogLevel.Error, "SERVICE", "There was a problem updating configuration");
1287					U.L(LogLevel.Error, "SERVICE", response);
1288				}
1289				else
1290				{
1291					U.L(LogLevel.Debug, "SERVICE", "Configuration was updated successfully");
1292				}
1293				if (response != null) response.Close();
1294			};
1295			Thread th = new Thread(thread);
1296			th.Name = "Sync thread";
1297			th.Priority = ThreadPriority.Lowest;
1298			th.Start();
1299		}
1300
1301		#endregion
1302
1303		#region Dispatchers
1304
1305		/// <summary>
1306		/// Trigger the PropertyChanged event
1307		/// </summary>
1308		/// <param name="name">The name of the property that was changed</param>
1309		/// <param name="oldValue">The value of the property before the change</param>
1310		/// <param name="newValue">The value of the property after the change</param>
1311		/// <param name="force">Dispatch even when no change occured between values</param>
1312		public static void DispatchPropertyChanged(string name, object oldValue, object newValue, bool force = false)
1313		{
1314			if (PropertyChanged != null && (oldValue != newValue || force))
1315				PropertyChanged(null, new PropertyChangedWithValuesEventArgs(name, oldValue, newValue));
1316		}
1317
1318		/// <summary>
1319		/// Trigger the Initialized event.
1320		/// </summary>
1321		public static void DispatchInitialized()
1322		{
1323			if (Initialized != null)
1324				Initialized(null, new EventArgs());
1325		}
1326
1327		/// <summary>
1328		/// Trigger the Disconnected event.
1329		/// </summary>
1330		public static void DispatchDisconnected()
1331		{
1332			if (Disconnected != null)
1333				Disconnected(null, new EventArgs());
1334		}
1335
1336		#endregion
1337
1338		#endregion
1339		
1340		#region Events
1341
1342		/// <summary>
1343		/// Occurs when a property has been changed
1344		/// </summary>
1345		public static event PropertyChangedWithValuesEventHandler PropertyChanged;
1346
1347		/// <summary>
1348		/// Occurs when the service manager has been fully initialized.
1349		/// </summary>
1350		public static event EventHandler Initialized;
1351
1352		/// <summary>
1353		/// Occurs when the service manager has been disconnected from service.
1354		/// </summary>
1355		public static event EventHandler Disconnected;
1356
1357		#endregion
1358	}
1359
1360	/// <summary>
1361	/// Describes the interface that the async javascript will call to update attributes
1362	/// </summary>
1363	[ComVisibleAttribute(true)]
1364	public class CloudSyncInterface
1365	{
1366		/// <summary>
1367		/// Invoked when an object changes.
1368		/// </summary>
1369		/// <param name="objectType">The name of the type of object</param>
1370		/// <param name="objectID">The object ID</param>
1371		/// <param name="properties">The updated properties encoded in JSON</param>
1372		public void UpdateObject(string objectType, int objectID, string properties)
1373		{
1374			ServiceManager.UpdateObject(objectType, objectID, properties);
1375		}
1376
1377		/// <summary>
1378		/// Invoked when an object is created.
1379		/// </summary>
1380		/// <param name="objectType">The name of the type of object</param>
1381		/// <param name="objectJSON">The object encoded as JSON</param>
1382		public void CreateObject(string objectType, string objectJSON)
1383		{
1384			ServiceManager.CreateObject(objectType, objectJSON);
1385		}
1386
1387		/// <summary>
1388		/// Invoked when an object is deleted.
1389		/// </summary>
1390		/// <param name="objectType">The name of the type of object</param>
1391		/// <param name="objectID">The object id</param>
1392		public void DeleteObject(string objectType, int objectID)
1393		{
1394			ServiceManager.DeleteObject(objectType, objectID);
1395		}
1396
1397		/// <summary>
1398		/// Invoked when a command is to be executed.
1399		/// </summary>
1400		/// <param name="command">The name of the command</param>
1401		/// <param name="configID">The ID of the config</param>
1402		public void Execute(string command, string configID)
1403		{
1404			ServiceManager.ExecuteCommand(command, configID);
1405		}
1406	}
1407
1408	#region Delegates
1409	#endregion
1410
1411	#region Event arguments
1412	#endregion
1413
1414	#region Enums
1415	#endregion
1416}