using System.Reactive.Linq; using Common.Extensions; using Serilog; using TrashLib.ExceptionTypes; using TrashLib.Sonarr.Api; using TrashLib.Sonarr.Api.Objects; using TrashLib.Sonarr.Config; using TrashLib.Sonarr.ReleaseProfile.Guide; namespace TrashLib.Sonarr.ReleaseProfile; internal class ReleaseProfileUpdater : IReleaseProfileUpdater { private readonly ISonarrApi _api; private readonly ISonarrCompatibility _compatibility; private readonly ISonarrGuideService _guide; private readonly ILogger _log; public ReleaseProfileUpdater( ILogger logger, ISonarrGuideService guide, ISonarrApi api, ISonarrCompatibility compatibility) { _log = logger; _guide = guide; _api = api; _compatibility = compatibility; } public async Task Process(bool isPreview, SonarrConfiguration config) { var profilesFromGuide = _guide.GetReleaseProfileData(); var filteredProfiles = new List<(ReleaseProfileData Profile, IReadOnlyCollection Tags)>(); var filterer = new ReleaseProfileDataFilterer(_log); var configProfiles = config.ReleaseProfiles.SelectMany(x => x.TrashIds.Select(y => (TrashId: y, Config: x))); foreach (var (trashId, configProfile) in configProfiles) { // For each release profile specified in our YAML config, find the matching profile in the guide. var selectedProfile = profilesFromGuide.FirstOrDefault(x => x.TrashId.EqualsIgnoreCase(trashId)); if (selectedProfile is null) { _log.Warning("A release profile with Trash ID {TrashId} does not exist", trashId); continue; } _log.Debug("Found Release Profile: {ProfileName} ({TrashId})", selectedProfile.Name, selectedProfile.TrashId); if (configProfile.Filter != null) { _log.Debug("This profile will be filtered"); var newProfile = filterer.FilterProfile(selectedProfile, configProfile.Filter); if (newProfile is not null) { selectedProfile = newProfile; } } if (isPreview) { Utils.PrintTermsAndScores(selectedProfile); continue; } filteredProfiles.Add((selectedProfile, configProfile.Tags)); } await ProcessReleaseProfiles(filteredProfiles); } private async Task ProcessReleaseProfiles( List<(ReleaseProfileData Profile, IReadOnlyCollection Tags)> profilesAndTags) { await DoVersionEnforcement(); // Obtain all of the existing release profiles first. If any were previously created by our program // here, we favor replacing those instead of creating new ones, which would just be mostly duplicates // (but with some differences, since there have likely been updates since the last run). var existingProfiles = await _api.GetReleaseProfiles(); foreach (var (profile, tags) in profilesAndTags) { // If tags were provided, ensure they exist. Tags that do not exist are added first, so that we // may specify them with the release profile request payload. var tagIds = await CreateTagsInSonarr(tags); var title = BuildProfileTitle(profile.Name); var profileToUpdate = GetProfileToUpdate(existingProfiles, title); if (profileToUpdate != null) { _log.Information("Update existing profile: {ProfileName}", title); await UpdateExistingProfile(profileToUpdate, profile, tagIds); } else { _log.Information("Create new profile: {ProfileName}", title); await CreateNewProfile(title, profile, tagIds); } } // Any profiles with `[Trash]` in front of their name are managed exclusively by Trash Updater. As such, if // there are any still in Sonarr that we didn't update, those are most certainly old and shouldn't be kept // around anymore. await DeleteOldManagedProfiles(profilesAndTags, existingProfiles); } private async Task DeleteOldManagedProfiles( IEnumerable<(ReleaseProfileData Profile, IReadOnlyCollection Tags)> profilesAndTags, IEnumerable sonarrProfiles) { var profiles = profilesAndTags.Select(x => x.Profile).ToList(); var sonarrProfilesToDelete = sonarrProfiles .Where(sonarrProfile => { return sonarrProfile.Name.StartsWithIgnoreCase("[Trash]") && !profiles.Any(profile => sonarrProfile.Name.EndsWithIgnoreCase(profile.Name)); }); foreach (var profile in sonarrProfilesToDelete) { _log.Information("Deleting old Trash release profile: {ProfileName}", profile.Name); await _api.DeleteReleaseProfile(profile.Id); } } private async Task> CreateTagsInSonarr(IReadOnlyCollection tags) { if (!tags.Any()) { return Array.Empty(); } var sonarrTags = await _api.GetTags(); await CreateMissingTags(sonarrTags, tags); return sonarrTags .Where(t => tags.Any(ct => ct.EqualsIgnoreCase(t.Label))) .Select(t => t.Id) .ToList(); } private async Task DoVersionEnforcement() { var capabilities = await _compatibility.Capabilities.LastAsync(); if (!capabilities.SupportsNamedReleaseProfiles) { throw new VersionException( $"Your Sonarr version {capabilities.Version} does not meet the minimum " + $"required version of {_compatibility.MinimumVersion} to use this program"); } } private async Task CreateMissingTags(ICollection sonarrTags, IEnumerable configTags) { var missingTags = configTags.Where(t => !sonarrTags.Any(t2 => t2.Label.EqualsIgnoreCase(t))); foreach (var tag in missingTags) { _log.Debug("Creating Tag: {Tag}", tag); var newTag = await _api.CreateTag(tag); sonarrTags.Add(newTag); } } private const string ProfileNamePrefix = "[Trash]"; private static string BuildProfileTitle(string profileName) { return $"{ProfileNamePrefix} {profileName}"; } private static SonarrReleaseProfile? GetProfileToUpdate(IEnumerable profiles, string profileName) { return profiles.FirstOrDefault(p => p.Name == profileName); } private static void SetupProfileRequestObject(SonarrReleaseProfile profileToUpdate, ReleaseProfileData profile, IReadOnlyCollection tagIds) { profileToUpdate.Preferred = profile.Preferred .SelectMany(x => x.Terms.Select(termData => new SonarrPreferredTerm(x.Score, termData.Term))) .ToList(); profileToUpdate.Ignored = profile.Ignored.Select(x => x.Term).ToList(); profileToUpdate.Required = profile.Required.Select(x => x.Term).ToList(); profileToUpdate.IncludePreferredWhenRenaming = profile.IncludePreferredWhenRenaming; profileToUpdate.Tags = tagIds; } private async Task UpdateExistingProfile(SonarrReleaseProfile profileToUpdate, ReleaseProfileData profile, IReadOnlyCollection tagIds) { _log.Debug("Update existing profile with id {ProfileId}", profileToUpdate.Id); SetupProfileRequestObject(profileToUpdate, profile, tagIds); await _api.UpdateReleaseProfile(profileToUpdate); } private async Task CreateNewProfile(string title, ReleaseProfileData profile, IReadOnlyCollection tagIds) { var newProfile = new SonarrReleaseProfile {Name = title, Enabled = true}; SetupProfileRequestObject(newProfile, profile, tagIds); await _api.CreateReleaseProfile(newProfile); } }