/// <summary>
        /// Processes a batch of file changes.
        /// </summary>
        /// <param name='notification'>
        /// The notification containing the changes to process.
        /// </param>
        private void ProcessFileChanges(AudioLibraryUpdateNotification notification)
        {
            // If no rows, do nothing
            if ((notification.UpdatedFiles.Count == 0) && (notification.NewFiles.Count == 0) && (notification.OnlineFiles.Count == 0) && (notification.DeletedFiles.Count == 0) && (notification.OfflineFiles.Count == 0))
            {
                return;
            }

            // Merge these changes in the database
            AudioFileFactory.ApplicationInstance.MergeChanges(notification.NewFiles, notification.OnlineFiles, notification.UpdatedFiles);

            // Raise the notification
            Controller.NotificationNetworkServer.SendNotification(notification);
        }
        /// <summary>
        /// Handle a device being unmounted.
        /// </summary>
        /// <param name='sender'>
        /// The event sender.
        /// </param>
        /// <param name='e'>
        /// The event arguments.
        /// </param>
        private void MountManagerOnUnmounted(object sender, MountedDeviceEventArgs e)
        {
            Logger.Debug("Unmounted " + e.Device.Device + "   from   " + e.Device.MountPath);
            lock (_mountedDeviceUuids)
                if (_mountedDeviceUuids.Contains(e.Device.Uuid))
                {
                    _mountedDeviceUuids.Remove(e.Device.Uuid);
                }
            lock (_discoveryThreads)
            {
                if (_discoveryThreads.ContainsKey(e.Device))
                {
                    // Force the thread to terminate
                    try
                    {
                        _discoveryThreads[e.Device].Abort();
                    }
                    catch
                    {
                    }
                    _discoveryThreads.Remove(e.Device);
                }
            }

            // Get all files for this audio source and set them offline
            AudioFile[] filesOnDevice = AudioFileFactory.ApplicationInstance.ReadAll().Where(audioFile => audioFile.DeviceUuid == e.Device.Uuid).ToArray();
            if (filesOnDevice.Length < 1)
            {
                return;
            }
            AudioLibraryUpdateNotification notification = new AudioLibraryUpdateNotification();

            foreach (AudioFile audioFile in filesOnDevice)
            {
                notification.OfflineFiles.Add(audioFile.AudioFileId);
            }
            Controller.NotificationNetworkServer.SendNotification(notification);
        }
        private void ProcessFileList(MountedDevice device, List <string> mp3Files, AudioFile[] allAudioFiles)
        {
            // Create a new notification object if required
            AudioLibraryUpdateNotification notification = new AudioLibraryUpdateNotification();

            // Now probe each file within this directory
            try
            {
                foreach (string relativePath in mp3Files)
                {
                    string file = Path.Combine(device.MountPath + relativePath);
                    if (!File.Exists(file))
                    {
                        Logger.Error("File no longer exists when probing mounted file " + file + " for audio information, aborting loop");
                        break;
                    }
                    try
                    {
                        AudioFile audioFile = new AudioFile();
                        using (TagLib.File tagFile = TagLib.File.Create(file))
                        {
                            if (!string.IsNullOrEmpty(tagFile.Tag.Album))
                            {
                                audioFile.Album = tagFile.Tag.Album;
                            }
                            if (!string.IsNullOrEmpty(tagFile.Tag.JoinedAlbumArtists))
                            {
                                audioFile.Artist = tagFile.Tag.JoinedAlbumArtists;
                            }
                            if (tagFile.Properties.Duration != TimeSpan.Zero)
                            {
                                audioFile.Duration = tagFile.Properties.Duration;
                            }
                            if (!string.IsNullOrEmpty(tagFile.Tag.Title))
                            {
                                audioFile.Title = tagFile.Tag.Title;
                            }
                            if (tagFile.Tag.Track != 0)
                            {
                                audioFile.TrackNumber = (int)tagFile.Tag.Track;
                            }
                        }
                        if (string.IsNullOrEmpty(audioFile.Artist))
                        {
                            string[] parts       = relativePath.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
                            int      depth       = parts.Length - 1;
                            string   partToParse = parts[depth < 2 ? 0 : depth - 2];
                            if (partToParse.Contains("-"))
                            {
                                partToParse = partToParse.Split(new char[] { '-' })[0].Trim();
                            }
                            audioFile.Artist = partToParse;
                            if (audioFile.Artist == string.Empty)
                            {
                                audioFile.Artist = null;
                            }
                        }
                        if (string.IsNullOrEmpty(audioFile.Album))
                        {
                            string[] parts       = relativePath.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
                            int      depth       = parts.Length - 1;
                            string   partToParse = parts[depth < 1 ? 0 : depth - 1];
                            if (partToParse.Contains("-"))
                            {
                                partToParse = partToParse.Split(new char[] { '-' })[0].Trim();
                            }
                            audioFile.Album = partToParse;
                            if (audioFile.Album == string.Empty)
                            {
                                audioFile.Album = null;
                            }
                        }
                        if (string.IsNullOrEmpty(audioFile.Title))
                        {
                            audioFile.Title = Path.GetFileNameWithoutExtension(file).Trim();
                            if (audioFile.Title.Contains("-"))
                            {
                                audioFile.Title = audioFile.Title.Split(new char[] { '-' }, 2)[1].Trim();
                            }
                            while (!string.IsNullOrEmpty(audioFile.Title) && char.IsDigit(audioFile.Title[0]))
                            {
                                if (audioFile.Title.Length == 1)
                                {
                                    audioFile.Title = null;
                                    break;
                                }
                                audioFile.Title = audioFile.Title.Substring(1, audioFile.Title.Length - 1).Trim();
                            }
                            if (audioFile.Title == string.Empty)
                            {
                                audioFile.Title = null;
                            }
                        }
                        audioFile.DeviceUuid   = device.Uuid;
                        audioFile.LastSeen     = DateTime.UtcNow;
                        audioFile.RelativePath = relativePath;
                        Logger.Debug(audioFile.ToString());

                        // See if we have an existing file for this one
                        AudioFile existing        = allAudioFiles.Where(existingFile => existingFile.RelativePath == audioFile.RelativePath).FirstOrDefault();
                        bool      existingIsEqual = false;
                        if (existing != null)
                        {
                            existingIsEqual = audioFile.Equals(existing);
                            if (!existingIsEqual)
                            {
                                if ((existing.Album != audioFile.Album) || (existing.Artist != audioFile.Artist))
                                {
                                    existing.ArtworkSearchDate = null;
                                }
                                existing.Album       = audioFile.Album;
                                existing.Artist      = audioFile.Artist;
                                existing.Duration    = audioFile.Duration;
                                existing.LastSeen    = audioFile.LastSeen;
                                existing.Title       = audioFile.Title;
                                existing.TrackNumber = audioFile.TrackNumber;
                                audioFile            = existing;
                            }
                        }

                        // The "AC/DC Fix"
                        if ((audioFile.Artist == "AC;DC") || (audioFile.Artist == "AC; DC") || (audioFile.Artist == "AC_DC"))
                        {
                            audioFile.Artist = "AC/DC";
                        }

                        // Add to update notification if new or if it is updated
                        if (audioFile.AudioFileId == 0)
                        {
                            notification.NewFiles.Add(audioFile);
                        }
                        else if (!existingIsEqual)
                        {
                            notification.UpdatedFiles.Add(audioFile);
                        }

                        // Deal database updates and changes and firing of notifications if we have reached enough
                        // files in this object and create a new object
                        if (notification.ReadyToSend)
                        {
                            ProcessFileChanges(notification);
                            notification = new AudioLibraryUpdateNotification();
                        }
                    }
                    catch (Exception ex)
                    {
                        Logger.Error("Failed to probe file " + file, ex);
                    }
                }
            }
            catch
            {
            }

            // Deal database updates and changes and firing of notifications if we have reached enough
            // files in this object and create a new object
            ProcessFileChanges(notification);
        }
        /// <summary>
        /// This thread is used to probe all files on a particular device.
        /// </summary>
        /// <param name='args'>
        /// The arguments to pass to the thread.
        /// </param>
        private void AudioDiscoveryThread(object args)
        {
            try
            {
                // Cast object from thread arguments
                MountedDevice device = (MountedDevice)args;

                // Get a list from database of all existing tracks
                AudioFile[] allAudioFiles = AudioFileFactory.ApplicationInstance.ReadAll().Where(file => file.DeviceUuid == device.Uuid).ToArray();

                // Recursively gain two lists - new and existing files.  We always process new files
                // first, then online, then we check for changed to online and fire updated
                // and finally deleted to give quickest response to the client
                List <string> newFiles      = new List <string>();
                List <string> existingFiles = new List <string>();
                GetAllFiles(device, "/", allAudioFiles, newFiles, existingFiles);

                // Now process the file sets
                DateTime startTime = DateTime.UtcNow;
                Thread.CurrentThread.Priority = ThreadPriority.AboveNormal;
                ProcessFileList(device, newFiles, allAudioFiles);
                AudioLibraryUpdateNotification notification = new AudioLibraryUpdateNotification();
                if (existingFiles.Count > 0)
                {
                    foreach (string relativePath in existingFiles)
                    {
                        AudioFile file = allAudioFiles.FirstOrDefault(f => f.RelativePath == relativePath);
                        if (file == null)
                        {
                            continue;
                        }
                        notification.OnlineFiles.Add(file);
                    }
                    ProcessFileChanges(notification);
                    notification = new AudioLibraryUpdateNotification();
                }
                Thread.CurrentThread.Priority = ThreadPriority.BelowNormal;
                ProcessFileList(device, existingFiles, allAudioFiles);
                Thread.CurrentThread.Priority = ThreadPriority.Normal;
                foreach (AudioFile file in allAudioFiles)
                {
                    if (file.LastSeen < startTime)
                    {
                        notification.DeletedFiles.Add(file.AudioFileId);
                    }
                }
                if (notification.DeletedFiles.Count > 0)
                {
                    Controller.NotificationNetworkServer.SendNotification(notification);
                }
                AudioFileFactory.ApplicationInstance.RemoveForUuid(device.Uuid, startTime);
            }
            catch (ThreadAbortException)
            {
            }
            catch (Exception ex)
            {
                Logger.Error("Fatal error during audio file discovery", ex);
            }
            finally
            {
                lock (_discoveryThreads)
                {
                    MountedDevice key = null;
                    foreach (KeyValuePair <MountedDevice, Thread> kvp in _discoveryThreads)
                    {
                        if (kvp.Value == Thread.CurrentThread)
                        {
                            key = kvp.Key;
                            break;
                        }
                    }
                    if ((key != null) && (_discoveryThreads.ContainsKey(key)))
                    {
                        _discoveryThreads.Remove(key);
                    }
                }
            }
        }