/Application/Core/ServiceManager.cs
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}