using Lantean.QBTMud.Helpers; using Lantean.QBTMud.Models; namespace Lantean.QBTMud.Services { public class DataManager : IDataManager { private static Status[]? _statusArray = null; public PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers) { var peers = new Dictionary(); if (torrentPeers.Peers is not null) { foreach (var (key, peer) in torrentPeers.Peers) { var newPeer = CreatePeer(key, peer); peers[key] = newPeer; } } var peerList = new PeerList(peers); return peerList; } public MainData CreateMainData(QBitTorrentClient.Models.MainData mainData, string version) { var majorVersion = VersionHelper.GetMajorVersion(version); var torrents = new Dictionary(mainData.Torrents?.Count ?? 0); if (mainData.Torrents is not null) { foreach (var (hash, torrent) in mainData.Torrents) { var newTorrent = CreateTorrent(hash, torrent); torrents[hash] = newTorrent; } } var tags = new List(); if (mainData.Tags is not null) { var seenTags = new HashSet(StringComparer.Ordinal); foreach (var tag in mainData.Tags) { var normalizedTag = NormalizeTag(tag); if (string.IsNullOrEmpty(normalizedTag) || !seenTags.Add(normalizedTag)) { continue; } tags.Add(normalizedTag); } } var categories = new Dictionary(mainData.Categories?.Count ?? 0); if (mainData.Categories is not null) { foreach (var (name, category) in mainData.Categories) { var newCategory = CreateCategory(category); categories[name] = newCategory; } } var trackers = new Dictionary>(mainData.Trackers?.Count ?? 0); if (mainData.Trackers is not null) { foreach (var (url, hashes) in mainData.Trackers) { trackers[url] = hashes; } } var serverState = CreateServerState(mainData.ServerState); var tagState = new Dictionary>(tags.Count + 2) { { FilterHelper.TAG_ALL, torrents.Keys.ToHashSet() }, { FilterHelper.TAG_UNTAGGED, torrents.Values.Where(t => FilterHelper.FilterTag(t, FilterHelper.TAG_UNTAGGED)).ToHashesHashSet() } }; foreach (var tag in tags) { tagState.Add(tag, torrents.Values.Where(t => FilterHelper.FilterTag(t, tag)).ToHashesHashSet()); } var categoriesState = new Dictionary>(categories.Count + 2); categoriesState.Add(FilterHelper.CATEGORY_ALL, torrents.Keys.ToHashSet()); categoriesState.Add(FilterHelper.CATEGORY_UNCATEGORIZED, torrents.Values.Where(t => FilterHelper.FilterCategory(t, FilterHelper.CATEGORY_UNCATEGORIZED, serverState.UseSubcategories)).ToHashesHashSet()); foreach (var category in categories.Keys) { categoriesState.Add(category, torrents.Values.Where(t => FilterHelper.FilterCategory(t, category, serverState.UseSubcategories)).ToHashesHashSet()); } var statuses = GetStatuses(majorVersion).ToArray(); var statusState = new Dictionary>(statuses.Length + 2); foreach (var status in statuses) { statusState.Add(status.ToString(), torrents.Values.Where(t => FilterHelper.FilterStatus(t, status)).ToHashesHashSet()); } var trackersState = new Dictionary>(trackers.Count + 2); trackersState.Add(FilterHelper.TRACKER_ALL, torrents.Keys.ToHashSet()); trackersState.Add(FilterHelper.TRACKER_TRACKERLESS, torrents.Values.Where(t => FilterHelper.FilterTracker(t, FilterHelper.TRACKER_TRACKERLESS)).ToHashesHashSet()); foreach (var tracker in trackers.Keys) { trackersState.Add(tracker, torrents.Values.Where(t => FilterHelper.FilterTracker(t, tracker)).ToHashesHashSet()); } var torrentList = new MainData(torrents, tags, categories, trackers, serverState, tagState, categoriesState, statusState, trackersState, majorVersion); return torrentList; } private static ServerState CreateServerState(QBitTorrentClient.Models.ServerState? serverState) { if (serverState is null) { return new ServerState(); } return new ServerState( serverState.AllTimeDownloaded.GetValueOrDefault(), serverState.AllTimeUploaded.GetValueOrDefault(), serverState.AverageTimeQueue.GetValueOrDefault(), serverState.ConnectionStatus!, serverState.DHTNodes.GetValueOrDefault(), serverState.DownloadInfoData.GetValueOrDefault(), serverState.DownloadInfoSpeed.GetValueOrDefault(), serverState.DownloadRateLimit.GetValueOrDefault(), serverState.FreeSpaceOnDisk.GetValueOrDefault(), serverState.GlobalRatio.GetValueOrDefault(), serverState.QueuedIOJobs.GetValueOrDefault(), serverState.Queuing.GetValueOrDefault(), serverState.ReadCacheHits.GetValueOrDefault(), serverState.ReadCacheOverload.GetValueOrDefault(), serverState.RefreshInterval.GetValueOrDefault(), serverState.TotalBuffersSize.GetValueOrDefault(), serverState.TotalPeerConnections.GetValueOrDefault(), serverState.TotalQueuedSize.GetValueOrDefault(), serverState.TotalWastedSession.GetValueOrDefault(), serverState.UploadInfoData.GetValueOrDefault(), serverState.UploadInfoSpeed.GetValueOrDefault(), serverState.UploadRateLimit.GetValueOrDefault(), serverState.UseAltSpeedLimits.GetValueOrDefault(), serverState.UseSubcategories.GetValueOrDefault(), serverState.WriteCacheOverload.GetValueOrDefault()); } public bool MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList, out bool filterChanged) { filterChanged = false; var dataChanged = false; if (mainData.CategoriesRemoved is not null) { foreach (var category in mainData.CategoriesRemoved) { if (torrentList.Categories.Remove(category)) { dataChanged = true; filterChanged = true; } if (torrentList.CategoriesState.Remove(category)) { filterChanged = true; } } } if (mainData.TagsRemoved is not null) { foreach (var tag in mainData.TagsRemoved) { var normalizedTag = NormalizeTag(tag); if (string.IsNullOrEmpty(normalizedTag)) { continue; } if (torrentList.TagState.Remove(normalizedTag)) { filterChanged = true; } torrentList.TagState.Remove(normalizedTag); } } if (mainData.TrackersRemoved is not null) { foreach (var tracker in mainData.TrackersRemoved) { if (torrentList.Trackers.Remove(tracker)) { dataChanged = true; filterChanged = true; } if (torrentList.TrackersState.Remove(tracker)) { filterChanged = true; } } } if (mainData.TorrentsRemoved is not null) { foreach (var hash in mainData.TorrentsRemoved) { if (torrentList.Torrents.Remove(hash)) { RemoveTorrentFromStates(torrentList, hash); dataChanged = true; filterChanged = true; } } } if (mainData.Categories is not null) { foreach (var (name, category) in mainData.Categories) { if (!torrentList.Categories.TryGetValue(name, out var existingCategory)) { var newCategory = CreateCategory(category); torrentList.Categories.Add(name, newCategory); dataChanged = true; filterChanged = true; } else if (UpdateCategory(existingCategory, category)) { dataChanged = true; } } } if (mainData.Tags is not null) { foreach (var tag in mainData.Tags) { var normalizedTag = NormalizeTag(tag); if (string.IsNullOrEmpty(normalizedTag)) { continue; } if (torrentList.Tags.Add(normalizedTag)) { dataChanged = true; filterChanged = true; } var matchingHashes = torrentList.Torrents .Where(pair => FilterHelper.FilterTag(pair.Value, normalizedTag)) .Select(pair => pair.Key) .ToHashSet(); torrentList.TagState[normalizedTag] = matchingHashes; } } if (mainData.Trackers is not null) { foreach (var (url, hashes) in mainData.Trackers) { if (!torrentList.Trackers.TryGetValue(url, out var existingHashes)) { torrentList.Trackers.Add(url, hashes); dataChanged = true; filterChanged = true; } else if (!existingHashes.SequenceEqual(hashes)) { torrentList.Trackers[url] = hashes; dataChanged = true; } } } if (mainData.Torrents is not null) { foreach (var (hash, torrent) in mainData.Torrents) { if (!torrentList.Torrents.TryGetValue(hash, out var existingTorrent)) { var newTorrent = CreateTorrent(hash, torrent); torrentList.Torrents.Add(hash, newTorrent); AddTorrentToStates(torrentList, hash, torrentList.MajorVersion); dataChanged = true; filterChanged = true; } else { var previousSnapshot = CreateSnapshot(existingTorrent); var updateResult = UpdateTorrent(existingTorrent, torrent); if (updateResult.FilterChanged) { UpdateTorrentStates(torrentList, hash, previousSnapshot, existingTorrent); filterChanged = true; } if (updateResult.DataChanged) { dataChanged = true; } } } } if (mainData.ServerState is not null) { if (UpdateServerState(torrentList.ServerState, mainData.ServerState)) { dataChanged = true; } } return dataChanged; } private static void AddTorrentToStates(MainData torrentList, string hash, int version) { if (!torrentList.Torrents.TryGetValue(hash, out var torrent)) { return; } torrentList.TagState[FilterHelper.TAG_ALL].Add(hash); UpdateTagStateForAddition(torrentList, torrent, hash); torrentList.CategoriesState[FilterHelper.CATEGORY_ALL].Add(hash); UpdateCategoryState(torrentList, torrent, hash, previousCategory: null); foreach (var status in GetStatuses(version)) { if (!torrentList.StatusState.TryGetValue(status.ToString(), out var statusSet)) { continue; } if (FilterHelper.FilterStatus(torrent, status)) { statusSet.Add(hash); } } torrentList.TrackersState[FilterHelper.TRACKER_ALL].Add(hash); UpdateTrackerState(torrentList, torrent, hash, previousTracker: null); } private static Status[] GetStatuses(int version) { if (_statusArray is not null) { return _statusArray; } if (version == 5) { _statusArray = Enum.GetValues().Where(s => s != Status.Paused).ToArray(); } else { _statusArray = Enum.GetValues().Where(s => s != Status.Stopped).ToArray(); } return _statusArray; } private static void UpdateTorrentStates(MainData torrentList, string hash, TorrentSnapshot previousSnapshot, Torrent updatedTorrent) { UpdateTagStateForUpdate(torrentList, hash, previousSnapshot.Tags, updatedTorrent.Tags); UpdateCategoryState(torrentList, updatedTorrent, hash, previousSnapshot.Category); UpdateStatusState(torrentList, hash, previousSnapshot.State, previousSnapshot.UploadSpeed, updatedTorrent.State, updatedTorrent.UploadSpeed); UpdateTrackerState(torrentList, updatedTorrent, hash, previousSnapshot.Tracker); } private static void RemoveTorrentFromStates(MainData torrentList, string hash) { if (!torrentList.Torrents.TryGetValue(hash, out var torrent)) { return; } var snapshot = CreateSnapshot(torrent); torrentList.TagState[FilterHelper.TAG_ALL].Remove(hash); UpdateTagStateForRemoval(torrentList, hash, snapshot.Tags); torrentList.CategoriesState[FilterHelper.CATEGORY_ALL].Remove(hash); UpdateCategoryStateForRemoval(torrentList, hash, snapshot.Category); foreach (var status in GetStatuses(torrentList.MajorVersion)) { if (!torrentList.StatusState.TryGetValue(status.ToString(), out var statusState)) { continue; } if (FilterHelper.FilterStatus(snapshot.State, snapshot.UploadSpeed, status)) { statusState.Remove(hash); } } torrentList.TrackersState[FilterHelper.TRACKER_ALL].Remove(hash); UpdateTrackerStateForRemoval(torrentList, hash, snapshot.Tracker); } private static bool UpdateServerState(ServerState existingServerState, QBitTorrentClient.Models.ServerState serverState) { var changed = false; if (serverState.AllTimeDownloaded.HasValue && existingServerState.AllTimeDownloaded != serverState.AllTimeDownloaded.Value) { existingServerState.AllTimeDownloaded = serverState.AllTimeDownloaded.Value; changed = true; } if (serverState.AllTimeUploaded.HasValue && existingServerState.AllTimeUploaded != serverState.AllTimeUploaded.Value) { existingServerState.AllTimeUploaded = serverState.AllTimeUploaded.Value; changed = true; } if (serverState.AverageTimeQueue.HasValue && existingServerState.AverageTimeQueue != serverState.AverageTimeQueue.Value) { existingServerState.AverageTimeQueue = serverState.AverageTimeQueue.Value; changed = true; } if (serverState.ConnectionStatus is not null && existingServerState.ConnectionStatus != serverState.ConnectionStatus) { existingServerState.ConnectionStatus = serverState.ConnectionStatus; changed = true; } if (serverState.DHTNodes.HasValue && existingServerState.DHTNodes != serverState.DHTNodes.Value) { existingServerState.DHTNodes = serverState.DHTNodes.Value; changed = true; } if (serverState.DownloadInfoData.HasValue && existingServerState.DownloadInfoData != serverState.DownloadInfoData.Value) { existingServerState.DownloadInfoData = serverState.DownloadInfoData.Value; changed = true; } if (serverState.DownloadInfoSpeed.HasValue && existingServerState.DownloadInfoSpeed != serverState.DownloadInfoSpeed.Value) { existingServerState.DownloadInfoSpeed = serverState.DownloadInfoSpeed.Value; changed = true; } if (serverState.DownloadRateLimit.HasValue && existingServerState.DownloadRateLimit != serverState.DownloadRateLimit.Value) { existingServerState.DownloadRateLimit = serverState.DownloadRateLimit.Value; changed = true; } if (serverState.FreeSpaceOnDisk.HasValue && existingServerState.FreeSpaceOnDisk != serverState.FreeSpaceOnDisk.Value) { existingServerState.FreeSpaceOnDisk = serverState.FreeSpaceOnDisk.Value; changed = true; } if (serverState.GlobalRatio.HasValue && existingServerState.GlobalRatio != serverState.GlobalRatio.Value) { existingServerState.GlobalRatio = serverState.GlobalRatio.Value; changed = true; } if (serverState.QueuedIOJobs.HasValue && existingServerState.QueuedIOJobs != serverState.QueuedIOJobs.Value) { existingServerState.QueuedIOJobs = serverState.QueuedIOJobs.Value; changed = true; } if (serverState.Queuing.HasValue && existingServerState.Queuing != serverState.Queuing.Value) { existingServerState.Queuing = serverState.Queuing.Value; changed = true; } if (serverState.ReadCacheHits.HasValue && existingServerState.ReadCacheHits != serverState.ReadCacheHits.Value) { existingServerState.ReadCacheHits = serverState.ReadCacheHits.Value; changed = true; } if (serverState.ReadCacheOverload.HasValue && existingServerState.ReadCacheOverload != serverState.ReadCacheOverload.Value) { existingServerState.ReadCacheOverload = serverState.ReadCacheOverload.Value; changed = true; } if (serverState.RefreshInterval.HasValue && existingServerState.RefreshInterval != serverState.RefreshInterval.Value) { existingServerState.RefreshInterval = serverState.RefreshInterval.Value; changed = true; } if (serverState.TotalBuffersSize.HasValue && existingServerState.TotalBuffersSize != serverState.TotalBuffersSize.Value) { existingServerState.TotalBuffersSize = serverState.TotalBuffersSize.Value; changed = true; } if (serverState.TotalPeerConnections.HasValue && existingServerState.TotalPeerConnections != serverState.TotalPeerConnections.Value) { existingServerState.TotalPeerConnections = serverState.TotalPeerConnections.Value; changed = true; } if (serverState.TotalQueuedSize.HasValue && existingServerState.TotalQueuedSize != serverState.TotalQueuedSize.Value) { existingServerState.TotalQueuedSize = serverState.TotalQueuedSize.Value; changed = true; } if (serverState.TotalWastedSession.HasValue && existingServerState.TotalWastedSession != serverState.TotalWastedSession.Value) { existingServerState.TotalWastedSession = serverState.TotalWastedSession.Value; changed = true; } if (serverState.UploadInfoData.HasValue && existingServerState.UploadInfoData != serverState.UploadInfoData.Value) { existingServerState.UploadInfoData = serverState.UploadInfoData.Value; changed = true; } if (serverState.UploadInfoSpeed.HasValue && existingServerState.UploadInfoSpeed != serverState.UploadInfoSpeed.Value) { existingServerState.UploadInfoSpeed = serverState.UploadInfoSpeed.Value; changed = true; } if (serverState.UploadRateLimit.HasValue && existingServerState.UploadRateLimit != serverState.UploadRateLimit.Value) { existingServerState.UploadRateLimit = serverState.UploadRateLimit.Value; changed = true; } if (serverState.UseAltSpeedLimits.HasValue && existingServerState.UseAltSpeedLimits != serverState.UseAltSpeedLimits.Value) { existingServerState.UseAltSpeedLimits = serverState.UseAltSpeedLimits.Value; changed = true; } if (serverState.UseSubcategories.HasValue && existingServerState.UseSubcategories != serverState.UseSubcategories.Value) { existingServerState.UseSubcategories = serverState.UseSubcategories.Value; changed = true; } if (serverState.WriteCacheOverload.HasValue && existingServerState.WriteCacheOverload != serverState.WriteCacheOverload.Value) { existingServerState.WriteCacheOverload = serverState.WriteCacheOverload.Value; changed = true; } return changed; } public void MergeTorrentPeers(QBitTorrentClient.Models.TorrentPeers torrentPeers, PeerList peerList) { if (torrentPeers.PeersRemoved is not null) { foreach (var key in torrentPeers.PeersRemoved) { peerList.Peers.Remove(key); } } if (torrentPeers.Peers is not null) { foreach (var (key, peer) in torrentPeers.Peers) { if (!peerList.Peers.TryGetValue(key, out var existingPeer)) { var newPeer = CreatePeer(key, peer); peerList.Peers.Add(key, newPeer); } else { UpdatePeer(existingPeer, peer); } } } } private static void UpdatePeer(Peer existingPeer, QBitTorrentClient.Models.Peer peer) { existingPeer.Client = peer.Client ?? existingPeer.Client; existingPeer.ClientId = peer.ClientId ?? existingPeer.ClientId; existingPeer.Connection = peer.Connection ?? existingPeer.Connection; existingPeer.Country = peer.Country ?? existingPeer.Country; existingPeer.CountryCode = peer.CountryCode ?? existingPeer.CountryCode; existingPeer.Downloaded = peer.Downloaded ?? existingPeer.Downloaded; existingPeer.DownloadSpeed = peer.DownloadSpeed ?? existingPeer.DownloadSpeed; existingPeer.Files = peer.Files ?? existingPeer.Files; existingPeer.Flags = peer.Flags ?? existingPeer.Flags; existingPeer.FlagsDescription = peer.FlagsDescription ?? existingPeer.FlagsDescription; existingPeer.IPAddress = peer.IPAddress ?? existingPeer.IPAddress; existingPeer.Port = peer.Port ?? existingPeer.Port; existingPeer.Progress = peer.Progress ?? existingPeer.Progress; existingPeer.Relevance = peer.Relevance ?? existingPeer.Relevance; existingPeer.Uploaded = peer.Uploaded ?? existingPeer.Uploaded; existingPeer.UploadSpeed = peer.UploadSpeed ?? existingPeer.UploadSpeed; } private static Category CreateCategory(QBitTorrentClient.Models.Category category) { return new Category(category.Name, category.SavePath!); } private static Peer CreatePeer(string key, QBitTorrentClient.Models.Peer peer) { return new Peer( key, peer.Client!, peer.ClientId!, peer.Connection!, peer.Country, peer.CountryCode, peer.Downloaded.GetValueOrDefault(), peer.DownloadSpeed.GetValueOrDefault(), peer.Files!, peer.Flags!, peer.FlagsDescription!, peer.IPAddress!, peer.Port.GetValueOrDefault(), peer.Progress.GetValueOrDefault(), peer.Relevance.GetValueOrDefault(), peer.Uploaded.GetValueOrDefault(), peer.UploadSpeed.GetValueOrDefault()); } public Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent) { var normalizedTags = torrent.Tags? .Select(NormalizeTag) .Where(static tag => !string.IsNullOrEmpty(tag)) .ToList() ?? new List(); return new Torrent( hash, torrent.AddedOn.GetValueOrDefault(), torrent.AmountLeft.GetValueOrDefault(), torrent.AutomaticTorrentManagement.GetValueOrDefault(), torrent.Availability.GetValueOrDefault(), torrent.Category!, torrent.Completed.GetValueOrDefault(), torrent.CompletionOn.GetValueOrDefault(), torrent.ContentPath!, torrent.DownloadLimit.GetValueOrDefault(), torrent.DownloadSpeed.GetValueOrDefault(), torrent.Downloaded.GetValueOrDefault(), torrent.DownloadedSession.GetValueOrDefault(), torrent.EstimatedTimeOfArrival.GetValueOrDefault(), torrent.FirstLastPiecePriority.GetValueOrDefault(), torrent.ForceStart.GetValueOrDefault(), torrent.InfoHashV1!, torrent.InfoHashV2!, torrent.LastActivity.GetValueOrDefault(), torrent.MagnetUri!, torrent.MaxRatio.GetValueOrDefault(), torrent.MaxSeedingTime.GetValueOrDefault(), torrent.Name!, torrent.NumberComplete.GetValueOrDefault(), torrent.NumberIncomplete.GetValueOrDefault(), torrent.NumberLeeches.GetValueOrDefault(), torrent.NumberSeeds.GetValueOrDefault(), torrent.Priority.GetValueOrDefault(), torrent.Progress.GetValueOrDefault(), torrent.Ratio.GetValueOrDefault(), torrent.RatioLimit.GetValueOrDefault(), torrent.SavePath!, torrent.SeedingTime.GetValueOrDefault(), torrent.SeedingTimeLimit.GetValueOrDefault(), torrent.SeenComplete.GetValueOrDefault(), torrent.SequentialDownload.GetValueOrDefault(), torrent.Size.GetValueOrDefault(), torrent.State!, torrent.SuperSeeding.GetValueOrDefault(), normalizedTags, torrent.TimeActive.GetValueOrDefault(), torrent.TotalSize.GetValueOrDefault(), torrent.Tracker!, torrent.UploadLimit.GetValueOrDefault(), torrent.Uploaded.GetValueOrDefault(), torrent.UploadedSession.GetValueOrDefault(), torrent.UploadSpeed.GetValueOrDefault(), torrent.Reannounce ?? 0, torrent.InactiveSeedingTimeLimit.GetValueOrDefault(), torrent.MaxInactiveSeedingTime.GetValueOrDefault()); } private static string NormalizeTag(string? tag) { if (string.IsNullOrEmpty(tag)) { return string.Empty; } var separatorIndex = tag.IndexOf('\t'); var normalized = (separatorIndex >= 0) ? tag[..separatorIndex] : tag; return normalized.Trim(); } private static TorrentSnapshot CreateSnapshot(Torrent torrent) { return new TorrentSnapshot( string.IsNullOrEmpty(torrent.Category) ? null : torrent.Category, torrent.Tags.ToList(), torrent.Tracker ?? string.Empty, torrent.State ?? string.Empty, torrent.UploadSpeed); } private readonly struct TorrentSnapshot { public TorrentSnapshot(string? category, List tags, string tracker, string state, long uploadSpeed) { Category = category; Tags = tags; Tracker = tracker; State = state; UploadSpeed = uploadSpeed; } public string? Category { get; } public IReadOnlyList Tags { get; } public string Tracker { get; } public string State { get; } public long UploadSpeed { get; } } private static void UpdateTagStateForAddition(MainData torrentList, Torrent torrent, string hash) { if (torrent.Tags.Count == 0) { torrentList.TagState[FilterHelper.TAG_UNTAGGED].Add(hash); return; } torrentList.TagState[FilterHelper.TAG_UNTAGGED].Remove(hash); foreach (var tag in torrent.Tags) { if (string.IsNullOrEmpty(tag)) { continue; } GetOrCreateTagSet(torrentList, tag).Add(hash); } } private static void UpdateTagStateForUpdate(MainData torrentList, string hash, IReadOnlyList previousTags, IList newTags) { UpdateTagStateForRemoval(torrentList, hash, previousTags); if (newTags.Count == 0) { torrentList.TagState[FilterHelper.TAG_UNTAGGED].Add(hash); return; } torrentList.TagState[FilterHelper.TAG_UNTAGGED].Remove(hash); foreach (var tag in newTags) { if (string.IsNullOrEmpty(tag)) { continue; } GetOrCreateTagSet(torrentList, tag).Add(hash); } } private static void UpdateTagStateForRemoval(MainData torrentList, string hash, IReadOnlyList previousTags) { torrentList.TagState[FilterHelper.TAG_UNTAGGED].Remove(hash); foreach (var tag in previousTags) { if (string.IsNullOrEmpty(tag)) { continue; } if (torrentList.TagState.TryGetValue(tag, out var set)) { set.Remove(hash); } } } private static void UpdateCategoryState(MainData torrentList, Torrent updatedTorrent, string hash, string? previousCategory) { var useSubcategories = torrentList.ServerState.UseSubcategories; if (!string.IsNullOrEmpty(previousCategory)) { foreach (var categoryKey in EnumerateCategoryKeys(previousCategory, useSubcategories)) { if (torrentList.CategoriesState.TryGetValue(categoryKey, out var set)) { set.Remove(hash); } } } else { torrentList.CategoriesState[FilterHelper.CATEGORY_UNCATEGORIZED].Remove(hash); } if (string.IsNullOrEmpty(updatedTorrent.Category)) { torrentList.CategoriesState[FilterHelper.CATEGORY_UNCATEGORIZED].Add(hash); return; } foreach (var categoryKey in EnumerateCategoryKeys(updatedTorrent.Category, useSubcategories)) { GetOrCreateCategorySet(torrentList, categoryKey).Add(hash); } } private static void UpdateCategoryStateForRemoval(MainData torrentList, string hash, string? previousCategory) { if (string.IsNullOrEmpty(previousCategory)) { torrentList.CategoriesState[FilterHelper.CATEGORY_UNCATEGORIZED].Remove(hash); return; } foreach (var categoryKey in EnumerateCategoryKeys(previousCategory, torrentList.ServerState.UseSubcategories)) { if (torrentList.CategoriesState.TryGetValue(categoryKey, out var set)) { set.Remove(hash); } } } private static void UpdateStatusState(MainData torrentList, string hash, string previousState, long previousUploadSpeed, string newState, long newUploadSpeed) { foreach (var status in GetStatuses(torrentList.MajorVersion)) { if (!torrentList.StatusState.TryGetValue(status.ToString(), out var statusSet)) { continue; } var wasMatch = FilterHelper.FilterStatus(previousState, previousUploadSpeed, status); var isMatch = FilterHelper.FilterStatus(newState, newUploadSpeed, status); if (wasMatch == isMatch) { continue; } if (wasMatch) { statusSet.Remove(hash); } if (isMatch) { statusSet.Add(hash); } } } private static void UpdateTrackerState(MainData torrentList, Torrent updatedTorrent, string hash, string? previousTracker) { if (!string.IsNullOrEmpty(previousTracker)) { if (torrentList.TrackersState.TryGetValue(previousTracker, out var oldSet)) { oldSet.Remove(hash); } } else { torrentList.TrackersState[FilterHelper.TRACKER_TRACKERLESS].Remove(hash); } var tracker = updatedTorrent.Tracker ?? string.Empty; if (string.IsNullOrEmpty(tracker)) { torrentList.TrackersState[FilterHelper.TRACKER_TRACKERLESS].Add(hash); return; } torrentList.TrackersState[FilterHelper.TRACKER_TRACKERLESS].Remove(hash); GetOrCreateTrackerSet(torrentList, tracker).Add(hash); } private static void UpdateTrackerStateForRemoval(MainData torrentList, string hash, string? previousTracker) { if (string.IsNullOrEmpty(previousTracker)) { torrentList.TrackersState[FilterHelper.TRACKER_TRACKERLESS].Remove(hash); return; } if (torrentList.TrackersState.TryGetValue(previousTracker, out var trackerSet)) { trackerSet.Remove(hash); } } private static IEnumerable EnumerateCategoryKeys(string category, bool useSubcategories) { if (string.IsNullOrEmpty(category)) { yield break; } yield return category; if (!useSubcategories) { yield break; } var current = category; while (true) { var separatorIndex = current.LastIndexOf('/'); if (separatorIndex < 0) { yield break; } current = current[..separatorIndex]; yield return current; } } private static HashSet GetOrCreateTagSet(MainData torrentList, string tag) { if (!torrentList.TagState.TryGetValue(tag, out var set)) { set = new HashSet(StringComparer.Ordinal); torrentList.TagState[tag] = set; } return set; } private static HashSet GetOrCreateCategorySet(MainData torrentList, string category) { if (!torrentList.CategoriesState.TryGetValue(category, out var set)) { set = new HashSet(StringComparer.Ordinal); torrentList.CategoriesState[category] = set; } return set; } private static HashSet GetOrCreateTrackerSet(MainData torrentList, string tracker) { if (!torrentList.TrackersState.TryGetValue(tracker, out var set)) { set = new HashSet(StringComparer.Ordinal); torrentList.TrackersState[tracker] = set; } return set; } private static bool UpdateCategory(Category existingCategory, QBitTorrentClient.Models.Category category) { if (category.SavePath is not null && existingCategory.SavePath != category.SavePath) { existingCategory.SavePath = category.SavePath; return true; } return false; } private readonly struct TorrentUpdateResult { public TorrentUpdateResult(bool dataChanged, bool filterChanged) { DataChanged = dataChanged; FilterChanged = filterChanged; } public bool DataChanged { get; } public bool FilterChanged { get; } } private static TorrentUpdateResult UpdateTorrent(Torrent existingTorrent, QBitTorrentClient.Models.Torrent torrent) { var dataChanged = false; var filterChanged = false; if (torrent.AddedOn.HasValue && existingTorrent.AddedOn != torrent.AddedOn.Value) { existingTorrent.AddedOn = torrent.AddedOn.Value; dataChanged = true; } if (torrent.AmountLeft.HasValue && existingTorrent.AmountLeft != torrent.AmountLeft.Value) { existingTorrent.AmountLeft = torrent.AmountLeft.Value; dataChanged = true; } if (torrent.AutomaticTorrentManagement.HasValue && existingTorrent.AutomaticTorrentManagement != torrent.AutomaticTorrentManagement.Value) { existingTorrent.AutomaticTorrentManagement = torrent.AutomaticTorrentManagement.Value; dataChanged = true; } if (torrent.Availability.HasValue && existingTorrent.Availability != torrent.Availability.Value) { existingTorrent.Availability = torrent.Availability.Value; dataChanged = true; } if (torrent.Category is not null && existingTorrent.Category != torrent.Category) { existingTorrent.Category = torrent.Category; dataChanged = true; filterChanged = true; } if (torrent.Completed.HasValue && existingTorrent.Completed != torrent.Completed.Value) { existingTorrent.Completed = torrent.Completed.Value; dataChanged = true; } if (torrent.CompletionOn.HasValue && existingTorrent.CompletionOn != torrent.CompletionOn.Value) { existingTorrent.CompletionOn = torrent.CompletionOn.Value; dataChanged = true; } if (torrent.ContentPath is not null && existingTorrent.ContentPath != torrent.ContentPath) { existingTorrent.ContentPath = torrent.ContentPath; dataChanged = true; } if (torrent.Downloaded.HasValue && existingTorrent.Downloaded != torrent.Downloaded.Value) { existingTorrent.Downloaded = torrent.Downloaded.Value; dataChanged = true; } if (torrent.DownloadedSession.HasValue && existingTorrent.DownloadedSession != torrent.DownloadedSession.Value) { existingTorrent.DownloadedSession = torrent.DownloadedSession.Value; dataChanged = true; } if (torrent.DownloadLimit.HasValue && existingTorrent.DownloadLimit != torrent.DownloadLimit.Value) { existingTorrent.DownloadLimit = torrent.DownloadLimit.Value; dataChanged = true; } if (torrent.DownloadSpeed.HasValue && existingTorrent.DownloadSpeed != torrent.DownloadSpeed.Value) { existingTorrent.DownloadSpeed = torrent.DownloadSpeed.Value; dataChanged = true; } if (torrent.EstimatedTimeOfArrival.HasValue && existingTorrent.EstimatedTimeOfArrival != torrent.EstimatedTimeOfArrival.Value) { existingTorrent.EstimatedTimeOfArrival = torrent.EstimatedTimeOfArrival.Value; dataChanged = true; } if (torrent.FirstLastPiecePriority.HasValue && existingTorrent.FirstLastPiecePriority != torrent.FirstLastPiecePriority.Value) { existingTorrent.FirstLastPiecePriority = torrent.FirstLastPiecePriority.Value; dataChanged = true; } if (torrent.ForceStart.HasValue && existingTorrent.ForceStart != torrent.ForceStart.Value) { existingTorrent.ForceStart = torrent.ForceStart.Value; dataChanged = true; } if (torrent.InfoHashV1 is not null && existingTorrent.InfoHashV1 != torrent.InfoHashV1) { existingTorrent.InfoHashV1 = torrent.InfoHashV1; dataChanged = true; } if (torrent.InfoHashV2 is not null && existingTorrent.InfoHashV2 != torrent.InfoHashV2) { existingTorrent.InfoHashV2 = torrent.InfoHashV2; dataChanged = true; } if (torrent.LastActivity.HasValue && existingTorrent.LastActivity != torrent.LastActivity.Value) { existingTorrent.LastActivity = torrent.LastActivity.Value; dataChanged = true; } if (torrent.MagnetUri is not null && existingTorrent.MagnetUri != torrent.MagnetUri) { existingTorrent.MagnetUri = torrent.MagnetUri; dataChanged = true; } if (torrent.MaxRatio.HasValue && existingTorrent.MaxRatio != torrent.MaxRatio.Value) { existingTorrent.MaxRatio = torrent.MaxRatio.Value; dataChanged = true; } if (torrent.MaxSeedingTime.HasValue && existingTorrent.MaxSeedingTime != torrent.MaxSeedingTime.Value) { existingTorrent.MaxSeedingTime = torrent.MaxSeedingTime.Value; dataChanged = true; } if (torrent.Name is not null && existingTorrent.Name != torrent.Name) { existingTorrent.Name = torrent.Name; dataChanged = true; filterChanged = true; } if (torrent.NumberComplete.HasValue && existingTorrent.NumberComplete != torrent.NumberComplete.Value) { existingTorrent.NumberComplete = torrent.NumberComplete.Value; dataChanged = true; } if (torrent.NumberIncomplete.HasValue && existingTorrent.NumberIncomplete != torrent.NumberIncomplete.Value) { existingTorrent.NumberIncomplete = torrent.NumberIncomplete.Value; dataChanged = true; } if (torrent.NumberLeeches.HasValue && existingTorrent.NumberLeeches != torrent.NumberLeeches.Value) { existingTorrent.NumberLeeches = torrent.NumberLeeches.Value; dataChanged = true; } if (torrent.NumberSeeds.HasValue && existingTorrent.NumberSeeds != torrent.NumberSeeds.Value) { existingTorrent.NumberSeeds = torrent.NumberSeeds.Value; dataChanged = true; } if (torrent.Priority.HasValue && existingTorrent.Priority != torrent.Priority.Value) { existingTorrent.Priority = torrent.Priority.Value; dataChanged = true; } if (torrent.Progress.HasValue && existingTorrent.Progress != torrent.Progress.Value) { existingTorrent.Progress = torrent.Progress.Value; dataChanged = true; } if (torrent.Ratio.HasValue && existingTorrent.Ratio != torrent.Ratio.Value) { existingTorrent.Ratio = torrent.Ratio.Value; dataChanged = true; } if (torrent.RatioLimit.HasValue && existingTorrent.RatioLimit != torrent.RatioLimit.Value) { existingTorrent.RatioLimit = torrent.RatioLimit.Value; dataChanged = true; } if (torrent.SavePath is not null && existingTorrent.SavePath != torrent.SavePath) { existingTorrent.SavePath = torrent.SavePath; dataChanged = true; } if (torrent.SeedingTime.HasValue && existingTorrent.SeedingTime != torrent.SeedingTime.Value) { existingTorrent.SeedingTime = torrent.SeedingTime.Value; dataChanged = true; } if (torrent.SeedingTimeLimit.HasValue && existingTorrent.SeedingTimeLimit != torrent.SeedingTimeLimit.Value) { existingTorrent.SeedingTimeLimit = torrent.SeedingTimeLimit.Value; dataChanged = true; } if (torrent.SeenComplete.HasValue && existingTorrent.SeenComplete != torrent.SeenComplete.Value) { existingTorrent.SeenComplete = torrent.SeenComplete.Value; dataChanged = true; } if (torrent.SequentialDownload.HasValue && existingTorrent.SequentialDownload != torrent.SequentialDownload.Value) { existingTorrent.SequentialDownload = torrent.SequentialDownload.Value; dataChanged = true; } if (torrent.Size.HasValue && existingTorrent.Size != torrent.Size.Value) { existingTorrent.Size = torrent.Size.Value; dataChanged = true; } if (torrent.State is not null && existingTorrent.State != torrent.State) { existingTorrent.State = torrent.State; dataChanged = true; filterChanged = true; } if (torrent.SuperSeeding.HasValue && existingTorrent.SuperSeeding != torrent.SuperSeeding.Value) { existingTorrent.SuperSeeding = torrent.SuperSeeding.Value; dataChanged = true; } if (torrent.Tags is not null) { var normalizedTags = torrent.Tags.Select(NormalizeTag) .Where(static tag => !string.IsNullOrEmpty(tag)) .ToList(); if (!existingTorrent.Tags.SequenceEqual(normalizedTags)) { existingTorrent.Tags.Clear(); existingTorrent.Tags.AddRange(normalizedTags); dataChanged = true; filterChanged = true; } } if (torrent.TimeActive.HasValue && existingTorrent.TimeActive != torrent.TimeActive.Value) { existingTorrent.TimeActive = torrent.TimeActive.Value; dataChanged = true; } if (torrent.TotalSize.HasValue && existingTorrent.TotalSize != torrent.TotalSize.Value) { existingTorrent.TotalSize = torrent.TotalSize.Value; dataChanged = true; } if (torrent.Tracker is not null && existingTorrent.Tracker != torrent.Tracker) { existingTorrent.Tracker = torrent.Tracker; dataChanged = true; filterChanged = true; } if (torrent.UploadLimit.HasValue && existingTorrent.UploadLimit != torrent.UploadLimit.Value) { existingTorrent.UploadLimit = torrent.UploadLimit.Value; dataChanged = true; } if (torrent.Uploaded.HasValue && existingTorrent.Uploaded != torrent.Uploaded.Value) { existingTorrent.Uploaded = torrent.Uploaded.Value; dataChanged = true; } if (torrent.UploadedSession.HasValue && existingTorrent.UploadedSession != torrent.UploadedSession.Value) { existingTorrent.UploadedSession = torrent.UploadedSession.Value; dataChanged = true; } var previousUploadSpeed = existingTorrent.UploadSpeed; if (torrent.UploadSpeed.HasValue && previousUploadSpeed != torrent.UploadSpeed.Value) { existingTorrent.UploadSpeed = torrent.UploadSpeed.Value; dataChanged = true; if ((previousUploadSpeed > 0) != (torrent.UploadSpeed.Value > 0)) { filterChanged = true; } } if (torrent.Reannounce.HasValue && existingTorrent.Reannounce != torrent.Reannounce.Value) { existingTorrent.Reannounce = torrent.Reannounce.Value; dataChanged = true; } if (torrent.InactiveSeedingTimeLimit.HasValue && existingTorrent.InactiveSeedingTimeLimit != torrent.InactiveSeedingTimeLimit.Value) { existingTorrent.InactiveSeedingTimeLimit = torrent.InactiveSeedingTimeLimit.Value; dataChanged = true; } if (torrent.MaxInactiveSeedingTime.HasValue && existingTorrent.MaxInactiveSeedingTime != torrent.MaxInactiveSeedingTime.Value) { existingTorrent.MaxInactiveSeedingTime = torrent.MaxInactiveSeedingTime.Value; dataChanged = true; } return new TorrentUpdateResult(dataChanged, filterChanged); } public Dictionary CreateContentsList(IReadOnlyList files) { return BuildContentsTree(files); } private static Dictionary BuildContentsTree(IReadOnlyList files) { var result = new Dictionary(); if (files.Count == 0) { return result; } var folderIndex = files.Min(f => f.Index) - 1; var nodes = new Dictionary(files.Count * 2); var root = new ContentTreeNode(null, null); foreach (var file in files) { var parent = root; string? parentPath = parent.Item?.Name; var segments = file.Name.Split(Extensions.DirectorySeparator); var directoriesLength = segments.Length - 1; for (var i = 0; i < directoriesLength; i++) { var folderName = segments[i]; if (folderName == ".unwanted") { continue; } var folderPath = string.IsNullOrEmpty(parentPath) ? folderName : string.Concat(parentPath, Extensions.DirectorySeparator, folderName); if (!nodes.TryGetValue(folderPath, out var folderNode)) { var level = (parent.Item?.Level ?? -1) + 1; var folderItem = new ContentItem(folderPath, folderName, folderIndex--, Priority.Normal, 0, 0, 0, true, level); folderNode = new ContentTreeNode(folderItem, parent); nodes[folderPath] = folderNode; parent.Children[folderPath] = folderNode; } parent = folderNode; parentPath = parent.Item!.Name; } var displayName = segments[^1]; var fileLevel = (parent.Item?.Level ?? -1) + 1; var fileItem = new ContentItem(file.Name, displayName, file.Index, (Priority)(int)file.Priority, file.Progress, file.Size, file.Availability, false, fileLevel); var fileNode = new ContentTreeNode(fileItem, parent); nodes[file.Name] = fileNode; parent.Children[fileItem.Name] = fileNode; } var folders = nodes.Values .Where(n => n.Item is not null && n.Item.IsFolder) .OrderByDescending(n => n.Item!.Level) .ToList(); foreach (var folder in folders) { var folderItem = folder.Item!; if (folder.Children.Count == 0) { folderItem.Size = 0; folderItem.Progress = 0; folderItem.Availability = 0; folderItem.Priority = Priority.Normal; continue; } long sizeSum = 0; double progressSum = 0; double availabilitySum = 0; var firstChild = true; var aggregatedPriority = Priority.Normal; foreach (var child in folder.Children.Values) { var childItem = child.Item!; sizeSum += childItem.Size; if (firstChild) { aggregatedPriority = childItem.Priority; firstChild = false; } else if (aggregatedPriority != childItem.Priority) { aggregatedPriority = Priority.Mixed; } if (childItem.Priority != Priority.DoNotDownload) { progressSum += childItem.Progress * childItem.Size; availabilitySum += childItem.Availability * childItem.Size; } } folderItem.Size = sizeSum; folderItem.Progress = sizeSum > 0 ? (float)(progressSum / sizeSum) : 0; folderItem.Availability = sizeSum > 0 ? (float)(availabilitySum / sizeSum) : 0; folderItem.Priority = firstChild ? Priority.Normal : aggregatedPriority; } foreach (var node in nodes.Values) { if (node.Item is null) { continue; } result[node.Item.Name] = node.Item; } return result; } private static bool UpdateContentItem(ContentItem destination, ContentItem source) { const float floatTolerance = 0.0001f; var changed = false; if (destination.Priority != source.Priority) { destination.Priority = source.Priority; changed = true; } if (System.Math.Abs(destination.Progress - source.Progress) > floatTolerance) { destination.Progress = source.Progress; changed = true; } if (destination.Size != source.Size) { destination.Size = source.Size; changed = true; } if (System.Math.Abs(destination.Availability - source.Availability) > floatTolerance) { destination.Availability = source.Availability; changed = true; } return changed; } private struct DirectoryAccumulator { public long TotalSize { get; private set; } private long _activeSize; private double _progressSum; private double _availabilitySum; private Priority? _priority; private bool _mixedPriority; public void Add(Priority priority, float progress, long size, float availability) { TotalSize += size; if (priority != Priority.DoNotDownload) { _activeSize += size; _progressSum += progress * size; _availabilitySum += availability * size; } if (!_priority.HasValue) { _priority = priority; } else if (_priority.Value != priority) { _mixedPriority = true; } } public Priority ResolvePriority() { if (_mixedPriority) { return Priority.Mixed; } return _priority ?? Priority.Normal; } public float ResolveProgress() { if (_activeSize == 0 || TotalSize == 0) { return 0f; } var value = _progressSum / _activeSize; if (value < 0) { return 0f; } if (value > 1) { return 1f; } return (float)value; } public float ResolveAvailability() { if (_activeSize == 0 || TotalSize == 0) { return 0f; } return (float)(_availabilitySum / _activeSize); } } private sealed class ContentTreeNode { public ContentTreeNode(ContentItem? item, ContentTreeNode? parent) { Item = item; Parent = parent; Children = new Dictionary(); } public ContentItem? Item { get; } public ContentTreeNode? Parent { get; } public Dictionary Children { get; } } public QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed) { if (original is null) { original = new QBitTorrentClient.Models.UpdatePreferences { AddToTopOfQueue = changed.AddToTopOfQueue, AddTrackers = changed.AddTrackers, AddTrackersEnabled = changed.AddTrackersEnabled, AltDlLimit = changed.AltDlLimit, AltUpLimit = changed.AltUpLimit, AlternativeWebuiEnabled = changed.AlternativeWebuiEnabled, AlternativeWebuiPath = changed.AlternativeWebuiPath, AnnounceIp = changed.AnnounceIp, AnnounceToAllTiers = changed.AnnounceToAllTiers, AnnounceToAllTrackers = changed.AnnounceToAllTrackers, AnonymousMode = changed.AnonymousMode, AsyncIoThreads = changed.AsyncIoThreads, AutoDeleteMode = changed.AutoDeleteMode, AutoTmmEnabled = changed.AutoTmmEnabled, AutorunEnabled = changed.AutorunEnabled, AutorunOnTorrentAddedEnabled = changed.AutorunOnTorrentAddedEnabled, AutorunOnTorrentAddedProgram = changed.AutorunOnTorrentAddedProgram, AutorunProgram = changed.AutorunProgram, BannedIPs = changed.BannedIPs, BdecodeDepthLimit = changed.BdecodeDepthLimit, BdecodeTokenLimit = changed.BdecodeTokenLimit, BittorrentProtocol = changed.BittorrentProtocol, BlockPeersOnPrivilegedPorts = changed.BlockPeersOnPrivilegedPorts, BypassAuthSubnetWhitelist = changed.BypassAuthSubnetWhitelist, BypassAuthSubnetWhitelistEnabled = changed.BypassAuthSubnetWhitelistEnabled, BypassLocalAuth = changed.BypassLocalAuth, CategoryChangedTmmEnabled = changed.CategoryChangedTmmEnabled, CheckingMemoryUse = changed.CheckingMemoryUse, ConnectionSpeed = changed.ConnectionSpeed, CurrentInterfaceAddress = changed.CurrentInterfaceAddress, CurrentInterfaceName = changed.CurrentInterfaceName, CurrentNetworkInterface = changed.CurrentNetworkInterface, Dht = changed.Dht, DiskCache = changed.DiskCache, DiskCacheTtl = changed.DiskCacheTtl, DiskIoReadMode = changed.DiskIoReadMode, DiskIoType = changed.DiskIoType, DiskIoWriteMode = changed.DiskIoWriteMode, DiskQueueSize = changed.DiskQueueSize, DlLimit = changed.DlLimit, DontCountSlowTorrents = changed.DontCountSlowTorrents, DyndnsDomain = changed.DyndnsDomain, DyndnsEnabled = changed.DyndnsEnabled, DyndnsPassword = changed.DyndnsPassword, DyndnsService = changed.DyndnsService, DyndnsUsername = changed.DyndnsUsername, EmbeddedTrackerPort = changed.EmbeddedTrackerPort, EmbeddedTrackerPortForwarding = changed.EmbeddedTrackerPortForwarding, EnableCoalesceReadWrite = changed.EnableCoalesceReadWrite, EnableEmbeddedTracker = changed.EnableEmbeddedTracker, EnableMultiConnectionsFromSameIp = changed.EnableMultiConnectionsFromSameIp, EnablePieceExtentAffinity = changed.EnablePieceExtentAffinity, EnableUploadSuggestions = changed.EnableUploadSuggestions, Encryption = changed.Encryption, ExcludedFileNames = changed.ExcludedFileNames, ExcludedFileNamesEnabled = changed.ExcludedFileNamesEnabled, ExportDir = changed.ExportDir, ExportDirFin = changed.ExportDirFin, FileLogAge = changed.FileLogAge, FileLogAgeType = changed.FileLogAgeType, FileLogBackupEnabled = changed.FileLogBackupEnabled, FileLogDeleteOld = changed.FileLogDeleteOld, FileLogEnabled = changed.FileLogEnabled, FileLogMaxSize = changed.FileLogMaxSize, FileLogPath = changed.FileLogPath, FilePoolSize = changed.FilePoolSize, HashingThreads = changed.HashingThreads, I2pAddress = changed.I2pAddress, I2pEnabled = changed.I2pEnabled, I2pInboundLength = changed.I2pInboundLength, I2pInboundQuantity = changed.I2pInboundQuantity, I2pMixedMode = changed.I2pMixedMode, I2pOutboundLength = changed.I2pOutboundLength, I2pOutboundQuantity = changed.I2pOutboundQuantity, I2pPort = changed.I2pPort, IdnSupportEnabled = changed.IdnSupportEnabled, IncompleteFilesExt = changed.IncompleteFilesExt, IpFilterEnabled = changed.IpFilterEnabled, IpFilterPath = changed.IpFilterPath, IpFilterTrackers = changed.IpFilterTrackers, LimitLanPeers = changed.LimitLanPeers, LimitTcpOverhead = changed.LimitTcpOverhead, LimitUtpRate = changed.LimitUtpRate, ListenPort = changed.ListenPort, Locale = changed.Locale, Lsd = changed.Lsd, MailNotificationAuthEnabled = changed.MailNotificationAuthEnabled, MailNotificationEmail = changed.MailNotificationEmail, MailNotificationEnabled = changed.MailNotificationEnabled, MailNotificationPassword = changed.MailNotificationPassword, MailNotificationSender = changed.MailNotificationSender, MailNotificationSmtp = changed.MailNotificationSmtp, MailNotificationSslEnabled = changed.MailNotificationSslEnabled, MailNotificationUsername = changed.MailNotificationUsername, MaxActiveCheckingTorrents = changed.MaxActiveCheckingTorrents, MaxActiveDownloads = changed.MaxActiveDownloads, MaxActiveTorrents = changed.MaxActiveTorrents, MaxActiveUploads = changed.MaxActiveUploads, MaxConcurrentHttpAnnounces = changed.MaxConcurrentHttpAnnounces, MaxConnec = changed.MaxConnec, MaxConnecPerTorrent = changed.MaxConnecPerTorrent, MaxInactiveSeedingTime = changed.MaxInactiveSeedingTime, MaxInactiveSeedingTimeEnabled = changed.MaxInactiveSeedingTimeEnabled, MaxRatio = changed.MaxRatio, MaxRatioAct = changed.MaxRatioAct, MaxRatioEnabled = changed.MaxRatioEnabled, MaxSeedingTime = changed.MaxSeedingTime, MaxSeedingTimeEnabled = changed.MaxSeedingTimeEnabled, MaxUploads = changed.MaxUploads, MaxUploadsPerTorrent = changed.MaxUploadsPerTorrent, MemoryWorkingSetLimit = changed.MemoryWorkingSetLimit, MergeTrackers = changed.MergeTrackers, OutgoingPortsMax = changed.OutgoingPortsMax, OutgoingPortsMin = changed.OutgoingPortsMin, PeerTos = changed.PeerTos, PeerTurnover = changed.PeerTurnover, PeerTurnoverCutoff = changed.PeerTurnoverCutoff, PeerTurnoverInterval = changed.PeerTurnoverInterval, PerformanceWarning = changed.PerformanceWarning, Pex = changed.Pex, PreallocateAll = changed.PreallocateAll, ProxyAuthEnabled = changed.ProxyAuthEnabled, ProxyBittorrent = changed.ProxyBittorrent, ProxyHostnameLookup = changed.ProxyHostnameLookup, ProxyIp = changed.ProxyIp, ProxyMisc = changed.ProxyMisc, ProxyPassword = changed.ProxyPassword, ProxyPeerConnections = changed.ProxyPeerConnections, ProxyPort = changed.ProxyPort, ProxyRss = changed.ProxyRss, ProxyType = changed.ProxyType, ProxyUsername = changed.ProxyUsername, QueueingEnabled = changed.QueueingEnabled, RandomPort = changed.RandomPort, ReannounceWhenAddressChanged = changed.ReannounceWhenAddressChanged, RecheckCompletedTorrents = changed.RecheckCompletedTorrents, RefreshInterval = changed.RefreshInterval, RequestQueueSize = changed.RequestQueueSize, ResolvePeerCountries = changed.ResolvePeerCountries, ResumeDataStorageType = changed.ResumeDataStorageType, RssAutoDownloadingEnabled = changed.RssAutoDownloadingEnabled, RssDownloadRepackProperEpisodes = changed.RssDownloadRepackProperEpisodes, RssMaxArticlesPerFeed = changed.RssMaxArticlesPerFeed, RssProcessingEnabled = changed.RssProcessingEnabled, RssRefreshInterval = changed.RssRefreshInterval, RssSmartEpisodeFilters = changed.RssSmartEpisodeFilters, SavePath = changed.SavePath, SavePathChangedTmmEnabled = changed.SavePathChangedTmmEnabled, SaveResumeDataInterval = changed.SaveResumeDataInterval, ScanDirs = changed.ScanDirs, ScheduleFromHour = changed.ScheduleFromHour, ScheduleFromMin = changed.ScheduleFromMin, ScheduleToHour = changed.ScheduleToHour, ScheduleToMin = changed.ScheduleToMin, SchedulerDays = changed.SchedulerDays, SchedulerEnabled = changed.SchedulerEnabled, SendBufferLowWatermark = changed.SendBufferLowWatermark, SendBufferWatermark = changed.SendBufferWatermark, SendBufferWatermarkFactor = changed.SendBufferWatermarkFactor, SlowTorrentDlRateThreshold = changed.SlowTorrentDlRateThreshold, SlowTorrentInactiveTimer = changed.SlowTorrentInactiveTimer, SlowTorrentUlRateThreshold = changed.SlowTorrentUlRateThreshold, SocketBacklogSize = changed.SocketBacklogSize, SocketReceiveBufferSize = changed.SocketReceiveBufferSize, SocketSendBufferSize = changed.SocketSendBufferSize, SsrfMitigation = changed.SsrfMitigation, StartPausedEnabled = changed.StartPausedEnabled, StopTrackerTimeout = changed.StopTrackerTimeout, TempPath = changed.TempPath, TempPathEnabled = changed.TempPathEnabled, TorrentChangedTmmEnabled = changed.TorrentChangedTmmEnabled, TorrentContentLayout = changed.TorrentContentLayout, TorrentFileSizeLimit = changed.TorrentFileSizeLimit, TorrentStopCondition = changed.TorrentStopCondition, UpLimit = changed.UpLimit, UploadChokingAlgorithm = changed.UploadChokingAlgorithm, UploadSlotsBehavior = changed.UploadSlotsBehavior, Upnp = changed.Upnp, UpnpLeaseDuration = changed.UpnpLeaseDuration, UseCategoryPathsInManualMode = changed.UseCategoryPathsInManualMode, UseHttps = changed.UseHttps, UseSubcategories = changed.UseSubcategories, UtpTcpMixedMode = changed.UtpTcpMixedMode, ValidateHttpsTrackerCertificate = changed.ValidateHttpsTrackerCertificate, WebUiAddress = changed.WebUiAddress, WebUiBanDuration = changed.WebUiBanDuration, WebUiClickjackingProtectionEnabled = changed.WebUiClickjackingProtectionEnabled, WebUiCsrfProtectionEnabled = changed.WebUiCsrfProtectionEnabled, WebUiCustomHttpHeaders = changed.WebUiCustomHttpHeaders, WebUiDomainList = changed.WebUiDomainList, WebUiHostHeaderValidationEnabled = changed.WebUiHostHeaderValidationEnabled, WebUiHttpsCertPath = changed.WebUiHttpsCertPath, WebUiHttpsKeyPath = changed.WebUiHttpsKeyPath, WebUiMaxAuthFailCount = changed.WebUiMaxAuthFailCount, WebUiPort = changed.WebUiPort, WebUiReverseProxiesList = changed.WebUiReverseProxiesList, WebUiReverseProxyEnabled = changed.WebUiReverseProxyEnabled, WebUiSecureCookieEnabled = changed.WebUiSecureCookieEnabled, WebUiSessionTimeout = changed.WebUiSessionTimeout, WebUiUpnp = changed.WebUiUpnp, WebUiUseCustomHttpHeadersEnabled = changed.WebUiUseCustomHttpHeadersEnabled, WebUiUsername = changed.WebUiUsername }; } else { original.AddToTopOfQueue = changed.AddToTopOfQueue ?? original.AddToTopOfQueue; original.AddTrackers = changed.AddTrackers ?? original.AddTrackers; original.AddTrackersEnabled = changed.AddTrackersEnabled ?? original.AddTrackersEnabled; original.AltDlLimit = changed.AltDlLimit ?? original.AltDlLimit; original.AltUpLimit = changed.AltUpLimit ?? original.AltUpLimit; original.AlternativeWebuiEnabled = changed.AlternativeWebuiEnabled ?? original.AlternativeWebuiEnabled; original.AlternativeWebuiPath = changed.AlternativeWebuiPath ?? original.AlternativeWebuiPath; original.AnnounceIp = changed.AnnounceIp ?? original.AnnounceIp; original.AnnounceToAllTiers = changed.AnnounceToAllTiers ?? original.AnnounceToAllTiers; original.AnnounceToAllTrackers = changed.AnnounceToAllTrackers ?? original.AnnounceToAllTrackers; original.AnonymousMode = changed.AnonymousMode ?? original.AnonymousMode; original.AsyncIoThreads = changed.AsyncIoThreads ?? original.AsyncIoThreads; original.AutoDeleteMode = changed.AutoDeleteMode ?? original.AutoDeleteMode; original.AutoTmmEnabled = changed.AutoTmmEnabled ?? original.AutoTmmEnabled; original.AutorunEnabled = changed.AutorunEnabled ?? original.AutorunEnabled; original.AutorunOnTorrentAddedEnabled = changed.AutorunOnTorrentAddedEnabled ?? original.AutorunOnTorrentAddedEnabled; original.AutorunOnTorrentAddedProgram = changed.AutorunOnTorrentAddedProgram ?? original.AutorunOnTorrentAddedProgram; original.AutorunProgram = changed.AutorunProgram ?? original.AutorunProgram; original.BannedIPs = changed.BannedIPs ?? original.BannedIPs; original.BdecodeDepthLimit = changed.BdecodeDepthLimit ?? original.BdecodeDepthLimit; original.BdecodeTokenLimit = changed.BdecodeTokenLimit ?? original.BdecodeTokenLimit; original.BittorrentProtocol = changed.BittorrentProtocol ?? original.BittorrentProtocol; original.BlockPeersOnPrivilegedPorts = changed.BlockPeersOnPrivilegedPorts ?? original.BlockPeersOnPrivilegedPorts; original.BypassAuthSubnetWhitelist = changed.BypassAuthSubnetWhitelist ?? original.BypassAuthSubnetWhitelist; original.BypassAuthSubnetWhitelistEnabled = changed.BypassAuthSubnetWhitelistEnabled ?? original.BypassAuthSubnetWhitelistEnabled; original.BypassLocalAuth = changed.BypassLocalAuth ?? original.BypassLocalAuth; original.CategoryChangedTmmEnabled = changed.CategoryChangedTmmEnabled ?? original.CategoryChangedTmmEnabled; original.CheckingMemoryUse = changed.CheckingMemoryUse ?? original.CheckingMemoryUse; original.ConnectionSpeed = changed.ConnectionSpeed ?? original.ConnectionSpeed; original.CurrentInterfaceAddress = changed.CurrentInterfaceAddress ?? original.CurrentInterfaceAddress; original.CurrentInterfaceName = changed.CurrentInterfaceName ?? original.CurrentInterfaceName; original.CurrentNetworkInterface = changed.CurrentNetworkInterface ?? original.CurrentNetworkInterface; original.Dht = changed.Dht ?? original.Dht; original.DiskCache = changed.DiskCache ?? original.DiskCache; original.DiskCacheTtl = changed.DiskCacheTtl ?? original.DiskCacheTtl; original.DiskIoReadMode = changed.DiskIoReadMode ?? original.DiskIoReadMode; original.DiskIoType = changed.DiskIoType ?? original.DiskIoType; original.DiskIoWriteMode = changed.DiskIoWriteMode ?? original.DiskIoWriteMode; original.DiskQueueSize = changed.DiskQueueSize ?? original.DiskQueueSize; original.DlLimit = changed.DlLimit ?? original.DlLimit; original.DontCountSlowTorrents = changed.DontCountSlowTorrents ?? original.DontCountSlowTorrents; original.DyndnsDomain = changed.DyndnsDomain ?? original.DyndnsDomain; original.DyndnsEnabled = changed.DyndnsEnabled ?? original.DyndnsEnabled; original.DyndnsPassword = changed.DyndnsPassword ?? original.DyndnsPassword; original.DyndnsService = changed.DyndnsService ?? original.DyndnsService; original.DyndnsUsername = changed.DyndnsUsername ?? original.DyndnsUsername; original.EmbeddedTrackerPort = changed.EmbeddedTrackerPort ?? original.EmbeddedTrackerPort; original.EmbeddedTrackerPortForwarding = changed.EmbeddedTrackerPortForwarding ?? original.EmbeddedTrackerPortForwarding; original.EnableCoalesceReadWrite = changed.EnableCoalesceReadWrite ?? original.EnableCoalesceReadWrite; original.EnableEmbeddedTracker = changed.EnableEmbeddedTracker ?? original.EnableEmbeddedTracker; original.EnableMultiConnectionsFromSameIp = changed.EnableMultiConnectionsFromSameIp ?? original.EnableMultiConnectionsFromSameIp; original.EnablePieceExtentAffinity = changed.EnablePieceExtentAffinity ?? original.EnablePieceExtentAffinity; original.EnableUploadSuggestions = changed.EnableUploadSuggestions ?? original.EnableUploadSuggestions; original.Encryption = changed.Encryption ?? original.Encryption; original.ExcludedFileNames = changed.ExcludedFileNames ?? original.ExcludedFileNames; original.ExcludedFileNamesEnabled = changed.ExcludedFileNamesEnabled ?? original.ExcludedFileNamesEnabled; original.ExportDir = changed.ExportDir ?? original.ExportDir; original.ExportDirFin = changed.ExportDirFin ?? original.ExportDirFin; original.FileLogAge = changed.FileLogAge ?? original.FileLogAge; original.FileLogAgeType = changed.FileLogAgeType ?? original.FileLogAgeType; original.FileLogBackupEnabled = changed.FileLogBackupEnabled ?? original.FileLogBackupEnabled; original.FileLogDeleteOld = changed.FileLogDeleteOld ?? original.FileLogDeleteOld; original.FileLogEnabled = changed.FileLogEnabled ?? original.FileLogEnabled; original.FileLogMaxSize = changed.FileLogMaxSize ?? original.FileLogMaxSize; original.FileLogPath = changed.FileLogPath ?? original.FileLogPath; original.FilePoolSize = changed.FilePoolSize ?? original.FilePoolSize; original.HashingThreads = changed.HashingThreads ?? original.HashingThreads; original.I2pAddress = changed.I2pAddress ?? original.I2pAddress; original.I2pEnabled = changed.I2pEnabled ?? original.I2pEnabled; original.I2pInboundLength = changed.I2pInboundLength ?? original.I2pInboundLength; original.I2pInboundQuantity = changed.I2pInboundQuantity ?? original.I2pInboundQuantity; original.I2pMixedMode = changed.I2pMixedMode ?? original.I2pMixedMode; original.I2pOutboundLength = changed.I2pOutboundLength ?? original.I2pOutboundLength; original.I2pOutboundQuantity = changed.I2pOutboundQuantity ?? original.I2pOutboundQuantity; original.I2pPort = changed.I2pPort ?? original.I2pPort; original.IdnSupportEnabled = changed.IdnSupportEnabled ?? original.IdnSupportEnabled; original.IncompleteFilesExt = changed.IncompleteFilesExt ?? original.IncompleteFilesExt; original.IpFilterEnabled = changed.IpFilterEnabled ?? original.IpFilterEnabled; original.IpFilterPath = changed.IpFilterPath ?? original.IpFilterPath; original.IpFilterTrackers = changed.IpFilterTrackers ?? original.IpFilterTrackers; original.LimitLanPeers = changed.LimitLanPeers ?? original.LimitLanPeers; original.LimitTcpOverhead = changed.LimitTcpOverhead ?? original.LimitTcpOverhead; original.LimitUtpRate = changed.LimitUtpRate ?? original.LimitUtpRate; original.ListenPort = changed.ListenPort ?? original.ListenPort; original.Locale = changed.Locale ?? original.Locale; original.Lsd = changed.Lsd ?? original.Lsd; original.MailNotificationAuthEnabled = changed.MailNotificationAuthEnabled ?? original.MailNotificationAuthEnabled; original.MailNotificationEmail = changed.MailNotificationEmail ?? original.MailNotificationEmail; original.MailNotificationEnabled = changed.MailNotificationEnabled ?? original.MailNotificationEnabled; original.MailNotificationPassword = changed.MailNotificationPassword ?? original.MailNotificationPassword; original.MailNotificationSender = changed.MailNotificationSender ?? original.MailNotificationSender; original.MailNotificationSmtp = changed.MailNotificationSmtp ?? original.MailNotificationSmtp; original.MailNotificationSslEnabled = changed.MailNotificationSslEnabled ?? original.MailNotificationSslEnabled; original.MailNotificationUsername = changed.MailNotificationUsername ?? original.MailNotificationUsername; original.MaxActiveCheckingTorrents = changed.MaxActiveCheckingTorrents ?? original.MaxActiveCheckingTorrents; original.MaxActiveDownloads = changed.MaxActiveDownloads ?? original.MaxActiveDownloads; original.MaxActiveTorrents = changed.MaxActiveTorrents ?? original.MaxActiveTorrents; original.MaxActiveUploads = changed.MaxActiveUploads ?? original.MaxActiveUploads; original.MaxConcurrentHttpAnnounces = changed.MaxConcurrentHttpAnnounces ?? original.MaxConcurrentHttpAnnounces; original.MaxConnec = changed.MaxConnec ?? original.MaxConnec; original.MaxConnecPerTorrent = changed.MaxConnecPerTorrent ?? original.MaxConnecPerTorrent; original.MaxInactiveSeedingTime = changed.MaxInactiveSeedingTime ?? original.MaxInactiveSeedingTime; original.MaxInactiveSeedingTimeEnabled = changed.MaxInactiveSeedingTimeEnabled ?? original.MaxInactiveSeedingTimeEnabled; original.MaxRatio = changed.MaxRatio ?? original.MaxRatio; original.MaxRatioAct = changed.MaxRatioAct ?? original.MaxRatioAct; original.MaxRatioEnabled = changed.MaxRatioEnabled ?? original.MaxRatioEnabled; original.MaxSeedingTime = changed.MaxSeedingTime ?? original.MaxSeedingTime; original.MaxSeedingTimeEnabled = changed.MaxSeedingTimeEnabled ?? original.MaxSeedingTimeEnabled; original.MaxUploads = changed.MaxUploads ?? original.MaxUploads; original.MaxUploadsPerTorrent = changed.MaxUploadsPerTorrent ?? original.MaxUploadsPerTorrent; original.MemoryWorkingSetLimit = changed.MemoryWorkingSetLimit ?? original.MemoryWorkingSetLimit; original.MergeTrackers = changed.MergeTrackers ?? original.MergeTrackers; original.OutgoingPortsMax = changed.OutgoingPortsMax ?? original.OutgoingPortsMax; original.OutgoingPortsMin = changed.OutgoingPortsMin ?? original.OutgoingPortsMin; original.PeerTos = changed.PeerTos ?? original.PeerTos; original.PeerTurnover = changed.PeerTurnover ?? original.PeerTurnover; original.PeerTurnoverCutoff = changed.PeerTurnoverCutoff ?? original.PeerTurnoverCutoff; original.PeerTurnoverInterval = changed.PeerTurnoverInterval ?? original.PeerTurnoverInterval; original.PerformanceWarning = changed.PerformanceWarning ?? original.PerformanceWarning; original.Pex = changed.Pex ?? original.Pex; original.PreallocateAll = changed.PreallocateAll ?? original.PreallocateAll; original.ProxyAuthEnabled = changed.ProxyAuthEnabled ?? original.ProxyAuthEnabled; original.ProxyBittorrent = changed.ProxyBittorrent ?? original.ProxyBittorrent; original.ProxyHostnameLookup = changed.ProxyHostnameLookup ?? original.ProxyHostnameLookup; original.ProxyIp = changed.ProxyIp ?? original.ProxyIp; original.ProxyMisc = changed.ProxyMisc ?? original.ProxyMisc; original.ProxyPassword = changed.ProxyPassword ?? original.ProxyPassword; original.ProxyPeerConnections = changed.ProxyPeerConnections ?? original.ProxyPeerConnections; original.ProxyPort = changed.ProxyPort ?? original.ProxyPort; original.ProxyRss = changed.ProxyRss ?? original.ProxyRss; original.ProxyType = changed.ProxyType ?? original.ProxyType; original.ProxyUsername = changed.ProxyUsername ?? original.ProxyUsername; original.QueueingEnabled = changed.QueueingEnabled ?? original.QueueingEnabled; original.RandomPort = changed.RandomPort ?? original.RandomPort; original.ReannounceWhenAddressChanged = changed.ReannounceWhenAddressChanged ?? original.ReannounceWhenAddressChanged; original.RecheckCompletedTorrents = changed.RecheckCompletedTorrents ?? original.RecheckCompletedTorrents; original.RefreshInterval = changed.RefreshInterval ?? original.RefreshInterval; original.RequestQueueSize = changed.RequestQueueSize ?? original.RequestQueueSize; original.ResolvePeerCountries = changed.ResolvePeerCountries ?? original.ResolvePeerCountries; original.ResumeDataStorageType = changed.ResumeDataStorageType ?? original.ResumeDataStorageType; original.RssAutoDownloadingEnabled = changed.RssAutoDownloadingEnabled ?? original.RssAutoDownloadingEnabled; original.RssDownloadRepackProperEpisodes = changed.RssDownloadRepackProperEpisodes ?? original.RssDownloadRepackProperEpisodes; original.RssMaxArticlesPerFeed = changed.RssMaxArticlesPerFeed ?? original.RssMaxArticlesPerFeed; original.RssProcessingEnabled = changed.RssProcessingEnabled ?? original.RssProcessingEnabled; original.RssRefreshInterval = changed.RssRefreshInterval ?? original.RssRefreshInterval; original.RssSmartEpisodeFilters = changed.RssSmartEpisodeFilters ?? original.RssSmartEpisodeFilters; original.SavePath = changed.SavePath ?? original.SavePath; original.SavePathChangedTmmEnabled = changed.SavePathChangedTmmEnabled ?? original.SavePathChangedTmmEnabled; original.SaveResumeDataInterval = changed.SaveResumeDataInterval ?? original.SaveResumeDataInterval; original.ScanDirs = changed.ScanDirs ?? original.ScanDirs; original.ScheduleFromHour = changed.ScheduleFromHour ?? original.ScheduleFromHour; original.ScheduleFromMin = changed.ScheduleFromMin ?? original.ScheduleFromMin; original.ScheduleToHour = changed.ScheduleToHour ?? original.ScheduleToHour; original.ScheduleToMin = changed.ScheduleToMin ?? original.ScheduleToMin; original.SchedulerDays = changed.SchedulerDays ?? original.SchedulerDays; original.SchedulerEnabled = changed.SchedulerEnabled ?? original.SchedulerEnabled; original.SendBufferLowWatermark = changed.SendBufferLowWatermark ?? original.SendBufferLowWatermark; original.SendBufferWatermark = changed.SendBufferWatermark ?? original.SendBufferWatermark; original.SendBufferWatermarkFactor = changed.SendBufferWatermarkFactor ?? original.SendBufferWatermarkFactor; original.SlowTorrentDlRateThreshold = changed.SlowTorrentDlRateThreshold ?? original.SlowTorrentDlRateThreshold; original.SlowTorrentInactiveTimer = changed.SlowTorrentInactiveTimer ?? original.SlowTorrentInactiveTimer; original.SlowTorrentUlRateThreshold = changed.SlowTorrentUlRateThreshold ?? original.SlowTorrentUlRateThreshold; original.SocketBacklogSize = changed.SocketBacklogSize ?? original.SocketBacklogSize; original.SocketReceiveBufferSize = changed.SocketReceiveBufferSize ?? original.SocketReceiveBufferSize; original.SocketSendBufferSize = changed.SocketSendBufferSize ?? original.SocketSendBufferSize; original.SsrfMitigation = changed.SsrfMitigation ?? original.SsrfMitigation; original.StartPausedEnabled = changed.StartPausedEnabled ?? original.StartPausedEnabled; original.StopTrackerTimeout = changed.StopTrackerTimeout ?? original.StopTrackerTimeout; original.TempPath = changed.TempPath ?? original.TempPath; original.TempPathEnabled = changed.TempPathEnabled ?? original.TempPathEnabled; original.TorrentChangedTmmEnabled = changed.TorrentChangedTmmEnabled ?? original.TorrentChangedTmmEnabled; original.TorrentContentLayout = changed.TorrentContentLayout ?? original.TorrentContentLayout; original.TorrentFileSizeLimit = changed.TorrentFileSizeLimit ?? original.TorrentFileSizeLimit; original.TorrentStopCondition = changed.TorrentStopCondition ?? original.TorrentStopCondition; original.UpLimit = changed.UpLimit ?? original.UpLimit; original.UploadChokingAlgorithm = changed.UploadChokingAlgorithm ?? original.UploadChokingAlgorithm; original.UploadSlotsBehavior = changed.UploadSlotsBehavior ?? original.UploadSlotsBehavior; original.Upnp = changed.Upnp ?? original.Upnp; original.UpnpLeaseDuration = changed.UpnpLeaseDuration ?? original.UpnpLeaseDuration; original.UseCategoryPathsInManualMode = changed.UseCategoryPathsInManualMode ?? original.UseCategoryPathsInManualMode; original.UseHttps = changed.UseHttps ?? original.UseHttps; original.UseSubcategories = changed.UseSubcategories ?? original.UseSubcategories; original.UtpTcpMixedMode = changed.UtpTcpMixedMode ?? original.UtpTcpMixedMode; original.ValidateHttpsTrackerCertificate = changed.ValidateHttpsTrackerCertificate ?? original.ValidateHttpsTrackerCertificate; original.WebUiAddress = changed.WebUiAddress ?? original.WebUiAddress; original.WebUiBanDuration = changed.WebUiBanDuration ?? original.WebUiBanDuration; original.WebUiClickjackingProtectionEnabled = changed.WebUiClickjackingProtectionEnabled ?? original.WebUiClickjackingProtectionEnabled; original.WebUiCsrfProtectionEnabled = changed.WebUiCsrfProtectionEnabled ?? original.WebUiCsrfProtectionEnabled; original.WebUiCustomHttpHeaders = changed.WebUiCustomHttpHeaders ?? original.WebUiCustomHttpHeaders; original.WebUiDomainList = changed.WebUiDomainList ?? original.WebUiDomainList; original.WebUiHostHeaderValidationEnabled = changed.WebUiHostHeaderValidationEnabled ?? original.WebUiHostHeaderValidationEnabled; original.WebUiHttpsCertPath = changed.WebUiHttpsCertPath ?? original.WebUiHttpsCertPath; original.WebUiHttpsKeyPath = changed.WebUiHttpsKeyPath ?? original.WebUiHttpsKeyPath; original.WebUiMaxAuthFailCount = changed.WebUiMaxAuthFailCount ?? original.WebUiMaxAuthFailCount; original.WebUiPort = changed.WebUiPort ?? original.WebUiPort; original.WebUiReverseProxiesList = changed.WebUiReverseProxiesList ?? original.WebUiReverseProxiesList; original.WebUiReverseProxyEnabled = changed.WebUiReverseProxyEnabled ?? original.WebUiReverseProxyEnabled; original.WebUiSecureCookieEnabled = changed.WebUiSecureCookieEnabled ?? original.WebUiSecureCookieEnabled; original.WebUiSessionTimeout = changed.WebUiSessionTimeout ?? original.WebUiSessionTimeout; original.WebUiUpnp = changed.WebUiUpnp ?? original.WebUiUpnp; original.WebUiUseCustomHttpHeadersEnabled = changed.WebUiUseCustomHttpHeadersEnabled ?? original.WebUiUseCustomHttpHeadersEnabled; original.WebUiUsername = changed.WebUiUsername ?? original.WebUiUsername; } return original; } public bool MergeContentsList(IReadOnlyList files, Dictionary contents) { if (files.Count == 0) { if (contents.Count == 0) { return false; } contents.Clear(); return true; } var hasChanges = false; var seenPaths = new HashSet(files.Count * 2); var directoryAccumulators = new Dictionary(); var minExistingIndex = contents.Count == 0 ? int.MaxValue : contents.Values.Min(c => c.Index); var minFileIndex = files.Min(f => f.Index); var nextFolderIndex = System.Math.Min(minExistingIndex, minFileIndex) - 1; foreach (var file in files) { var priority = (Priority)(int)file.Priority; var pathSegments = file.Name.Split(Extensions.DirectorySeparator); var level = pathSegments.Length - 1; var displayName = pathSegments[^1]; var filePath = file.Name; seenPaths.Add(filePath); if (contents.TryGetValue(filePath, out var existingFile)) { var updatedFile = new ContentItem(filePath, displayName, file.Index, priority, file.Progress, file.Size, file.Availability, false, level); if (UpdateContentItem(existingFile, updatedFile)) { hasChanges = true; } } else { var newFile = new ContentItem(filePath, displayName, file.Index, priority, file.Progress, file.Size, file.Availability, false, level); contents[filePath] = newFile; hasChanges = true; } string directoryPath = string.Empty; for (var i = 0; i < level; i++) { var segment = pathSegments[i]; if (segment == ".unwanted") { continue; } directoryPath = string.IsNullOrEmpty(directoryPath) ? segment : string.Concat(directoryPath, Extensions.DirectorySeparator, segment); seenPaths.Add(directoryPath); if (!contents.TryGetValue(directoryPath, out var directoryItem)) { var newDirectory = new ContentItem(directoryPath, segment, nextFolderIndex--, Priority.Normal, 0, 0, 0, true, i); contents[directoryPath] = newDirectory; hasChanges = true; } if (!directoryAccumulators.TryGetValue(directoryPath, out var accumulator)) { accumulator = new DirectoryAccumulator(); } accumulator.Add(priority, file.Progress, file.Size, file.Availability); directoryAccumulators[directoryPath] = accumulator; } } var keysToRemove = contents.Keys.Where(key => !seenPaths.Contains(key)).ToList(); if (keysToRemove.Count != 0) { hasChanges = true; foreach (var key in keysToRemove) { contents.Remove(key); } } foreach (var (directoryPath, accumulator) in directoryAccumulators) { if (!contents.TryGetValue(directoryPath, out var directoryItem)) { continue; } var updatedDirectory = new ContentItem( directoryPath, directoryItem.DisplayName, directoryItem.Index, accumulator.ResolvePriority(), accumulator.ResolveProgress(), accumulator.TotalSize, accumulator.ResolveAvailability(), true, directoryItem.Level); if (UpdateContentItem(directoryItem, updatedDirectory)) { hasChanges = true; } } return hasChanges; } public RssList CreateRssList(IReadOnlyDictionary rssItems) { var articles = new List(); var feeds = new Dictionary(); foreach (var (key, rssItem) in rssItems) { feeds.Add(key, new RssFeed(rssItem.HasError, rssItem.IsLoading, rssItem.LastBuildDate, rssItem.Title, rssItem.Uid, rssItem.Url)); if (rssItem.Articles is null) { continue; } foreach (var rssArticle in rssItem.Articles) { var article = new RssArticle( key, rssArticle.Category, rssArticle.Comments, rssArticle.Date!, rssArticle.Description, rssArticle.Id!, rssArticle.Link, rssArticle.Thumbnail, rssArticle.Title!, rssArticle.TorrentURL!, rssArticle.IsRead); articles.Add(article); } } return new RssList(feeds, articles); } } }