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