예제 #1
0
        internal PlayQueue(DACPServer server, byte[] data)
        {
            var nodes = DACPUtility.GetResponseNodes(data);

            foreach (var itemNode in nodes)
            {
                switch (itemNode.Key)
                {
                case "ceQk":
                    ID = itemNode.Value.GetStringValue();
                    break;

                case "ceQi":
                    StartIndex = itemNode.Value.GetInt32Value();
                    break;

                case "ceQm":
                    ItemCount = itemNode.Value.GetInt32Value();
                    break;

                case "ceQl":
                    Title1 = itemNode.Value.GetStringValue();
                    break;

                case "ceQh":
                    Title2 = itemNode.Value.GetStringValue();
                    break;
                }
            }
        }
        internal async Task <DACPResponse> SubmitRequestAsync(DACPRequest request)
        {
            if (request.IncludeSessionID)
            {
                request.QueryParameters["session-id"] = SessionID.ToString();
            }

            string uri = request.GetURI();

            _log.Info("Submitting request for: " + uri);

            HttpResponseMessage response = await HttpClient.PostAsync(uri, request.HttpContent, request.CancellationToken).ConfigureAwait(false);

            if (!response.IsSuccessStatusCode)
            {
                _log.Info("Invalid response ({0}) for: {1}", response.StatusCode, uri);
                throw new DACPRequestException(response);
            }

            byte[] data = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);

            _log.Info("Received response for: " + uri);

            // Get the content of the first node
            IEnumerable <DACPNode> nodes = null;

            if (data.Length > 0)
            {
                data  = DACPUtility.GetResponseNodes(data, true).First().Value;
                nodes = DACPUtility.GetResponseNodes(data);
            }

            return(new DACPResponse(response, nodes));
        }
예제 #3
0
 public DateTime GetDateTime(string key, DateTime defaultValue = default(DateTime))
 {
     if (!this.ContainsKey(key))
     {
         return(defaultValue);
     }
     return(DACPUtility.GetDateTimeValue(this[key]));
 }
예제 #4
0
 public bool GetBool(string key, bool defaultValue = default(bool))
 {
     if (!this.ContainsKey(key))
     {
         return(defaultValue);
     }
     return(DACPUtility.GetBoolValue(this[key]));
 }
예제 #5
0
 public long GetLong(string key, long defaultValue = default(long))
 {
     if (!this.ContainsKey(key))
     {
         return(defaultValue);
     }
     return(DACPUtility.GetInt64Value(this[key]));
 }
예제 #6
0
 public int?GetNullableInt(string key, int?defaultValue = default(int?))
 {
     if (!this.ContainsKey(key))
     {
         return(defaultValue);
     }
     return(DACPUtility.GetInt32Value(this[key]));
 }
예제 #7
0
 public short GetShort(string key, short defaultValue = default(short))
 {
     if (!this.ContainsKey(key))
     {
         return(defaultValue);
     }
     return(DACPUtility.GetInt16Value(this[key]));
 }
예제 #8
0
 public string GetString(string key, string defaultValue = default(string))
 {
     if (!this.ContainsKey(key))
     {
         return(defaultValue);
     }
     return(DACPUtility.GetStringValue(this[key]));
 }
        protected async Task <bool> UpdateCurrentSongUserRatingAsync()
        {
            // Make sure we have all the values we need
            if (CurrentDatabaseID == 0 || CurrentContainerID == 0 || CurrentItemID == 0 || CurrentAlbumPersistentID == 0)
            {
                ClearCurrentSongUserRating();
                return(true);
            }

            // Make sure this is for the main DB
            if (!ShowUserRating || MainDatabase == null || CurrentDatabaseID != MainDatabase.ID)
            {
                ClearCurrentSongUserRating();
                return(true);
            }

            // If we're requesting the rating for a new song, clear out the old value
            if (CurrentItemID != _ratingUpdatedForSongID)
            {
                ClearCurrentSongUserRating();
            }

            DACPRequest request = new DACPRequest("/databases/{0}/containers/{1}/items", CurrentDatabaseID, CurrentContainerID);

            request.QueryParameters["meta"] = "dmap.itemid,dmap.containeritemid,daap.songuserrating";
            request.QueryParameters["type"] = "music";
            request.QueryParameters["sort"] = "album";
            var query = DACPQueryCollection.And(DACPQueryPredicate.Is("daap.songalbumid", CurrentAlbumPersistentID), DACPQueryPredicate.Is("dmap.itemid", CurrentItemID));

            request.QueryParameters["query"] = query.ToString();

            try
            {
                var response = await SubmitRequestAsync(request).ConfigureAwait(false);

                var mlcl = response.Nodes.First(n => n.Key == "mlcl");

                var songNodes = DACPUtility.GetResponseNodes(mlcl.Value);
                foreach (var songData in songNodes)
                {
                    var nodes = DACPNodeDictionary.Parse(songData.Value);
                    var id    = nodes.GetInt("miid");
                    if (id != CurrentItemID)
                    {
                        continue;
                    }
                    var rating = nodes.GetByte("asur");
                    SetCurrentSongUserRatingFromServer(rating);
                    break;
                }
            }
            catch
            {
                ClearCurrentSongUserRating();
                return(false);
            }
            return(true);
        }
예제 #10
0
        private string GetDebugBytes(string code, byte[] body, int tabLevel, StringBuilder sb = null)
        {
            string debugText;
            string tab1 = new string('\t', tabLevel - 1);
            string tab2 = new string('\t', tabLevel);

            if (sb == null)
            {
                sb = new StringBuilder("Response content:\n");
            }
            sb.AppendFormat(tab1 + "{0}[{1,3}] +++", code, body.Length).AppendLine();

            var nodes = DACPUtility.GetResponseNodes(body);

            foreach (var kvp in nodes)
            {
                if (containerNodes.Contains(kvp.Key))
                {
                    GetDebugBytes(kvp.Key, kvp.Value, tabLevel + 1, sb);
                }
                else
                {
                    debugText = string.Format(tab2 + "{0}[{1,3}] ", kvp.Key, kvp.Value.Length);

                    switch (kvp.Value.Length)
                    {
                    case 1:
                        debugText += string.Format(" 0x{0:x2} = {0}", kvp.Value[0]);
                        break;

                    case 2:
                        Int16 value = kvp.Value.GetInt16Value();
                        debugText += string.Format(" 0x{0:x4} = {0}", value);
                        if (value >= 32 && value <= 126)
                        {
                            debugText += string.Format(" ({0})", (char)value);
                        }
                        break;

                    case 4:
                        debugText += string.Format(" 0x{0:x8} = {0}", kvp.Value.GetInt32Value());
                        break;

                    case 8:
                        debugText += string.Format(" 0x{0:x16} = {0}", kvp.Value.GetInt64Value());
                        break;

                    default:
                        debugText += " => " + kvp.Value.GetStringValue();
                        break;
                    }

                    sb.AppendLine(debugText);
                }
            }

            return(sb.ToString());
        }
예제 #11
0
        internal async Task <List <T> > GetListAsync <T>(DACPRequest request, Func <DACPNodeDictionary, T> itemGenerator, string listKey = DACPUtility.DefaultListKey)
        {
            try
            {
                var response = await SubmitRequestAsync(request).ConfigureAwait(false);

                return(DACPUtility.GetItemsFromNodes(response.Nodes, itemGenerator, listKey).ToList());
            }
            catch (Exception)
            {
                return(new List <T>());
            }
        }
예제 #12
0
        internal async Task <IDACPList> GetAlphaGroupedListAsync <T>(DACPRequest request, Func <byte[], T> itemGenerator, string listKey = DACPUtility.DefaultListKey)
        {
            try
            {
                var response = await SubmitRequestAsync(request).ConfigureAwait(false);

                return(DACPUtility.GetAlphaGroupedDACPList(response.Nodes, itemGenerator, listKey));
            }
            catch (Exception)
            {
                return(new DACPList <T>(false));
            }
        }
예제 #13
0
        protected async Task <bool> GetServerInfoAsync()
        {
            DACPRequest request = new DACPRequest("/server-info");

            request.IncludeSessionID = false;

            try
            {
                var response = await SubmitRequestAsync(request).ConfigureAwait(false);

                // Process response
                ServerVersionString = response.HTTPResponse.Headers.GetValues("DAAP-Server").FirstOrDefault();

                var nodes = DACPNodeDictionary.Parse(response.Nodes);

                // Fixing an issue with Apple TV devices where \0 may be appended to the end of the library name
                string libraryName = nodes.GetString("minm");
                if (!string.IsNullOrEmpty(libraryName))
                {
                    libraryName = libraryName.Replace("\0", "");
                }
                LibraryName = libraryName;

                ServerVersion     = nodes.GetInt("aeSV");
                ServerDMAPVersion = nodes.GetInt("mpro");
                ServerDAAPVersion = nodes.GetInt("apro");

                // MAC addresses
                if (nodes.ContainsKey("msml"))
                {
                    List <string> macAddresses = new List <string>();
                    var           addressNodes = DACPUtility.GetResponseNodes(nodes["msml"]).Where(n => n.Key == "msma").Select(n => n.Value);
                    foreach (var addressNode in addressNodes)
                    {
                        var address = BitConverter.ToInt64(addressNode, 0);
                        address = address >> 16;
                        macAddresses.Add(address.ToString("X12"));
                    }
                    MACAddresses = macAddresses.ToArray();
                }
            }
            catch (Exception e)
            {
                HandleHTTPException(request, e);
                return(false);
            }

            return(true);
        }
예제 #14
0
        public static IDACPList GetAlphaGroupedDACPList <T>(IEnumerable <DACPNode> nodes, Func <byte[], T> itemGenerator, out List <T> items, string listKey = DefaultListKey, bool useGroupMinimums = true)
        {
            var nodeList = nodes.ToList();

            items = GetItemsFromNodes(nodeList, itemGenerator, listKey).ToList();
            var headers = nodeList.FirstOrDefault(n => n.Key == "mshl");
            IEnumerable <DACPNode> headerNodes = null;

            if (headers != null)
            {
                headerNodes = DACPUtility.GetResponseNodes(headers.Value);
            }

            return(GetAlphaGroupedDACPList(items, headerNodes, useGroupMinimums));
        }
예제 #15
0
        protected async Task <bool> GetServerCapabilitiesAsync()
        {
            DACPRequest request = new DACPRequest("/ctrl-int");

            request.IncludeSessionID = false;

            try
            {
                var response = await SubmitRequestAsync(request).ConfigureAwait(false);

                // Process response
                var mlcl  = DACPUtility.GetResponseNodes(response.Nodes.First(n => n.Key == "mlcl").Value);
                var nodes = DACPNodeDictionary.Parse(mlcl.First(n => n.Key == "mlit").Value);

                if (nodes.ContainsKey("ceSX"))
                {
                    Int64 ceSX = nodes.GetLong("ceSX");

                    // Bit 0: Supports Play Queue
                    if ((ceSX & (1 << 0)) != 0)
                    {
                        SupportsPlayQueue = true;
                    }

                    // Bit 1: iTunes Radio? Appeared in iTunes 11.1.2 with the iTunes Radio DB.
                    // Apple's Remote for iOS doesn't seem to use this bit to determine whether iTunes Radio is available.
                    // Instead, it looks for an iTunes Radio database and checks whether it has any containers.

                    // Bit 2: Genius Shuffle Enabled/Available
                    if ((ceSX & (1 << 2)) != 0)
                    {
                        SupportsGeniusShuffle = true;
                    }
                }

                // Apple TV
                // TODO: Is this the best way to detect this?
                IsAppleTV = nodes.GetBool("ceDR");
            }
            catch (Exception e)
            {
                HandleHTTPException(request, e);
                return(false);
            }

            return(true);
        }
예제 #16
0
        /// <summary>
        /// Requests an update for the keyboard state and session ID.
        /// </summary>
        private async Task <bool> RequestAppleTVKeyboardInfoUpdateAsync()
        {
            List <byte> contentBytes = new List <byte>();

            contentBytes.AddRange(DACPUtility.GetDACPFormattedBytes("cmcc", "0"));
            contentBytes.AddRange(DACPUtility.GetDACPFormattedBytes("cmbe", "PromptResendReq"));

            ByteArrayContent content = new ByteArrayContent(contentBytes.ToArray());

            DACPRequest request = new DACPRequest("/ctrl-int/1/controlpromptentry");

            request.HttpContent = content;

            try
            {
                await SubmitRequestAsync(request).ConfigureAwait(false);
            }
            catch { return(false); }
            return(true);
        }
예제 #17
0
        /// <summary>
        /// Requests an update for the virtual trackpad connection parameters.
        /// </summary>
        private async Task <bool> RequestAppleTVTrackpadInfoUpdateAsync()
        {
            List <byte> contentBytes = new List <byte>();

            contentBytes.AddRange(DACPUtility.GetDACPFormattedBytes("cmcc", "0"));
            contentBytes.AddRange(DACPUtility.GetDACPFormattedBytes("cmbe", "DRPortInfoRequest"));
            contentBytes.AddRange(DACPUtility.GetDACPFormattedBytes("cmte", string.Format("{0},0x{1}", AppleTVEncryptionKey, PairingCode)));

            ByteArrayContent content = new ByteArrayContent(contentBytes.ToArray());

            DACPRequest request = new DACPRequest("/ctrl-int/1/controlpromptentry");

            request.HttpContent = content;

            try
            {
                await SubmitRequestAsync(request).ConfigureAwait(false);
            }
            catch { return(false); }
            return(true);
        }
예제 #18
0
        internal PlayQueueItem(DACPServer server, byte[] data)
        {
            Server = server;

            var nodes = DACPUtility.GetResponseNodes(data);

            foreach (var node in nodes)
            {
                switch (node.Key)
                {
                case "ceQs":
                    byte[] value  = node.Value;
                    byte[] dbID   = { value[0], value[1], value[2], value[3] };
                    byte[] songID = { value[12], value[13], value[14], value[15] };
                    DatabaseID = dbID.GetInt32Value();
                    SongID     = songID.GetInt32Value();
                    break;

                case "ceQn":
                    SongName = node.Value.GetStringValue();
                    break;

                case "ceQr":
                    ArtistName = node.Value.GetStringValue();
                    break;

                case "ceQa":
                    AlbumName = node.Value.GetStringValue();
                    break;

                case "ceQI":
                    // ceQI is a queue index value. The "currently playing" song has an index of 1. The first queued item has
                    // an index of 2. The first "history" item has an index of 0.
                    // This appears to be one off from how the index values are dealt with elsewhere, so I'm subtracting 1
                    // from the ceQI value to get the item's queue index.
                    QueueIndex = node.Value.GetInt32Value() - 1;
                    break;
                }
            }
        }
예제 #19
0
        private async Task <bool> SendAppleTVKeyboardStringUpdateCommandAsync(string value, bool done)
        {
            string command = (done) ? "PromptDone" : "PromptUpdate";

            List <byte> contentBytes = new List <byte>();

            contentBytes.AddRange(DACPUtility.GetDACPFormattedBytes("cmcc", _appleTVKeyboardSessionID));
            contentBytes.AddRange(DACPUtility.GetDACPFormattedBytes("cmbe", command));
            contentBytes.AddRange(DACPUtility.GetDACPFormattedBytes("cmte", value));

            ByteArrayContent content = new ByteArrayContent(contentBytes.ToArray());

            DACPRequest request = new DACPRequest("/ctrl-int/1/controlpromptentry");

            request.HttpContent = content;

            try
            {
                await SubmitRequestAsync(request).ConfigureAwait(false);
            }
            catch { return(false); }
            return(true);
        }
예제 #20
0
 public static string QueryEncodeString(string input)
 {
     return(Uri.EscapeDataString(DACPUtility.EscapeSingleQuotes(input)));
 }
예제 #21
0
 public static DACPNodeDictionary Parse(byte[] data)
 {
     return(Parse(DACPUtility.GetResponseNodes(data)));
 }
예제 #22
0
        protected void HTTPByteCallback(IAsyncResult result)
        {
            // Get the HTTPRequestInfo object
            HTTPRequestInfo requestInfo = (HTTPRequestInfo)result.AsyncState;

            _log.Info("Got HTTP response for: " + requestInfo.WebRequest.RequestUri);

            try
            {
                WebResponse response = requestInfo.WebRequest.EndGetResponse(result);
                requestInfo.WebResponse = response as HttpWebResponse;

                if (!IsConnected)
                {
                    return;
                }

                Stream       responseStream = response.GetResponseStream();
                BinaryReader br             = new BinaryReader(responseStream);
                MemoryStream data           = new MemoryStream();
                byte[]       buffer;

                do
                {
                    buffer = br.ReadBytes(8192);
                    data.Write(buffer, 0, buffer.Length);
                } while (buffer.Length > 0);

                data.Flush();

                byte[] byteResult = data.GetBuffer();

                var parsedResponse = DACPUtility.GetResponseNodes(byteResult).FirstOrDefault();
                if (parsedResponse != null)
                {
                    requestInfo.ResponseCode = parsedResponse.Key;
                    requestInfo.ResponseBody = parsedResponse.Value;
                }

                if (requestInfo.ResponseHandlerDelegate != null)
                {
                    requestInfo.ResponseHandlerDelegate(requestInfo);
                }
            }
            catch (Exception e)
            {
                _log.Warning("Caught exception for {0}: {1}", requestInfo.WebRequest.RequestUri, e.Message);
                _log.Debug("Exception details: " + e.ToString());

                if (e is WebException)
                {
                    WebException webException = (WebException)e;

                    _log.Warning("Caught web exception: " + webException.Message);
                    _log.Debug("WebException Status: " + webException.Status.ToString());

                    if (webException.Status == WebExceptionStatus.RequestCanceled)
                    {
                        lock (PendingHttpRequests)
                        {
                            if (!PendingHttpRequests.Contains(requestInfo))
                            {
                                return;
                            }
                        }
                    }

                    if (requestInfo.ExceptionHandlerDelegate != null)
                    {
                        requestInfo.ExceptionHandlerDelegate(requestInfo, webException);
                        return;
                    }
                }
                StringBuilder errorString = new StringBuilder("HTTPByteCallback Error:\r\n");
                errorString.AppendLine("URL: " + requestInfo.WebRequest.RequestUri.GetPathAndQueryString());
                errorString.AppendLine(e.ToString());
                _log.Error("Unhandled web exception.");
                _log.Debug(errorString.ToString());
                HandleConnectionError(errorString.ToString());
            }
            finally
            {
                lock (PendingHttpRequests)
                    PendingHttpRequests.Remove(requestInfo);
                UpdateGettingData();
            }
        }
예제 #23
0
        public async Task <bool> SendAppleTVKeyboardSecureTextAsync(string value)
        {
            if (!_appleTVKeyboardSecureText || string.IsNullOrEmpty(_appleTVKeyboardSecureTextCertificate) || string.IsNullOrEmpty(_appleTVKeyboardSecureTextChallenge))
            {
                return(false);
            }

            byte[] encryptedBytes;

            try
            {
                // Read the certificate
                byte[] certificateBytes = BitUtility.FromHexString(_appleTVKeyboardSecureTextCertificate);
                var    certificate      = new X509Certificate(certificateBytes);

                // X509Certificate gives us the ASN.1 DER-encoded RSA public key
                var publicKeyEncoded = certificate.GetPublicKey();
                if (publicKeyEncoded[0] != 0x30 || publicKeyEncoded[1] != 0x81)
                {
                    throw new Exception();
                }
                // Byte at index 2 is the length of the entire sequence, we can ignore this
                if (publicKeyEncoded[3] != 0x02 || publicKeyEncoded[4] != 0x81)
                {
                    throw new Exception();
                }

                // Length of the public key
                int length   = publicKeyEncoded[5];
                int position = 6;

                // Skip any padding at the beginning
                while (publicKeyEncoded[position] == 0x00)
                {
                    length--;
                    position++;
                }

                // Get the public key
                byte[] publicKey = new byte[length];
                System.Buffer.BlockCopy(publicKeyEncoded, position, publicKey, 0, length);

                // Length/position of exponent
                position += length;
                if (publicKeyEncoded[position] != 0x02)
                {
                    throw new Exception();
                }
                position++;
                length = publicKeyEncoded[position];
                position++;

                // Get the exponent
                byte[] exponent = new byte[length];
                System.Buffer.BlockCopy(publicKeyEncoded, position, exponent, 0, length);

                // Get bytes to encrypt
                var bytesToEncode = new List <byte>();
                bytesToEncode.AddRange(BitUtility.FromHexString(_appleTVKeyboardSecureTextChallenge));
                bytesToEncode.AddRange(Encoding.UTF8.GetBytes(value));

                // Set up RSA parameters
                var rsaParameters = new RSAParameters();
                rsaParameters.Modulus  = publicKey;
                rsaParameters.Exponent = exponent;

                // Create RSA provider
                using (var rsa = new RSACryptoServiceProvider())
                {
                    rsa.ImportParameters(rsaParameters);

                    // Encrypt the source data
                    encryptedBytes = rsa.Encrypt(bytesToEncode.ToArray(), false);
                }
            }
            catch { return(false); }

            // Create DACP request
            List <byte> contentBytes = new List <byte>();

            contentBytes.AddRange(DACPUtility.GetDACPFormattedBytes("cmcc", _appleTVKeyboardSessionID));
            contentBytes.AddRange(DACPUtility.GetDACPFormattedBytes("cmbe", "PromptDone"));
            contentBytes.AddRange(DACPUtility.GetDACPFormattedBytes("cmae", BitUtility.ToHexString(encryptedBytes)));

            ByteArrayContent content = new ByteArrayContent(contentBytes.ToArray());

            DACPRequest request = new DACPRequest("/ctrl-int/1/controlpromptentry");

            request.HttpContent = content;

            try
            {
                await SubmitRequestAsync(request).ConfigureAwait(false);
            }
            catch { return(false); }
            return(true);
        }
예제 #24
0
        private void HandlePlayQueueResponse(IEnumerable <DACPNode> responseNodes)
        {
            if (PlayQueues == null)
            {
                PlayQueues = new ObservableCollection <PlayQueue>();
            }

            List <PlayQueue>     queues     = new List <PlayQueue>();
            List <PlayQueueItem> queueItems = new List <PlayQueueItem>();

            var mlcl = responseNodes.FirstOrDefault(n => n.Key == "mlcl");

            if (mlcl != null)
            {
                var nodes = DACPUtility.GetResponseNodes(mlcl.Value);

                // Get the queues
                var ceQS = nodes.FirstOrDefault(n => n.Key == "ceQS");
                if (ceQS != null)
                {
                    queues.AddRange(DACPUtility.GetResponseNodes(ceQS.Value).Where(n => n.Key == "mlit").Select(n => new PlayQueue(this, n.Value)));
                }

                // Get the queue items
                queueItems.AddRange(nodes.Where(n => n.Key == "mlit").Select(n => new PlayQueueItem(this, n.Value)));
            }

            // Update the queues and queue items with minimal changes to avoid reloading the list while it's displayed.
            // This is optimized for simple inserts and deletions. Reordering items will still cause most of the list to reload.
            // Updating on the UI thread because of the observable collections being tied to UI elements.
            ThreadUtility.RunOnUIThread(() =>
            {
                // Remove queues
                var removedQueues = PlayQueues.Where(q1 => !queues.Any(q2 => q1.ID == q2.ID)).ToArray();
                foreach (var q in removedQueues)
                {
                    PlayQueues.Remove(q);
                }

                // Update/insert queues
                for (int i = 0; i < queues.Count; i++)
                {
                    var queue = queues[i];

                    // Add the queue to the list if we don't already have it
                    if (PlayQueues.Count <= i || PlayQueues[i].ID != queue.ID)
                    {
                        PlayQueues.Insert(i, queue);
                    }
                    // Update the existing queue object's start index and item count
                    else
                    {
                        PlayQueues[i].Title1     = queue.Title1;
                        PlayQueues[i].Title2     = queue.Title2;
                        PlayQueues[i].StartIndex = queue.StartIndex;
                        PlayQueues[i].ItemCount  = queue.ItemCount;
                    }
                }

                // Remove extra queues
                while (PlayQueues.Count > queues.Count)
                {
                    PlayQueues.RemoveAt(queues.Count);
                }

                // Put queue items in queues
                foreach (var queue in PlayQueues)
                {
                    int start = queue.StartIndex;
                    int stop  = start + queue.ItemCount;

                    var items = queueItems.Where(i => i.QueueIndex >= start && i.QueueIndex < stop).OrderBy(i => i.QueueIndex).ToArray();

                    // Remove items
                    var removedItems = queue.Where(i1 => !items.Any(i2 => i1.SongID == i2.SongID && i1.DatabaseID == i2.DatabaseID)).ToArray();
                    foreach (var i in removedItems)
                    {
                        queue.Remove(i);
                    }

                    // Update/insert items
                    for (int i = 0; i < items.Length; i++)
                    {
                        if (queue.Count <= i || queue[i].SongID != items[i].SongID || queue[i].DatabaseID != items[i].DatabaseID)
                        {
                            queue.Insert(i, items[i]);
                        }
                        else
                        {
                            queue[i].QueueIndex = items[i].QueueIndex;
                        }
                    }

                    // Remove extra items
                    while (queue.Count > items.Length)
                    {
                        queue.RemoveAt(items.Length);
                    }
                }

                // Update upcoming songs
                string upcomingSongName1 = null;
                string upcomingSongName2 = null;

                // If an upcoming song is from a different artist than the currently playing artist, display both the
                // artist name and the song name. Also, if the first upcoming song is from a different artist, make sure
                // the artist name is shown for both songs to avoid any confusion.
                bool includeArtistName = false;

                var upcomingItem1 = queueItems.FirstOrDefault(i => i.QueueIndex == 1);
                if (upcomingItem1 != null)
                {
                    if (upcomingItem1.ArtistName != CurrentArtist)
                    {
                        includeArtistName = true;
                    }

                    if (includeArtistName)
                    {
                        upcomingSongName1 = upcomingItem1.ArtistName + " – " + upcomingItem1.SongName;
                    }
                    else
                    {
                        upcomingSongName1 = upcomingItem1.SongName;
                    }
                }

                var upcomingItem2 = queueItems.FirstOrDefault(i => i.QueueIndex == 2);
                if (upcomingItem2 != null)
                {
                    if (upcomingItem2.ArtistName != CurrentArtist)
                    {
                        includeArtistName = true;
                    }

                    if (includeArtistName)
                    {
                        upcomingSongName2 = upcomingItem2.ArtistName + " – " + upcomingItem2.SongName;
                    }
                    else
                    {
                        upcomingSongName2 = upcomingItem2.SongName;
                    }
                }

                PlayQueueUpcomingSongName1 = upcomingSongName1;
                PlayQueueUpcomingSongName2 = upcomingSongName2;
            });
        }
        protected void ProcessGetSpeakersResponse(HTTPRequestInfo requestInfo)
        {
            List <UInt64> foundSpeakerIDs = new List <UInt64>();

            string name;
            UInt64 id;
            bool   hasPassword;
            bool   hasVideo;
            bool   active;
            int    volume;

            foreach (var kvp in requestInfo.ResponseNodes)
            {
                if (kvp.Key == "mdcl")
                {
                    name        = string.Empty;
                    id          = 0;
                    hasPassword = false;
                    hasVideo    = false;
                    active      = false;
                    volume      = 0;

                    var speakerNodes = DACPUtility.GetResponseNodes(kvp.Value);

                    foreach (var speakerKvp in speakerNodes)
                    {
                        switch (speakerKvp.Key)
                        {
                        case "minm":     // Speaker name
                            name = speakerKvp.Value.GetStringValue();
                            break;

                        case "msma":     // Speaker ID
                            id = (UInt64)speakerKvp.Value.GetInt64Value();
                            break;

                        case "cahp":     // Has Password
                            hasPassword = (speakerKvp.Value[0] > 0);
                            break;

                        case "caiv":     // Has Video
                            hasVideo = true;
                            break;

                        case "caia":     // Speaker active
                            active = (speakerKvp.Value[0] > 0);
                            break;

                        case "cmvo":     // Speaker volume
                            volume = speakerKvp.Value.GetInt32Value();
                            break;

                        default:
                            break;
                        }
                    }

                    if (foundSpeakerIDs.Contains(id))
                    {
                        continue;
                    }

                    foundSpeakerIDs.Add(id);

                    AirPlaySpeaker speaker;

                    lock (Speakers)
                    {
                        speaker = Speakers.FirstOrDefault(s => s.ID == id);
                        if (speaker == null)
                        {
                            speaker          = new AirPlaySpeaker(this, id);
                            speaker.HasVideo = (hasVideo || id == 0);
                            ThreadUtility.RunOnUIThread(() => Speakers.Add(speaker));
                        }
                    }

                    speaker.HasPassword = hasPassword;
                    speaker.Name        = name;
                    speaker.Active      = active;
                    speaker.Volume      = volume;
                    speaker.WaitingForActiveResponse = false;
                }
            }

            lock (Speakers)
            {
                // Handle speakers that are no longer available
                // Need to call ToList() so the source collection doesn't change during enumeration
                var removedSpeakers = Speakers.Where(s => !foundSpeakerIDs.Contains(s.ID)).ToList();
                foreach (AirPlaySpeaker removedSpeaker in removedSpeakers)
                {
                    ThreadUtility.RunOnUIThread(() => Speakers.Remove(removedSpeaker));
                }
            }

            SendAirPlaySpeakerUpdate();
            gettingSpeakers = false;
        }