PageRenderTime 60ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

/Espera.Core/Mobile/MobileClient.cs

http://github.com/flagbug/Espera
C# | 921 lines | 713 code | 190 blank | 18 comment | 69 complexity | ec963c83f7ebc4b4e201299df66454e4 MD5 | raw file
Possible License(s): BSD-3-Clause, CC-BY-SA-3.0
  1. using Akavache;
  2. using Espera.Core.Audio;
  3. using Espera.Core.Management;
  4. using Espera.Network;
  5. using Newtonsoft.Json;
  6. using Newtonsoft.Json.Linq;
  7. using Rareform.Validation;
  8. using ReactiveMarrow;
  9. using ReactiveUI;
  10. using Splat;
  11. using System;
  12. using System.Collections.Generic;
  13. using System.Diagnostics;
  14. using System.IO;
  15. using System.Linq;
  16. using System.Net.Sockets;
  17. using System.Reactive;
  18. using System.Reactive.Disposables;
  19. using System.Reactive.Linq;
  20. using System.Reactive.Subjects;
  21. using System.Reflection;
  22. using System.Threading;
  23. using System.Threading.Tasks;
  24. namespace Espera.Core.Mobile
  25. {
  26. /// <summary>
  27. /// Represents one mobile endpoint and handles the interaction.
  28. /// </summary>
  29. public class MobileClient : IDisposable, IEnableLogger
  30. {
  31. private readonly Subject<Unit> disconnected;
  32. private readonly CompositeDisposable disposable;
  33. private readonly TcpClient fileSocket;
  34. private readonly SemaphoreSlim gate;
  35. private readonly Library library;
  36. private readonly Dictionary<RequestAction, Func<JToken, Task<ResponseInfo>>> messageActionMap;
  37. private readonly TcpClient socket;
  38. private readonly Subject<Unit> videoPlayerToggleRequest;
  39. private Guid accessToken;
  40. private IReadOnlyList<SoundCloudSong> lastSoundCloudRequest;
  41. private IReadOnlyList<YoutubeSong> lastYoutubeRequest;
  42. private IObservable<SongTransferMessage> songTransfers;
  43. public MobileClient(TcpClient socket, TcpClient fileSocket, Library library)
  44. {
  45. if (socket == null)
  46. Throw.ArgumentNullException(() => socket);
  47. if (fileSocket == null)
  48. Throw.ArgumentNullException(() => socket);
  49. if (library == null)
  50. Throw.ArgumentNullException(() => library);
  51. this.socket = socket;
  52. this.fileSocket = fileSocket;
  53. this.library = library;
  54. this.disposable = new CompositeDisposable();
  55. this.gate = new SemaphoreSlim(1, 1);
  56. this.disconnected = new Subject<Unit>();
  57. this.lastSoundCloudRequest = new List<SoundCloudSong>();
  58. this.lastYoutubeRequest = new List<YoutubeSong>();
  59. this.videoPlayerToggleRequest = new Subject<Unit>();
  60. this.messageActionMap = new Dictionary<RequestAction, Func<JToken, Task<ResponseInfo>>>
  61. {
  62. {RequestAction.GetConnectionInfo, this.GetConnectionInfo},
  63. {RequestAction.ToggleYoutubePlayer, this.ToggleVideoPlayer},
  64. {RequestAction.GetLibraryContent, this.GetLibraryContent},
  65. {RequestAction.GetSoundCloudSongs, this.GetSoundCloudSongs},
  66. {RequestAction.GetYoutubeSongs, this.GetYoutubeSongs},
  67. {RequestAction.AddPlaylistSongs, this.AddPlaylistSongs},
  68. {RequestAction.AddPlaylistSongsNow, this.AddPlaylistSongsNow},
  69. {RequestAction.GetCurrentPlaylist, this.GetCurrentPlaylist},
  70. {RequestAction.PlayPlaylistSong, this.PlayPlaylistSong},
  71. {RequestAction.ContinueSong, this.ContinueSong},
  72. {RequestAction.PauseSong, this.PauseSong},
  73. {RequestAction.PlayNextSong, this.PlayNextSong},
  74. {RequestAction.PlayPreviousSong, this.PlayPreviousSong},
  75. {RequestAction.RemovePlaylistSong, this.PostRemovePlaylistSong},
  76. {RequestAction.MovePlaylistSongUp, this.MovePlaylistSongUp},
  77. {RequestAction.MovePlaylistSongDown, this.MovePlaylistSongDown},
  78. {RequestAction.GetVolume, this.GetVolume},
  79. {RequestAction.SetVolume, this.SetVolume},
  80. {RequestAction.VoteForSong, this.VoteForSong},
  81. {RequestAction.QueueRemoteSong, this.QueueRemoteSong},
  82. {RequestAction.SetCurrentTime, this.SetCurrentTime}
  83. };
  84. }
  85. public IObservable<Unit> Disconnected
  86. {
  87. get { return this.disconnected.AsObservable(); }
  88. }
  89. /// <summary>
  90. /// Signals when the mobile client wants to toggle the visibility of the video player.
  91. /// </summary>
  92. public IObservable<Unit> VideoPlayerToggleRequest
  93. {
  94. get { return this.videoPlayerToggleRequest.AsObservable(); }
  95. }
  96. public void Dispose()
  97. {
  98. this.socket.Close();
  99. this.gate.Dispose();
  100. this.disposable.Dispose();
  101. }
  102. public void ListenAsync()
  103. {
  104. Observable.FromAsync(() => this.socket.GetStream().ReadNextMessageAsync())
  105. .Repeat()
  106. .LoggedCatch(this, null, "Message connection was closed by the remote device or the connection failed")
  107. .TakeWhile(x => x != null)
  108. // If we don't do this, the application will throw up whenever we are manipulating a
  109. // collection that is surfaced to the UI Yes, this is astoundingly stupid
  110. .ObserveOn(RxApp.MainThreadScheduler)
  111. .SelectMany(async message =>
  112. {
  113. RequestInfo request;
  114. try
  115. {
  116. request = message.Payload.ToObject<RequestInfo>();
  117. }
  118. catch (JsonException ex)
  119. {
  120. this.Log().ErrorException(String.Format("Mobile client with access token {0} sent a malformed request", this.accessToken), ex);
  121. return Unit.Default;
  122. }
  123. var responseMessage = new NetworkMessage { MessageType = NetworkMessageType.Response };
  124. Func<JToken, Task<ResponseInfo>> action;
  125. if (this.messageActionMap.TryGetValue(request.RequestAction, out action))
  126. {
  127. bool isFatalRequest = false;
  128. try
  129. {
  130. ResponseInfo response = await action(request.Parameters);
  131. response.RequestId = request.RequestId;
  132. responseMessage.Payload = await Task.Run(() => JObject.FromObject(response));
  133. await this.SendMessage(responseMessage);
  134. }
  135. catch (Exception ex)
  136. {
  137. this.Log().ErrorException(String.Format("Mobile client with access token {0} sent a request that caused an exception", this.accessToken), ex);
  138. if (Debugger.IsAttached)
  139. {
  140. Debugger.Break();
  141. }
  142. isFatalRequest = true;
  143. }
  144. if (isFatalRequest)
  145. {
  146. ResponseInfo response = CreateResponse(ResponseStatus.Fatal);
  147. response.RequestId = request.RequestId;
  148. responseMessage.Payload = JObject.FromObject(response);
  149. // Client what are you doing? Client stahp!
  150. await this.SendMessage(responseMessage);
  151. }
  152. return Unit.Default;
  153. }
  154. return Unit.Default;
  155. })
  156. .Finally(() => this.disconnected.OnNext(Unit.Default))
  157. .Subscribe()
  158. .DisposeWith(this.disposable);
  159. var transfers = Observable.FromAsync(() => this.fileSocket.GetStream().ReadNextFileTransferMessageAsync())
  160. .Repeat()
  161. .LoggedCatch(this, null, "File transfer connection was closed by the remote device or the connection failed")
  162. .TakeWhile(x => x != null)
  163. .Publish();
  164. transfers.Connect().DisposeWith(this.disposable);
  165. this.songTransfers = transfers;
  166. }
  167. private static NetworkMessage CreatePushMessage(PushAction action, JObject content)
  168. {
  169. var message = new NetworkMessage
  170. {
  171. MessageType = NetworkMessageType.Push,
  172. Payload = JObject.FromObject(new PushInfo
  173. {
  174. Content = content,
  175. PushAction = action
  176. })
  177. };
  178. return message;
  179. }
  180. private static ResponseInfo CreateResponse(ResponseStatus status, JObject content = null)
  181. {
  182. return CreateResponse(status, null, content);
  183. }
  184. private static ResponseInfo CreateResponse(ResponseStatus status, string message, JObject content = null)
  185. {
  186. return new ResponseInfo
  187. {
  188. Status = status,
  189. Message = message,
  190. Content = content,
  191. };
  192. }
  193. private async Task<ResponseInfo> AddPlaylistSongs(JToken parameters)
  194. {
  195. IEnumerable<Song> songs;
  196. ResponseInfo response;
  197. bool areValid = this.TryValidateSongGuids(parameters["guids"].Select(x => x.ToString()), out songs, out response);
  198. if (areValid)
  199. {
  200. AccessPermission permission = await this.library.RemoteAccessControl.ObserveAccessPermission(this.accessToken).FirstAsync();
  201. if (permission == AccessPermission.Guest)
  202. {
  203. int? remainingVotes = await this.library.RemoteAccessControl.ObserveRemainingVotes(this.accessToken).FirstAsync();
  204. if (remainingVotes == null)
  205. {
  206. return CreateResponse(ResponseStatus.NotSupported, "Voting isn't supported");
  207. }
  208. if (remainingVotes == 0)
  209. {
  210. return CreateResponse(ResponseStatus.Rejected, "Not enough votes left");
  211. }
  212. }
  213. if (permission == AccessPermission.Admin)
  214. {
  215. this.library.AddSongsToPlaylist(songs, this.accessToken);
  216. }
  217. else
  218. {
  219. if (songs.Count() > 1)
  220. {
  221. return CreateResponse(ResponseStatus.Unauthorized, "Guests can't add more than one song");
  222. }
  223. this.library.AddGuestSongToPlaylist(songs.First(), this.accessToken);
  224. }
  225. return CreateResponse(ResponseStatus.Success);
  226. }
  227. return response;
  228. }
  229. private async Task<ResponseInfo> AddPlaylistSongsNow(JToken parameters)
  230. {
  231. IEnumerable<Song> songs;
  232. ResponseInfo response;
  233. bool areValid = this.TryValidateSongGuids(parameters["guids"].Select(x => x.ToString()), out songs, out response);
  234. if (areValid)
  235. {
  236. try
  237. {
  238. await this.library.PlayInstantlyAsync(songs, this.accessToken);
  239. }
  240. catch (AccessException)
  241. {
  242. return CreateResponse(ResponseStatus.Unauthorized);
  243. }
  244. return CreateResponse(ResponseStatus.Success);
  245. }
  246. return response;
  247. }
  248. private async Task<ResponseInfo> ContinueSong(JToken content)
  249. {
  250. try
  251. {
  252. await this.library.ContinueSongAsync(this.accessToken);
  253. }
  254. catch (AccessException)
  255. {
  256. return CreateResponse(ResponseStatus.Unauthorized);
  257. }
  258. return CreateResponse(ResponseStatus.Success);
  259. }
  260. private async Task<ResponseInfo> GetConnectionInfo(JToken parameters)
  261. {
  262. Guid deviceId = Guid.Parse(parameters["deviceId"].ToString());
  263. this.accessToken = this.library.RemoteAccessControl.RegisterRemoteAccessToken(deviceId);
  264. this.Log().Info("Registering new mobile client with access token {0}", this.accessToken);
  265. if (this.library.RemoteAccessControl.IsRemoteAccessReallyLocked)
  266. {
  267. var password = parameters["password"].Value<string>();
  268. if (password != null)
  269. {
  270. try
  271. {
  272. this.library.RemoteAccessControl.UpgradeRemoteAccess(this.accessToken, password);
  273. }
  274. catch (WrongPasswordException)
  275. {
  276. return CreateResponse(ResponseStatus.WrongPassword);
  277. }
  278. }
  279. }
  280. AccessPermission accessPermission = await this.library.RemoteAccessControl.ObserveAccessPermission(this.accessToken).FirstAsync();
  281. // This is stupid
  282. NetworkAccessPermission permission = accessPermission == AccessPermission.Admin ? NetworkAccessPermission.Admin : NetworkAccessPermission.Guest;
  283. int? remainingVotes = await this.library.RemoteAccessControl.ObserveRemainingVotes(this.accessToken).FirstAsync();
  284. var guestSystemInfo = new GuestSystemInfo
  285. {
  286. IsEnabled = remainingVotes.HasValue,
  287. };
  288. if (remainingVotes.HasValue)
  289. {
  290. guestSystemInfo.RemainingVotes = remainingVotes.Value;
  291. }
  292. var connectionInfo = new ConnectionInfo
  293. {
  294. AccessPermission = permission,
  295. ServerVersion = AppInfo.Version,
  296. GuestSystemInfo = guestSystemInfo
  297. };
  298. this.SetupPushNotifications();
  299. return CreateResponse(ResponseStatus.Success, null, JObject.FromObject(connectionInfo));
  300. }
  301. private async Task<ResponseInfo> GetCurrentPlaylist(JToken dontCare)
  302. {
  303. Playlist playlist = this.library.CurrentPlaylist;
  304. AudioPlayerState playbackState = await this.library.PlaybackState.FirstAsync();
  305. TimeSpan currentTime = await this.library.CurrentPlaybackTime.FirstAsync();
  306. TimeSpan totalTime = await this.library.TotalTime.FirstAsync();
  307. JObject content = MobileHelper.SerializePlaylist(playlist, playbackState, currentTime, totalTime);
  308. return CreateResponse(ResponseStatus.Success, null, content);
  309. }
  310. private async Task<ResponseInfo> GetLibraryContent(JToken dontCare)
  311. {
  312. JObject content = await Task.Run(() => MobileHelper.SerializeSongs(this.library.Songs));
  313. return CreateResponse(ResponseStatus.Success, null, content);
  314. }
  315. private async Task<ResponseInfo> GetSoundCloudSongs(JToken parameters)
  316. {
  317. var searchTerm = parameters["searchTerm"].ToObject<string>();
  318. try
  319. {
  320. var requestCache = Locator.Current.GetService<IBlobCache>(BlobCacheKeys.RequestCacheContract);
  321. var songFinder = new SoundCloudSongFinder(requestCache);
  322. IReadOnlyList<SoundCloudSong> songs = await songFinder.GetSongsAsync(searchTerm);
  323. // Cache the latest SoundCloud search request, so we can find the songs by GUID when
  324. // we add one to the playlist later
  325. this.lastSoundCloudRequest = songs;
  326. JObject content = MobileHelper.SerializeSongs(songs);
  327. return CreateResponse(ResponseStatus.Success, content);
  328. }
  329. catch (NetworkSongFinderException)
  330. {
  331. return CreateResponse(ResponseStatus.Failed, "Couldn't retrieve any SoundCloud songs");
  332. }
  333. }
  334. private Task<ResponseInfo> GetVolume(JToken dontCare)
  335. {
  336. float volume = this.library.Volume;
  337. var response = JObject.FromObject(new
  338. {
  339. volume
  340. });
  341. return Task.FromResult(CreateResponse(ResponseStatus.Success, response));
  342. }
  343. private async Task<ResponseInfo> GetYoutubeSongs(JToken parameters)
  344. {
  345. var searchTerm = parameters["searchTerm"].ToObject<string>();
  346. try
  347. {
  348. var requestCache = Locator.Current.GetService<IBlobCache>(BlobCacheKeys.RequestCacheContract);
  349. var songFinder = new YoutubeSongFinder(requestCache);
  350. IReadOnlyList<YoutubeSong> songs = await songFinder.GetSongsAsync(searchTerm);
  351. // Cache the latest YouTube search request, so we can find the songs by GUID when we
  352. // add one to the playlist later
  353. this.lastYoutubeRequest = songs;
  354. JObject content = MobileHelper.SerializeSongs(songs);
  355. return CreateResponse(ResponseStatus.Success, content);
  356. }
  357. catch (NetworkSongFinderException)
  358. {
  359. return CreateResponse(ResponseStatus.Failed, "Couldn't retrieve any YouTube songs");
  360. };
  361. }
  362. private Task<ResponseInfo> MovePlaylistSongDown(JToken parameters)
  363. {
  364. Guid songGuid;
  365. bool valid = Guid.TryParse(parameters["entryGuid"].ToString(), out songGuid);
  366. if (!valid)
  367. {
  368. return Task.FromResult(CreateResponse(ResponseStatus.MalformedRequest, "Malformed GUID"));
  369. }
  370. PlaylistEntry entry = this.library.CurrentPlaylist.FirstOrDefault(x => x.Guid == songGuid);
  371. if (entry == null)
  372. {
  373. return Task.FromResult(CreateResponse(ResponseStatus.NotFound, "Playlist entry not found"));
  374. }
  375. try
  376. {
  377. this.library.MovePlaylistSong(entry.Index, entry.Index + 1, this.accessToken);
  378. }
  379. catch (AccessException)
  380. {
  381. return Task.FromResult(CreateResponse(ResponseStatus.Unauthorized));
  382. }
  383. return Task.FromResult(CreateResponse(ResponseStatus.Success));
  384. }
  385. private Task<ResponseInfo> MovePlaylistSongUp(JToken parameters)
  386. {
  387. Guid songGuid;
  388. bool valid = Guid.TryParse(parameters["entryGuid"].ToString(), out songGuid);
  389. if (!valid)
  390. {
  391. return Task.FromResult(CreateResponse(ResponseStatus.MalformedRequest, "Malformed GUID"));
  392. }
  393. PlaylistEntry entry = this.library.CurrentPlaylist.FirstOrDefault(x => x.Guid == songGuid);
  394. if (entry == null)
  395. {
  396. return Task.FromResult(CreateResponse(ResponseStatus.NotFound));
  397. }
  398. try
  399. {
  400. this.library.MovePlaylistSong(entry.Index, entry.Index - 1, this.accessToken);
  401. }
  402. catch (AccessException)
  403. {
  404. return Task.FromResult(CreateResponse(ResponseStatus.Unauthorized));
  405. }
  406. return Task.FromResult(CreateResponse(ResponseStatus.Success));
  407. }
  408. private async Task<ResponseInfo> PauseSong(JToken dontCare)
  409. {
  410. try
  411. {
  412. await this.library.PauseSongAsync(this.accessToken);
  413. }
  414. catch (AccessException)
  415. {
  416. return CreateResponse(ResponseStatus.Unauthorized);
  417. }
  418. return CreateResponse(ResponseStatus.Success);
  419. }
  420. private async Task<ResponseInfo> PlayNextSong(JToken dontCare)
  421. {
  422. try
  423. {
  424. await this.library.PlayNextSongAsync(this.accessToken);
  425. }
  426. catch (AccessException)
  427. {
  428. return CreateResponse(ResponseStatus.Unauthorized);
  429. }
  430. return CreateResponse(ResponseStatus.Success);
  431. }
  432. private async Task<ResponseInfo> PlayPlaylistSong(JToken parameters)
  433. {
  434. Guid songGuid;
  435. bool valid = Guid.TryParse(parameters["entryGuid"].ToString(), out songGuid);
  436. if (!valid)
  437. {
  438. return CreateResponse(ResponseStatus.MalformedRequest, "Malformed GUID");
  439. }
  440. PlaylistEntry entry = this.library.CurrentPlaylist.FirstOrDefault(x => x.Guid == songGuid);
  441. if (entry == null)
  442. {
  443. return CreateResponse(ResponseStatus.NotFound, "Playlist entry not found");
  444. }
  445. try
  446. {
  447. await this.library.PlaySongAsync(entry.Index, this.accessToken);
  448. }
  449. catch (AccessException)
  450. {
  451. return CreateResponse(ResponseStatus.Unauthorized);
  452. }
  453. return CreateResponse(ResponseStatus.Success);
  454. }
  455. private async Task<ResponseInfo> PlayPreviousSong(JToken dontCare)
  456. {
  457. try
  458. {
  459. await this.library.PlayPreviousSongAsync(this.accessToken);
  460. }
  461. catch (AccessException)
  462. {
  463. return CreateResponse(ResponseStatus.Unauthorized);
  464. }
  465. return CreateResponse(ResponseStatus.Success);
  466. }
  467. private Task<ResponseInfo> PostRemovePlaylistSong(JToken parameters)
  468. {
  469. Guid songGuid = Guid.Parse(parameters["entryGuid"].ToString());
  470. PlaylistEntry entry = this.library.CurrentPlaylist.FirstOrDefault(x => x.Guid == songGuid);
  471. if (entry == null)
  472. {
  473. return Task.FromResult(CreateResponse(ResponseStatus.NotFound, "Guid not found"));
  474. }
  475. this.library.RemoveFromPlaylist(new[] { entry.Index }, this.accessToken);
  476. return Task.FromResult(CreateResponse(ResponseStatus.Success));
  477. }
  478. private async Task PushAccessPermission(AccessPermission accessPermission)
  479. {
  480. var content = JObject.FromObject(new
  481. {
  482. accessPermission
  483. });
  484. NetworkMessage message = CreatePushMessage(PushAction.UpdateAccessPermission, content);
  485. await this.SendMessage(message);
  486. }
  487. private Task PushCurrentPlaybackTime(TimeSpan currentPlaybackTime)
  488. {
  489. var content = JObject.FromObject(new
  490. {
  491. currentPlaybackTime
  492. });
  493. NetworkMessage message = CreatePushMessage(PushAction.UpdateCurrentPlaybackTime, content);
  494. return this.SendMessage(message);
  495. }
  496. private Task PushGuestSystemInfo(int? remainingVotes)
  497. {
  498. var guestSystemInfo = new GuestSystemInfo
  499. {
  500. IsEnabled = remainingVotes.HasValue,
  501. };
  502. if (remainingVotes.HasValue)
  503. {
  504. guestSystemInfo.RemainingVotes = remainingVotes.Value;
  505. }
  506. NetworkMessage message = CreatePushMessage(PushAction.UpdateGuestSystemInfo, JObject.FromObject(guestSystemInfo));
  507. return this.SendMessage(message);
  508. }
  509. private async Task PushPlaybackState(AudioPlayerState state)
  510. {
  511. var content = JObject.FromObject(new
  512. {
  513. state
  514. });
  515. NetworkMessage message = CreatePushMessage(PushAction.UpdatePlaybackState, content);
  516. await this.SendMessage(message);
  517. }
  518. private async Task PushPlaylist(Playlist playlist, AudioPlayerState state)
  519. {
  520. JObject content = MobileHelper.SerializePlaylist(playlist, state,
  521. await this.library.CurrentPlaybackTime.FirstAsync(),
  522. await this.library.TotalTime.FirstAsync());
  523. NetworkMessage message = CreatePushMessage(PushAction.UpdateCurrentPlaylist, content);
  524. await this.SendMessage(message);
  525. }
  526. private async Task<ResponseInfo> QueueRemoteSong(JToken parameters)
  527. {
  528. AccessPermission permission = await this.library.RemoteAccessControl.ObserveAccessPermission(this.accessToken).FirstAsync();
  529. if (permission == AccessPermission.Guest)
  530. {
  531. int? remainingVotes = await this.library.RemoteAccessControl.ObserveRemainingVotes(this.accessToken).FirstAsync();
  532. if (remainingVotes == null)
  533. {
  534. return CreateResponse(ResponseStatus.NotSupported, "Voting isn't supported");
  535. }
  536. if (remainingVotes == 0)
  537. {
  538. return CreateResponse(ResponseStatus.Rejected, "Not enough votes left");
  539. }
  540. }
  541. var transferInfo = parameters.ToObject<SongTransferInfo>();
  542. IObservable<byte[]> data = this.songTransfers.FirstAsync(x => x.TransferId == transferInfo.TransferId).Select(x => x.Data);
  543. var song = MobileSong.Create(transferInfo.Metadata, data);
  544. if (permission == AccessPermission.Guest)
  545. {
  546. this.library.AddGuestSongToPlaylist(song, this.accessToken);
  547. }
  548. else
  549. {
  550. this.library.AddSongsToPlaylist(new[] { song }, this.accessToken);
  551. }
  552. return CreateResponse(ResponseStatus.Success);
  553. }
  554. private async Task SendMessage(NetworkMessage content)
  555. {
  556. byte[] message;
  557. using (MeasureHelper.Measure())
  558. {
  559. message = await NetworkHelpers.PackMessageAsync(content);
  560. }
  561. await this.gate.WaitAsync();
  562. try
  563. {
  564. await this.socket.GetStream().WriteAsync(message, 0, message.Length);
  565. }
  566. catch (Exception)
  567. {
  568. this.disconnected.OnNext(Unit.Default);
  569. }
  570. finally
  571. {
  572. this.gate.Release();
  573. }
  574. }
  575. private Task<ResponseInfo> SetCurrentTime(JToken parameters)
  576. {
  577. var time = parameters["time"].ToObject<TimeSpan>();
  578. try
  579. {
  580. this.library.SetCurrentTime(time, this.accessToken);
  581. }
  582. catch (AccessException)
  583. {
  584. return Task.FromResult(CreateResponse(ResponseStatus.Unauthorized));
  585. }
  586. return Task.FromResult(CreateResponse(ResponseStatus.Success));
  587. }
  588. private void SetupPushNotifications()
  589. {
  590. this.library.WhenAnyValue(x => x.CurrentPlaylist).Skip(1)
  591. .Merge(this.library.WhenAnyValue(x => x.CurrentPlaylist)
  592. .Select(x => x.Changed().Select(y => x))
  593. .Switch())
  594. .Merge(this.library.WhenAnyValue(x => x.CurrentPlaylist)
  595. .Select(x => x.WhenAnyValue(y => y.CurrentSongIndex).Skip(1).Select(y => x))
  596. .Switch())
  597. .CombineLatest(this.library.PlaybackState, Tuple.Create)
  598. .ObserveOn(RxApp.TaskpoolScheduler)
  599. .Subscribe(x => this.PushPlaylist(x.Item1, x.Item2))
  600. .DisposeWith(this.disposable);
  601. this.library.PlaybackState.Skip(1)
  602. .ObserveOn(RxApp.TaskpoolScheduler)
  603. .Subscribe(x => this.PushPlaybackState(x))
  604. .DisposeWith(this.disposable);
  605. this.library.RemoteAccessControl.ObserveAccessPermission(this.accessToken)
  606. .Skip(1)
  607. .ObserveOn(RxApp.TaskpoolScheduler)
  608. .Subscribe(x => this.PushAccessPermission(x))
  609. .DisposeWith(this.disposable);
  610. this.library.RemoteAccessControl.ObserveRemainingVotes(this.accessToken)
  611. .Skip(1)
  612. .ObserveOn(RxApp.TaskpoolScheduler)
  613. .Subscribe(x => this.PushGuestSystemInfo(x))
  614. .DisposeWith(this.disposable);
  615. TimeSpan lastTime = TimeSpan.Zero;
  616. // We can assume that, if the total time difference exceeds two seconds, the time change
  617. // is from an external source (e.g the user clicked on the time slider)
  618. this.library.CurrentPlaybackTime
  619. .Select(x => Math.Abs(lastTime.TotalSeconds - x.TotalSeconds) >= 2 ? Tuple.Create(x, true) : Tuple.Create(x, false))
  620. .Do(x => lastTime = x.Item1)
  621. .Where(x => x.Item2)
  622. .Select(x => x.Item1)
  623. .ObserveOn(RxApp.TaskpoolScheduler)
  624. .Subscribe(x => this.PushCurrentPlaybackTime(x))
  625. .DisposeWith(this.disposable);
  626. }
  627. private Task<ResponseInfo> SetVolume(JToken parameters)
  628. {
  629. var volume = parameters["volume"].ToObject<float>();
  630. if (volume < 0 || volume > 1.0)
  631. return Task.FromResult(CreateResponse(ResponseStatus.MalformedRequest, "Volume must be between 0 and 1"));
  632. try
  633. {
  634. this.library.SetVolume(volume, this.accessToken);
  635. }
  636. catch (AccessException)
  637. {
  638. return Task.FromResult(CreateResponse(ResponseStatus.Unauthorized));
  639. }
  640. return Task.FromResult(CreateResponse(ResponseStatus.Success));
  641. }
  642. private Task<ResponseInfo> ToggleVideoPlayer(JToken arg)
  643. {
  644. this.videoPlayerToggleRequest.OnNext(Unit.Default);
  645. return Task.FromResult(CreateResponse(ResponseStatus.Success));
  646. }
  647. private bool TryValidateSongGuids(IEnumerable<string> guidStrings, out IEnumerable<Song> foundSongs, out ResponseInfo responseInfo)
  648. {
  649. var guids = new List<Guid>();
  650. foreach (string guidString in guidStrings)
  651. {
  652. Guid guid;
  653. bool valid = Guid.TryParse(guidString, out guid);
  654. if (valid)
  655. {
  656. guids.Add(guid);
  657. }
  658. else
  659. {
  660. responseInfo = CreateResponse(ResponseStatus.MalformedRequest, "One or more GUIDs are malformed");
  661. foundSongs = null;
  662. return false;
  663. }
  664. }
  665. // Look if any song in our local library or any song of the last SoundCloud or YouTube
  666. // requests has the requested Guid
  667. Dictionary<Guid, Song> dic = this.library.Songs
  668. .Concat(this.lastSoundCloudRequest.Cast<Song>())
  669. .Concat(this.lastYoutubeRequest)
  670. .ToDictionary(x => x.Guid);
  671. List<Song> songs = guids.Select(x =>
  672. {
  673. Song song;
  674. dic.TryGetValue(x, out song);
  675. return song;
  676. })
  677. .Where(x => x != null)
  678. .ToList();
  679. if (guids.Count != songs.Count)
  680. {
  681. responseInfo = CreateResponse(ResponseStatus.NotFound, "One or more songs could not be found");
  682. foundSongs = null;
  683. return false;
  684. }
  685. responseInfo = null;
  686. foundSongs = songs;
  687. return true;
  688. }
  689. private async Task<ResponseInfo> VoteForSong(JToken parameters)
  690. {
  691. int? remainingVotes = await this.library.RemoteAccessControl.ObserveRemainingVotes(this.accessToken).FirstAsync();
  692. if (remainingVotes == null)
  693. {
  694. return CreateResponse(ResponseStatus.NotSupported, "Voting isn't supported");
  695. }
  696. if (remainingVotes == 0)
  697. {
  698. return CreateResponse(ResponseStatus.Rejected, "Not enough votes left");
  699. }
  700. Guid songGuid;
  701. bool valid = Guid.TryParse(parameters["entryGuid"].ToString(), out songGuid);
  702. if (!valid)
  703. {
  704. return CreateResponse(ResponseStatus.MalformedRequest, "Malformed GUID");
  705. }
  706. Playlist playlist = this.library.CurrentPlaylist;
  707. PlaylistEntry entry = playlist.FirstOrDefault(x => x.Guid == songGuid);
  708. if (entry == null)
  709. {
  710. return CreateResponse(ResponseStatus.NotFound, "Playlist entry not found");
  711. }
  712. if (this.library.RemoteAccessControl.IsVoteRegistered(this.accessToken, entry))
  713. {
  714. return CreateResponse(ResponseStatus.Rejected, "Vote already registered");
  715. }
  716. if (playlist.CurrentSongIndex.HasValue && entry.Index <= playlist.CurrentSongIndex.Value)
  717. {
  718. return CreateResponse(ResponseStatus.Rejected, "Vote rejected");
  719. }
  720. try
  721. {
  722. this.library.VoteForPlaylistEntry(entry.Index, this.accessToken);
  723. }
  724. catch (AccessException)
  725. {
  726. return CreateResponse(ResponseStatus.Unauthorized, "Unauthorized");
  727. }
  728. return CreateResponse(ResponseStatus.Success);
  729. }
  730. }
  731. }