From 7a25f79fa8f94e96aa3a47147ff71255c3b57099 Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Fri, 7 Oct 2022 09:05:45 -0500 Subject: [PATCH] feat: Custom format lists are now grouped by category When using `--list-custom-formats` with either the `sonarr` or `radarr` subcommand, custom formats will be grouped according to the respective tables at the top of the "Collection of custom formats" page. --- CHANGELOG.md | 6 + src/TrashLib.TestLibrary/NewCf.cs | 6 +- .../CustomFormat/CachePersisterTest.cs | 6 +- .../Guide/CustomFormatCategoryParserTest.cs | 108 ++++++++++++++++++ .../Guide/CustomFormatGroupParserTest.cs | 87 -------------- .../Guide/CustomFormatLoaderTest.cs | 16 ++- .../Processors/GuideProcessorTest.cs | 2 +- .../GuideSteps/CustomFormatStepTest.cs | 5 +- src/TrashLib/Repo/IRepoPaths.cs | 2 + src/TrashLib/Repo/RepoPaths.cs | 4 +- src/TrashLib/Repo/RepoPathsFactory.cs | 5 +- .../Services/Common/GuideDataLister.cs | 16 ++- .../CustomFormat/CustomFormatAutofacModule.cs | 1 + .../Guide/CustomFormatCategoryItem.cs | 3 + ...arser.cs => CustomFormatCategoryParser.cs} | 35 ++---- .../CustomFormat/Guide/CustomFormatLoader.cs | 25 +++- .../CustomFormat/Guide/CustomFormatParser.cs | 4 +- .../Guide/ICustomFormatCategoryParser.cs | 8 ++ .../CustomFormat/Guide/ICustomFormatLoader.cs | 3 +- .../CustomFormat/Guide/ICustomFormatParser.cs | 2 +- .../CustomFormat/Models/CustomFormatData.cs | 8 +- .../Models/ProcessedCustomFormatData.cs | 2 +- .../Radarr/LocalRepoRadarrGuideService.cs | 4 +- .../Guide/LocalRepoSonarrGuideService.cs | 4 +- 24 files changed, 218 insertions(+), 144 deletions(-) create mode 100644 src/TrashLib.Tests/CustomFormat/Guide/CustomFormatCategoryParserTest.cs delete mode 100644 src/TrashLib.Tests/CustomFormat/Guide/CustomFormatGroupParserTest.cs create mode 100644 src/TrashLib/Services/CustomFormat/Guide/CustomFormatCategoryItem.cs rename src/TrashLib/Services/CustomFormat/Guide/{CustomFormatGroupParser.cs => CustomFormatCategoryParser.cs} (69%) create mode 100644 src/TrashLib/Services/CustomFormat/Guide/ICustomFormatCategoryParser.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 701db243..2277aa9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- For both Sonarr and Radarr, the custom formats in the output of `--list-custom-formats` are now + grouped by their category, as determined by the tables at the top of the "Collection of custom + formats" pages in the guide for each service. + ## [2.5.0] - 2022-09-11 ### Added diff --git a/src/TrashLib.TestLibrary/NewCf.cs b/src/TrashLib.TestLibrary/NewCf.cs index 1405a5e6..04306418 100644 --- a/src/TrashLib.TestLibrary/NewCf.cs +++ b/src/TrashLib.TestLibrary/NewCf.cs @@ -9,7 +9,7 @@ public static class NewCf public static CustomFormatData Data(string name, string trashId, int? score = null) { var json = JObject.Parse($"{{'name':'{name}'}}"); - return new CustomFormatData(name, trashId, score, new JObject(json)); + return new CustomFormatData("", name, trashId, score, new JObject(json)); } public static ProcessedCustomFormatData Processed(string name, string trashId, int? score = null) @@ -19,7 +19,7 @@ public static class NewCf public static ProcessedCustomFormatData Processed(string name, string trashId, int? score, JObject json) { - return new ProcessedCustomFormatData(new CustomFormatData(name, trashId, score, json)); + return new ProcessedCustomFormatData(new CustomFormatData("", name, trashId, score, json)); } public static ProcessedCustomFormatData Processed(string name, string trashId, JObject json) @@ -38,7 +38,7 @@ public static class NewCf public static ProcessedCustomFormatData Processed(string name, string trashId, JObject json, TrashIdMapping? cacheEntry) { - return new ProcessedCustomFormatData(new CustomFormatData(name, trashId, null, json)) + return new ProcessedCustomFormatData(new CustomFormatData("", name, trashId, null, json)) { CacheEntry = cacheEntry }; diff --git a/src/TrashLib.Tests/CustomFormat/CachePersisterTest.cs b/src/TrashLib.Tests/CustomFormat/CachePersisterTest.cs index c19e0cb6..85533694 100644 --- a/src/TrashLib.Tests/CustomFormat/CachePersisterTest.cs +++ b/src/TrashLib.Tests/CustomFormat/CachePersisterTest.cs @@ -1,6 +1,5 @@ using System.Collections.ObjectModel; using FluentAssertions; -using Newtonsoft.Json.Linq; using NSubstitute; using NUnit.Framework; using Serilog; @@ -9,6 +8,7 @@ using TrashLib.Services.CustomFormat; using TrashLib.Services.CustomFormat.Models; using TrashLib.Services.CustomFormat.Models.Cache; using TrashLib.Services.CustomFormat.Processors.PersistenceSteps; +using TrashLib.TestLibrary; namespace TrashLib.Tests.CustomFormat; @@ -31,7 +31,7 @@ public class CachePersisterTest private static ProcessedCustomFormatData QuickMakeCf(string cfName, string trashId, int cfId) { - var cf = new CustomFormatData(cfName, trashId, null, new JObject()); + var cf = NewCf.Data(cfName, trashId); return new ProcessedCustomFormatData(cf) { CacheEntry = new TrashIdMapping(trashId, cfName) {CustomFormatId = cfId} @@ -128,7 +128,7 @@ public class CachePersisterTest var customFormatData = new List { - new(new CustomFormatData("", "trashid", null, new JObject())) + new(NewCf.Data("", "trashid")) {CacheEntry = new TrashIdMapping("trashid", "cfname", 10)} }; diff --git a/src/TrashLib.Tests/CustomFormat/Guide/CustomFormatCategoryParserTest.cs b/src/TrashLib.Tests/CustomFormat/Guide/CustomFormatCategoryParserTest.cs new file mode 100644 index 00000000..c45d30e5 --- /dev/null +++ b/src/TrashLib.Tests/CustomFormat/Guide/CustomFormatCategoryParserTest.cs @@ -0,0 +1,108 @@ +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; +using AutoFixture.NUnit3; +using FluentAssertions; +using NUnit.Framework; +using TestLibrary.AutoFixture; +using TrashLib.Services.CustomFormat.Guide; +using TrashLib.Startup; + +namespace TrashLib.Tests.CustomFormat.Guide; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class CustomFormatCategoryParserTest +{ + [Test, AutoMockData] + public void It_works( + [Frozen] IAppPaths paths, + [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, + CustomFormatCategoryParser sut) + { + const string markdown = @" +## INDEX + +------ + +| Audio Advanced #1 | Audio Advanced #2 | Anime | Anime | +| ----------------------------------------- | ------------------------------- | --------------------------------------------------------------------- | ----------- | +| [TrueHD ATMOS](#truehd-atmos) | [FLAC](#flac) | [Anime Web Tier 01 (Muxers)](#anime-web-tier-01-muxers) | [v0](#v0) | +| [DTS X](#dts-x) | [PCM](#pcm) | [Anime Web Tier 02 (Top FanSubs)](#anime-web-tier-02-top-fansubs) | [v1](#v1) | +| [ATMOS (undefined)](#atmos-undefined) | [DTS-HD HRA](#dts-hd-hra) | [Anime Web Tier 03 (Official Subs)](#anime-web-tier-03-official-subs) | [v2](#v2) | +| [DD+ ATMOS](#dd-atmos) | [AAC](#aac) | [Anime Web Tier 04 (Official Subs)](#anime-web-tier-04-official-subs) | [v3](#v3) | +| [TrueHD](#truehd) | [DD](#dd) | [Anime Web Tier 05 (FanSubs)](#anime-web-tier-05-fansubs) | [v4](#v4) | +| [DTS-HD MA](#dts-hd-ma) | [MP3](#mp3) | [Anime Web Tier 06 (FanSubs)](#anime-web-tier-06-fansubs) | [VRV](#vrv) | +| [DD+](#ddplus) | [Opus](#opus) | [Anime Raws](#anime-raws) | | +| [DTS-ES](#dts-es) | | [Anime LQ Groups](#anime-lq-groups) | | +| [DTS](#dts) | | | | +| | | | | + +------ + +| Movie Versions | Unwanted | +| --------------------------------------------- | ---------------------------------- | +| [Hybrid](#hybrid) | [BR-DISK](#br-disk) | +| [Remaster](#remaster) | [EVO (no WEBDL)](#evo-no-webdl) | +| [4K Remaster](#4k-remaster) | [LQ](#lq) | +| [Special Editions](#special-edition) | [x265 (720/1080p)](#x265-7201080p) | +| [Criterion Collection](#criterion-collection) | [3D](#3d) | +| [Theatrical Cut](#theatrical-cut) | [No-RlsGroup](#no-rlsgroup) | +| [IMAX](#imax) | [Obfuscated](#obfuscated) | +| [IMAX Enhanced](#imax-enhanced) | [DV (WEBDL)](#dv-webdl) | +| | | + +------ +"; + + var file = paths.RepoDirectory + .SubDirectory("docs") + .SubDirectory("Radarr") + .File("Radarr-collection-of-custom-formats.md"); + + fs.AddFile(file.FullName, new MockFileData(markdown)); + + var result = sut.Parse(file); + + result.Select(x => x.CategoryName).Distinct() + .Should().BeEquivalentTo( + "Anime", + "Audio Advanced #1", + "Audio Advanced #2", + "Movie Versions", + "Unwanted" + ); + + result.Where(x => x.CategoryName == "Audio Advanced #1").Select(x => (x.CfName, x.CfAnchor)) + .Should().BeEquivalentTo(new[] + { + ("TrueHD ATMOS", "truehd-atmos"), + ("DTS X", "dts-x"), + ("ATMOS (undefined)", "atmos-undefined"), + ("DD+ ATMOS", "dd-atmos"), + ("TrueHD", "truehd"), + ("DTS-HD MA", "dts-hd-ma"), + ("DD+", "ddplus"), + ("DTS-ES", "dts-es"), + ("DTS", "dts") + }); + + result.Where(x => x.CategoryName == "Anime").Select(x => (x.CfName, x.CfAnchor)) + .Should().BeEquivalentTo(new[] + { + ("Anime Web Tier 01 (Muxers)", "anime-web-tier-01-muxers"), + ("Anime Web Tier 02 (Top FanSubs)", "anime-web-tier-02-top-fansubs"), + ("Anime Web Tier 03 (Official Subs)", "anime-web-tier-03-official-subs"), + ("Anime Web Tier 04 (Official Subs)", "anime-web-tier-04-official-subs"), + ("Anime Web Tier 05 (FanSubs)", "anime-web-tier-05-fansubs"), + ("Anime Web Tier 06 (FanSubs)", "anime-web-tier-06-fansubs"), + ("Anime Raws", "anime-raws"), + ("Anime LQ Groups", "anime-lq-groups"), + ("v0", "v0"), + ("v1", "v1"), + ("v2", "v2"), + ("v3", "v3"), + ("v4", "v4"), + ("VRV", "vrv") + }); + } +} diff --git a/src/TrashLib.Tests/CustomFormat/Guide/CustomFormatGroupParserTest.cs b/src/TrashLib.Tests/CustomFormat/Guide/CustomFormatGroupParserTest.cs deleted file mode 100644 index 0eb52100..00000000 --- a/src/TrashLib.Tests/CustomFormat/Guide/CustomFormatGroupParserTest.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.IO.Abstractions; -using System.IO.Abstractions.TestingHelpers; -using AutoFixture.NUnit3; -using FluentAssertions; -using NUnit.Framework; -using TestLibrary.AutoFixture; -using TrashLib.Services.CustomFormat.Guide; -using TrashLib.Startup; - -namespace TrashLib.Tests.CustomFormat.Guide; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class CustomFormatGroupParserTest -{ - [Test, AutoMockData] - public void It_works( - [Frozen] IAppPaths paths, - [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - CustomFormatGroupParser sut) - { - const string markdown = @" -## INDEX - ------- - -| Audio Advanced #1 | Audio Advanced #2 | -| ----------------------------------------- | ------------------------------- | -| [TrueHD ATMOS](#truehd-atmos) | [FLAC](#flac) | -| [DTS X](#dts-x) | [PCM](#pcm) | -| [ATMOS (undefined)](#atmos-undefined) | [DTS-HD HRA](#dts-hd-hra) | -| [DD+ ATMOS](#dd-atmos) | [AAC](#aac) | -| [TrueHD](#truehd) | [DD](#dd) | -| [DTS-HD MA](#dts-hd-ma) | [MP3](#mp3) | -| [DD+](#ddplus) | [Opus](#opus) | -| [DTS-ES](#dts-es) | | -| [DTS](#dts) | | -| | | - ------- - -| Movie Versions | Unwanted | -| --------------------------------------------- | ---------------------------------- | -| [Hybrid](#hybrid) | [BR-DISK](#br-disk) | -| [Remaster](#remaster) | [EVO (no WEBDL)](#evo-no-webdl) | -| [4K Remaster](#4k-remaster) | [LQ](#lq) | -| [Special Editions](#special-edition) | [x265 (720/1080p)](#x265-7201080p) | -| [Criterion Collection](#criterion-collection) | [3D](#3d) | -| [Theatrical Cut](#theatrical-cut) | [No-RlsGroup](#no-rlsgroup) | -| [IMAX](#imax) | [Obfuscated](#obfuscated) | -| [IMAX Enhanced](#imax-enhanced) | [DV (WEBDL)](#dv-webdl) | -| | | - ------- -"; - - var file = paths.RepoDirectory - .SubDirectory("docs") - .SubDirectory("Radarr") - .File("Radarr-collection-of-custom-formats.md"); - - fs.AddFile(file.FullName, new MockFileData(markdown)); - - var result = sut.Parse(); - - result.Keys.Should().BeEquivalentTo( - "Audio Advanced #1", - "Audio Advanced #2", - "Movie Versions", - "Unwanted" - ); - - result.Should().ContainKey("Audio Advanced #1") - .WhoseValue.Should().BeEquivalentTo(new[] - { - new CustomFormatGroupItem("TrueHD ATMOS", "truehd-atmos"), - new CustomFormatGroupItem("DTS X", "dts-x"), - new CustomFormatGroupItem("ATMOS (undefined)", "atmos-undefined"), - new CustomFormatGroupItem("DD+ ATMOS", "dd-atmos"), - new CustomFormatGroupItem("TrueHD", "truehd"), - new CustomFormatGroupItem("DTS-HD MA", "dts-hd-ma"), - new CustomFormatGroupItem("DD+", "ddplus"), - new CustomFormatGroupItem("DTS-ES", "dts-es"), - new CustomFormatGroupItem("DTS", "dts") - }); - } -} diff --git a/src/TrashLib.Tests/CustomFormat/Guide/CustomFormatLoaderTest.cs b/src/TrashLib.Tests/CustomFormat/Guide/CustomFormatLoaderTest.cs index 9071b34b..7e5481a2 100644 --- a/src/TrashLib.Tests/CustomFormat/Guide/CustomFormatLoaderTest.cs +++ b/src/TrashLib.Tests/CustomFormat/Guide/CustomFormatLoaderTest.cs @@ -1,3 +1,4 @@ +using System.IO.Abstractions; using System.IO.Abstractions.Extensions; using System.IO.Abstractions.TestingHelpers; using FluentAssertions; @@ -20,19 +21,22 @@ public class CustomFormatLoaderTest : IntegrationFixture var sut = Resolve(); Fs.AddFile("first.json", new MockFileData("{'name':'first','trash_id':'1'}")); Fs.AddFile("second.json", new MockFileData("{'name':'second','trash_id':'2'}")); + Fs.AddFile("collection_of_cfs.md", new MockFileData("")); - var results = sut.LoadAllCustomFormatsAtPaths(new[] {Fs.CurrentDirectory()}); + var dir = Fs.CurrentDirectory(); + var results = sut.LoadAllCustomFormatsAtPaths(new[] {dir}, dir.File("collection_of_cfs.md")); results.Should().BeEquivalentTo(new[] { - NewCf.Data("first", "1"), - NewCf.Data("second", "2") + NewCf.Data("first", "1") with {FileName = "first.json"}, + NewCf.Data("second", "2") with {FileName = "second.json"} }); } [Test] public void Trash_properties_are_removed() { + Fs.AddFile("collection_of_cfs.md", new MockFileData("")); Fs.AddFile("first.json", new MockFileData(@" { 'name':'first', @@ -44,12 +48,14 @@ public class CustomFormatLoaderTest : IntegrationFixture var sut = Resolve(); - var results = sut.LoadAllCustomFormatsAtPaths(new[] {Fs.CurrentDirectory()}); + var dir = Fs.CurrentDirectory(); + var results = sut.LoadAllCustomFormatsAtPaths( + new[] {dir}, dir.File("collection_of_cfs.md")); const string expectedExtraJson = @"{'name':'first','extra': 'e1'}"; results.Should() - .ContainSingle().Which.ExtraJson.Should() + .ContainSingle().Which.Json.Should() .BeEquivalentTo(JObject.Parse(expectedExtraJson), op => op.Using(new JsonEquivalencyStep())); } } diff --git a/src/TrashLib.Tests/CustomFormat/Processors/GuideProcessorTest.cs b/src/TrashLib.Tests/CustomFormat/Processors/GuideProcessorTest.cs index d6c9b513..c22d4304 100644 --- a/src/TrashLib.Tests/CustomFormat/Processors/GuideProcessorTest.cs +++ b/src/TrashLib.Tests/CustomFormat/Processors/GuideProcessorTest.cs @@ -39,7 +39,7 @@ public class GuideProcessorTest public CustomFormatData ReadCustomFormat(string textFile) { var parser = new CustomFormatParser(); - return parser.ParseCustomFormatData(ReadText(textFile)); + return parser.ParseCustomFormatData(ReadText(textFile), ""); } public string ReadText(string textFile) => Data.ReadData(textFile); diff --git a/src/TrashLib.Tests/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs b/src/TrashLib.Tests/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs index ced529a9..71bfa4c6 100644 --- a/src/TrashLib.Tests/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs +++ b/src/TrashLib.Tests/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs @@ -1,6 +1,5 @@ using System.Collections.ObjectModel; using FluentAssertions; -using Newtonsoft.Json.Linq; using NUnit.Framework; using TestLibrary.AutoFixture; using TestLibrary.FluentAssertions; @@ -181,7 +180,7 @@ public class CustomFormatStepTest var guideCfs = new List { - new("3D", "id1", null, new JObject()) + NewCf.Data("3D", "id1") }; processor.Process(guideCfs, Array.Empty(), cache); @@ -197,7 +196,7 @@ public class CustomFormatStepTest { var guideData = new List { - new("name2", "id1", null, new JObject()) + NewCf.Data("name2", "id1") }; var testConfig = new List diff --git a/src/TrashLib/Repo/IRepoPaths.cs b/src/TrashLib/Repo/IRepoPaths.cs index 856ec2a9..2b40d7e8 100644 --- a/src/TrashLib/Repo/IRepoPaths.cs +++ b/src/TrashLib/Repo/IRepoPaths.cs @@ -9,4 +9,6 @@ public interface IRepoPaths IReadOnlyCollection SonarrQualityPaths { get; } IReadOnlyCollection RadarrQualityPaths { get; } IReadOnlyCollection SonarrCustomFormatPaths { get; } + IFileInfo RadarrCollectionOfCustomFormats { get; } + IFileInfo SonarrCollectionOfCustomFormats { get; } } diff --git a/src/TrashLib/Repo/RepoPaths.cs b/src/TrashLib/Repo/RepoPaths.cs index 1f3aad89..8c4a725c 100644 --- a/src/TrashLib/Repo/RepoPaths.cs +++ b/src/TrashLib/Repo/RepoPaths.cs @@ -7,5 +7,7 @@ public record RepoPaths( IReadOnlyCollection SonarrReleaseProfilePaths, IReadOnlyCollection RadarrQualityPaths, IReadOnlyCollection SonarrQualityPaths, - IReadOnlyCollection SonarrCustomFormatPaths + IReadOnlyCollection SonarrCustomFormatPaths, + IFileInfo RadarrCollectionOfCustomFormats, + IFileInfo SonarrCollectionOfCustomFormats ) : IRepoPaths; diff --git a/src/TrashLib/Repo/RepoPathsFactory.cs b/src/TrashLib/Repo/RepoPathsFactory.cs index cbc9056b..fa47fc33 100644 --- a/src/TrashLib/Repo/RepoPathsFactory.cs +++ b/src/TrashLib/Repo/RepoPathsFactory.cs @@ -24,13 +24,16 @@ public class RepoPathsFactory : IRepoPathsFactory public IRepoPaths Create() { + var docs = _paths.RepoDirectory.SubDirectory("docs"); var metadata = _metadata.Value; return new RepoPaths( ToDirectoryInfoList(metadata.JsonPaths.Radarr.CustomFormats), ToDirectoryInfoList(metadata.JsonPaths.Sonarr.ReleaseProfiles), ToDirectoryInfoList(metadata.JsonPaths.Radarr.Qualities), ToDirectoryInfoList(metadata.JsonPaths.Sonarr.Qualities), - ToDirectoryInfoList(metadata.JsonPaths.Sonarr.CustomFormats) + ToDirectoryInfoList(metadata.JsonPaths.Sonarr.CustomFormats), + docs.SubDirectory("Radarr").File("Radarr-collection-of-custom-formats.md"), + docs.SubDirectory("Sonarr").File("sonarr-collection-of-custom-formats.md") ); } } diff --git a/src/TrashLib/Services/Common/GuideDataLister.cs b/src/TrashLib/Services/Common/GuideDataLister.cs index 002f4314..9661db04 100644 --- a/src/TrashLib/Services/Common/GuideDataLister.cs +++ b/src/TrashLib/Services/Common/GuideDataLister.cs @@ -14,11 +14,21 @@ public class GuideDataLister : IGuideDataLister public void ListCustomFormats(IEnumerable customFormats) { - _console.Output.WriteLine("\nList of Custom Formats in the TRaSH Guides:\n"); + _console.Output.WriteLine("\nList of Custom Formats in the TRaSH Guides:"); - foreach (var cf in customFormats) + var categories = customFormats + .ToLookup(x => x.Category) + .OrderBy(x => x.Key); + + foreach (var cat in categories) { - _console.Output.WriteLine($" - {cf.TrashId} # {cf.Name}"); + var title = cat.Key is not null ? $"{cat.Key}" : "[No Category]"; + _console.Output.WriteLine($"\n{title}\n"); + + foreach (var cf in cat) + { + _console.Output.WriteLine($" - {cf.TrashId} # {cf.Name}"); + } } _console.Output.WriteLine( diff --git a/src/TrashLib/Services/CustomFormat/CustomFormatAutofacModule.cs b/src/TrashLib/Services/CustomFormat/CustomFormatAutofacModule.cs index ca12180f..2299bffe 100644 --- a/src/TrashLib/Services/CustomFormat/CustomFormatAutofacModule.cs +++ b/src/TrashLib/Services/CustomFormat/CustomFormatAutofacModule.cs @@ -30,5 +30,6 @@ public class CustomFormatAutofacModule : Module builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As(); + builder.RegisterType().As(); } } diff --git a/src/TrashLib/Services/CustomFormat/Guide/CustomFormatCategoryItem.cs b/src/TrashLib/Services/CustomFormat/Guide/CustomFormatCategoryItem.cs new file mode 100644 index 00000000..4a41545f --- /dev/null +++ b/src/TrashLib/Services/CustomFormat/Guide/CustomFormatCategoryItem.cs @@ -0,0 +1,3 @@ +namespace TrashLib.Services.CustomFormat.Guide; + +public record CustomFormatCategoryItem(string CategoryName, string CfName, string CfAnchor); diff --git a/src/TrashLib/Services/CustomFormat/Guide/CustomFormatGroupParser.cs b/src/TrashLib/Services/CustomFormat/Guide/CustomFormatCategoryParser.cs similarity index 69% rename from src/TrashLib/Services/CustomFormat/Guide/CustomFormatGroupParser.cs rename to src/TrashLib/Services/CustomFormat/Guide/CustomFormatCategoryParser.cs index e266a42d..2bf1a808 100644 --- a/src/TrashLib/Services/CustomFormat/Guide/CustomFormatGroupParser.cs +++ b/src/TrashLib/Services/CustomFormat/Guide/CustomFormatCategoryParser.cs @@ -1,34 +1,19 @@ -using System.Collections.ObjectModel; using System.IO.Abstractions; using System.Text.RegularExpressions; using Common.Extensions; -using TrashLib.Startup; namespace TrashLib.Services.CustomFormat.Guide; -public record CustomFormatGroupItem(string Name, string Anchor); - -public class CustomFormatGroupParser +public class CustomFormatCategoryParser : ICustomFormatCategoryParser { - private readonly IAppPaths _paths; private static readonly Regex TableRegex = new(@"^\s*\|(.*)\|\s*$"); private static readonly Regex LinkRegex = new(@"^\[(.+?)\]\(#(.+?)\)$"); - public CustomFormatGroupParser(IAppPaths paths) + public ICollection Parse(IFileInfo collectionOfCustomFormatsMdFile) { - _paths = paths; - } - - public IDictionary> Parse() - { - var mdFile = _paths.RepoDirectory - .SubDirectory("docs") - .SubDirectory("Radarr") - .File("Radarr-collection-of-custom-formats.md"); - var columns = new List>(); - using var md = mdFile.OpenText(); + using var md = collectionOfCustomFormatsMdFile.OpenText(); while (!md.EndOfStream) { var rows = ParseTable(md); @@ -41,15 +26,19 @@ public class CustomFormatGroupParser .Select(x => x.ToList())); } - return columns.ToDictionary( - x => x[0], - x => x.Skip(1).Select(ParseLink).NotNull().ToList().AsReadOnly()); + return columns + .GroupBy(x => x[0], x => x.Skip(1)) + .SelectMany(x => x.SelectMany(y => y).Distinct().Select(y => ParseLink(x.Key, y))) + .NotNull() + .ToList(); } - private static CustomFormatGroupItem? ParseLink(string markdownLink) + private static CustomFormatCategoryItem? ParseLink(string categoryName, string markdownLink) { var match = LinkRegex.Match(markdownLink); - return match.Success ? new CustomFormatGroupItem(match.Groups[1].Value, match.Groups[2].Value) : null; + return match.Success + ? new CustomFormatCategoryItem(categoryName, match.Groups[1].Value, match.Groups[2].Value) + : null; } private static IEnumerable> ParseTable(TextReader stream) diff --git a/src/TrashLib/Services/CustomFormat/Guide/CustomFormatLoader.cs b/src/TrashLib/Services/CustomFormat/Guide/CustomFormatLoader.cs index 923dcb21..8ef5cf6d 100644 --- a/src/TrashLib/Services/CustomFormat/Guide/CustomFormatLoader.cs +++ b/src/TrashLib/Services/CustomFormat/Guide/CustomFormatLoader.cs @@ -12,29 +12,44 @@ public class CustomFormatLoader : ICustomFormatLoader { private readonly ILogger _log; private readonly ICustomFormatParser _parser; + private readonly ICustomFormatCategoryParser _categoryParser; - public CustomFormatLoader(ILogger log, ICustomFormatParser parser) + public CustomFormatLoader(ILogger log, ICustomFormatParser parser, ICustomFormatCategoryParser categoryParser) { _log = log; _parser = parser; + _categoryParser = categoryParser; } - public ICollection LoadAllCustomFormatsAtPaths(IEnumerable jsonPaths) + public ICollection LoadAllCustomFormatsAtPaths( + IEnumerable jsonPaths, + IFileInfo collectionOfCustomFormats) { + var categories = _categoryParser.Parse(collectionOfCustomFormats).AsReadOnly(); var jsonFiles = jsonPaths.SelectMany(x => x.GetFiles("*.json")); return jsonFiles.ToObservable() - .Select(x => Observable.Defer(() => LoadJsonFromFile(x))) + .Select(x => Observable.Defer(() => LoadJsonFromFile(x, categories))) .Merge(8) .NotNull() .ToEnumerable() .ToList(); } - private IObservable LoadJsonFromFile(IFileInfo file) + private IObservable LoadJsonFromFile(IFileInfo file, + IReadOnlyCollection categories) { return Observable.Using(file.OpenText, x => x.ReadToEndAsync().ToObservable()) .Do(_ => _log.Debug("Parsing CF Json: {Name}", file.Name)) - .Select(_parser.ParseCustomFormatData) + .Select(x => + { + var cf = _parser.ParseCustomFormatData(x, file.Name); + var matchingCategory = categories.FirstOrDefault(y => + { + var fileName = Path.GetFileNameWithoutExtension(cf.FileName); + return y.CfName.EqualsIgnoreCase(cf.Name) || y.CfAnchor.EqualsIgnoreCase(fileName); + }); + return cf with {Category = matchingCategory?.CategoryName}; + }) .Catch((JsonException e) => { _log.Warning("Failed to parse JSON file: {File} ({Reason})", file.Name, e.Message); diff --git a/src/TrashLib/Services/CustomFormat/Guide/CustomFormatParser.cs b/src/TrashLib/Services/CustomFormat/Guide/CustomFormatParser.cs index f7e9381f..9f2d4f4d 100644 --- a/src/TrashLib/Services/CustomFormat/Guide/CustomFormatParser.cs +++ b/src/TrashLib/Services/CustomFormat/Guide/CustomFormatParser.cs @@ -7,7 +7,7 @@ namespace TrashLib.Services.CustomFormat.Guide; public class CustomFormatParser : ICustomFormatParser { - public CustomFormatData ParseCustomFormatData(string guideData) + public CustomFormatData ParseCustomFormatData(string guideData, string fileName) { var obj = JObject.Parse(guideData); @@ -29,6 +29,6 @@ public class CustomFormatParser : ICustomFormatParser trashProperty.Remove(); } - return new CustomFormatData(name, trashId, finalScore, obj); + return new CustomFormatData(fileName, name, trashId, finalScore, obj); } } diff --git a/src/TrashLib/Services/CustomFormat/Guide/ICustomFormatCategoryParser.cs b/src/TrashLib/Services/CustomFormat/Guide/ICustomFormatCategoryParser.cs new file mode 100644 index 00000000..94b64e86 --- /dev/null +++ b/src/TrashLib/Services/CustomFormat/Guide/ICustomFormatCategoryParser.cs @@ -0,0 +1,8 @@ +using System.IO.Abstractions; + +namespace TrashLib.Services.CustomFormat.Guide; + +public interface ICustomFormatCategoryParser +{ + ICollection Parse(IFileInfo collectionOfCustomFormatsMdFile); +} diff --git a/src/TrashLib/Services/CustomFormat/Guide/ICustomFormatLoader.cs b/src/TrashLib/Services/CustomFormat/Guide/ICustomFormatLoader.cs index 19d8038b..918364bb 100644 --- a/src/TrashLib/Services/CustomFormat/Guide/ICustomFormatLoader.cs +++ b/src/TrashLib/Services/CustomFormat/Guide/ICustomFormatLoader.cs @@ -5,5 +5,6 @@ namespace TrashLib.Services.CustomFormat.Guide; public interface ICustomFormatLoader { - ICollection LoadAllCustomFormatsAtPaths(IEnumerable jsonPaths); + ICollection LoadAllCustomFormatsAtPaths(IEnumerable jsonPaths, + IFileInfo collectionOfCustomFormats); } diff --git a/src/TrashLib/Services/CustomFormat/Guide/ICustomFormatParser.cs b/src/TrashLib/Services/CustomFormat/Guide/ICustomFormatParser.cs index 6e9825d9..ff38a700 100644 --- a/src/TrashLib/Services/CustomFormat/Guide/ICustomFormatParser.cs +++ b/src/TrashLib/Services/CustomFormat/Guide/ICustomFormatParser.cs @@ -4,5 +4,5 @@ namespace TrashLib.Services.CustomFormat.Guide; public interface ICustomFormatParser { - CustomFormatData ParseCustomFormatData(string guideData); + CustomFormatData ParseCustomFormatData(string guideData, string fileName); } diff --git a/src/TrashLib/Services/CustomFormat/Models/CustomFormatData.cs b/src/TrashLib/Services/CustomFormat/Models/CustomFormatData.cs index c4798d16..1fc0249d 100644 --- a/src/TrashLib/Services/CustomFormat/Models/CustomFormatData.cs +++ b/src/TrashLib/Services/CustomFormat/Models/CustomFormatData.cs @@ -4,8 +4,12 @@ using Newtonsoft.Json.Linq; namespace TrashLib.Services.CustomFormat.Models; public record CustomFormatData( + string FileName, string Name, string TrashId, int? Score, - [property: JsonExtensionData] JObject ExtraJson -); + [property: JsonExtensionData] JObject Json +) +{ + public string? Category { get; init; } +} diff --git a/src/TrashLib/Services/CustomFormat/Models/ProcessedCustomFormatData.cs b/src/TrashLib/Services/CustomFormat/Models/ProcessedCustomFormatData.cs index 85d23d60..55012d3e 100644 --- a/src/TrashLib/Services/CustomFormat/Models/ProcessedCustomFormatData.cs +++ b/src/TrashLib/Services/CustomFormat/Models/ProcessedCustomFormatData.cs @@ -11,7 +11,7 @@ public class ProcessedCustomFormatData public ProcessedCustomFormatData(CustomFormatData data) { _data = data; - Json = _data.ExtraJson; + Json = _data.Json; } public string Name => _data.Name; diff --git a/src/TrashLib/Services/Radarr/LocalRepoRadarrGuideService.cs b/src/TrashLib/Services/Radarr/LocalRepoRadarrGuideService.cs index c731cd28..b26c293a 100644 --- a/src/TrashLib/Services/Radarr/LocalRepoRadarrGuideService.cs +++ b/src/TrashLib/Services/Radarr/LocalRepoRadarrGuideService.cs @@ -26,6 +26,8 @@ public class LocalRepoRadarrGuideService : IRadarrGuideService public ICollection GetCustomFormatData() { var paths = _pathsFactory.Create(); - return _cfLoader.LoadAllCustomFormatsAtPaths(paths.RadarrCustomFormatPaths); + return _cfLoader.LoadAllCustomFormatsAtPaths( + paths.RadarrCustomFormatPaths, + paths.RadarrCollectionOfCustomFormats); } } diff --git a/src/TrashLib/Services/Sonarr/ReleaseProfile/Guide/LocalRepoSonarrGuideService.cs b/src/TrashLib/Services/Sonarr/ReleaseProfile/Guide/LocalRepoSonarrGuideService.cs index 075090ed..2411ecd9 100644 --- a/src/TrashLib/Services/Sonarr/ReleaseProfile/Guide/LocalRepoSonarrGuideService.cs +++ b/src/TrashLib/Services/Sonarr/ReleaseProfile/Guide/LocalRepoSonarrGuideService.cs @@ -38,7 +38,9 @@ public class LocalRepoSonarrGuideService : ISonarrGuideService public ICollection GetCustomFormatData() { var paths = _pathsFactory.Create(); - return _cfLoader.LoadAllCustomFormatsAtPaths(paths.SonarrCustomFormatPaths); + return _cfLoader.LoadAllCustomFormatsAtPaths( + paths.SonarrCustomFormatPaths, + paths.SonarrCollectionOfCustomFormats); } private IEnumerable GetReleaseProfileDataImpl()