diff --git a/SabreTools.RedumpLib.Test/Data/ExtensionsTests.cs b/SabreTools.RedumpLib.Test/Data/ExtensionsTests.cs
index 1e0e088..40a0a0f 100644
--- a/SabreTools.RedumpLib.Test/Data/ExtensionsTests.cs
+++ b/SabreTools.RedumpLib.Test/Data/ExtensionsTests.cs
@@ -921,6 +921,113 @@ namespace SabreTools.RedumpLib.Test.Data
#endregion
+ #region List Subpath
+
+ ///
+ /// Check that every ListSubpath has a long name provided
+ ///
+ /// ListSubpath value to check
+ /// True to expect a null value, false otherwise
+ [Theory]
+ [MemberData(nameof(GenerateListSubpathTestData))]
+ public void ListSubpath_LongName(ListSubpath? listSubpath, bool expectNull)
+ {
+ var actual = listSubpath.LongName();
+
+ if (expectNull)
+ Assert.Null(actual);
+ else
+ Assert.NotNull(actual);
+ }
+
+ ///
+ /// Check that every ListSubpath has a short name provided
+ ///
+ /// ListSubpath value to check
+ /// True to expect a null value, false otherwise
+ [Theory]
+ [MemberData(nameof(GenerateListSubpathTestData))]
+ public void ListSubpath_ShortName(ListSubpath? listSubpath, bool expectNull)
+ {
+ var actual = listSubpath.ShortName();
+
+ if (expectNull)
+ Assert.Null(actual);
+ else
+ Assert.NotNull(actual);
+ }
+
+ ///
+ /// Ensure that every ListSubpath that has a short name that is unique
+ ///
+ [Fact]
+ public void ListSubpath_ShortName_NoDuplicates()
+ {
+ var fullListSubpaths = Enum.GetValues().Cast().ToList();
+ var filteredListSubpaths = new Dictionary();
+
+ int totalCount = 0;
+ foreach (ListSubpath? listSubpath in fullListSubpaths)
+ {
+ var code = listSubpath.ShortName();
+ if (string.IsNullOrEmpty(code))
+ continue;
+
+ // Throw if the code already exists
+ if (filteredListSubpaths.ContainsKey(code))
+ throw new DuplicateNameException($"Code {code} already in dictionary");
+
+ filteredListSubpaths[code] = listSubpath;
+ totalCount++;
+ }
+
+ Assert.Equal(totalCount, filteredListSubpaths.Count);
+ }
+
+ ///
+ /// Check that every ListSubpath can be mapped from a string
+ ///
+ /// ListSubpath value to check
+ /// True to expect a null value, false otherwise
+ [Theory]
+ [MemberData(nameof(GenerateListSubpathTestData))]
+ public void ListSubpath_ToListSubpath(ListSubpath? listSubpath, bool expectNull)
+ {
+ string? longName = listSubpath.LongName();
+ string? longNameSpaceless = longName?.Replace(" ", string.Empty);
+
+ var actualNormal = longName.ToListSubpath();
+ var actualSpaceless = longNameSpaceless.ToListSubpath();
+
+ if (expectNull)
+ {
+ Assert.Null(actualNormal);
+ Assert.Null(actualSpaceless);
+ }
+ else
+ {
+ Assert.Equal(listSubpath, actualNormal);
+ Assert.Equal(listSubpath, actualSpaceless);
+ }
+ }
+
+ ///
+ /// Generate a test set of ListSubpath values
+ ///
+ /// MemberData-compatible list of ListSubpath values
+ public static TheoryData GenerateListSubpathTestData()
+ {
+ var testData = new TheoryData() { { null, true } };
+ foreach (ListSubpath? listSubpath in Enum.GetValues().Cast())
+ {
+ testData.Add(listSubpath, false);
+ }
+
+ return testData;
+ }
+
+ #endregion
+
#region Media Type
///
diff --git a/SabreTools.RedumpLib.Test/Endpoints/ListTests.cs b/SabreTools.RedumpLib.Test/Endpoints/ListTests.cs
new file mode 100644
index 0000000..6fea384
--- /dev/null
+++ b/SabreTools.RedumpLib.Test/Endpoints/ListTests.cs
@@ -0,0 +1,75 @@
+using SabreTools.RedumpLib.Data;
+using Xunit;
+
+namespace SabreTools.RedumpLib.Test.Endpoints
+{
+ public class ListTests
+ {
+ #region BuildUrl
+
+ [Fact]
+ public void BuildUrl_MissingUsername_DefaultEmpty()
+ {
+ var endpoint = new RedumpLib.Endpoints.List();
+ string actual = endpoint.BuildUrl();
+ Assert.Equal($"https://redump.info/list/", actual);
+ }
+
+ [Fact]
+ public void BuildUrl_Subpath_Builds()
+ {
+ // Create a base endpoint and then modify the fields
+ var endpoint = new RedumpLib.Endpoints.List { Username = "user" };
+
+ // Invalid subpath, no system
+ endpoint.Subpath = null;
+ endpoint.System = null;
+ string actual = endpoint.BuildUrl();
+ Assert.Equal("https://redump.info/list/user", actual);
+
+ // Invalid subpath, valid system
+ endpoint.Subpath = null;
+ endpoint.System = PhysicalSystem.AcornArchimedesAndRiscPC;
+ actual = endpoint.BuildUrl();
+ Assert.Equal("https://redump.info/list/user/ARCH", actual);
+
+ // Have subpath, no system
+ endpoint.Subpath = ListSubpath.Have;
+ endpoint.System = null;
+ actual = endpoint.BuildUrl();
+ Assert.Equal("https://redump.info/list/have/user", actual);
+
+ // Have subpath, valid system
+ endpoint.Subpath = ListSubpath.Have;
+ endpoint.System = PhysicalSystem.AcornArchimedesAndRiscPC;
+ actual = endpoint.BuildUrl();
+ Assert.Equal("https://redump.info/list/have/user/ARCH", actual);
+
+ // Cuesheets subpath, no system
+ endpoint.Subpath = ListSubpath.Cuesheets;
+ endpoint.System = null;
+ actual = endpoint.BuildUrl();
+ Assert.Equal("https://redump.info/list/cues/user", actual);
+
+ // Cuesheets subpath, valid system
+ endpoint.Subpath = ListSubpath.Cuesheets;
+ endpoint.System = PhysicalSystem.AcornArchimedesAndRiscPC;
+ actual = endpoint.BuildUrl();
+ Assert.Equal("https://redump.info/list/cues/user/ARCH", actual);
+
+ // Datfile subpath, no system
+ endpoint.Subpath = ListSubpath.Datfile;
+ endpoint.System = null;
+ actual = endpoint.BuildUrl();
+ Assert.Equal("https://redump.info/list/datfile/user", actual);
+
+ // Datfile subpath, valid system
+ endpoint.Subpath = ListSubpath.Datfile;
+ endpoint.System = PhysicalSystem.AcornArchimedesAndRiscPC;
+ actual = endpoint.BuildUrl();
+ Assert.Equal("https://redump.info/list/datfile/user/ARCH", actual);
+ }
+
+ #endregion
+ }
+}
diff --git a/SabreTools.RedumpLib/Data/Enumerations.cs b/SabreTools.RedumpLib/Data/Enumerations.cs
index 50a8e8e..26cb0b7 100644
--- a/SabreTools.RedumpLib/Data/Enumerations.cs
+++ b/SabreTools.RedumpLib/Data/Enumerations.cs
@@ -87,6 +87,21 @@ namespace SabreTools.RedumpLib.Data
VerifiedGreen = 5,
}
+ ///
+ /// List of all list subpaths
+ ///
+ public enum ListSubpath
+ {
+ [HumanReadable(LongName = "Have", ShortName = "have")]
+ Have = 1,
+
+ [HumanReadable(LongName = "Cuesheets", ShortName = "cues")]
+ Cuesheets = 2,
+
+ [HumanReadable(LongName = "Datfile", ShortName = "datfile")]
+ Datfile = 3,
+ }
+
///
/// List of all site-supported media types
///
diff --git a/SabreTools.RedumpLib/Data/Extensions.cs b/SabreTools.RedumpLib/Data/Extensions.cs
index 601f453..0ee8cf8 100644
--- a/SabreTools.RedumpLib/Data/Extensions.cs
+++ b/SabreTools.RedumpLib/Data/Extensions.cs
@@ -241,10 +241,10 @@ namespace SabreTools.RedumpLib.Data
=> AttributeHelper.GetHumanReadableAttribute(discSubpath)?.ShortName;
///
- /// Get the Region enum value for a given string
+ /// Get the DiscSubpath enum value for a given string
///
/// String value to convert
- /// Region represented by the string, if possible
+ /// DiscSubpath represented by the string, if possible
public static DiscSubpath? ToDiscSubpath(this string? discSubpath)
{
// No value means no match
@@ -374,6 +374,70 @@ namespace SabreTools.RedumpLib.Data
#endregion
+ #region List Subpath
+
+ ///
+ /// Get the human readable name for a ListSubpath
+ ///
+ ///
+ ///
+ public static string? LongName(this ListSubpath listSubpath)
+ => AttributeHelper.GetHumanReadableAttribute(listSubpath)?.LongName;
+
+ ///
+ /// Get the human readable name for a ListSubpath
+ ///
+ ///
+ ///
+ public static string? LongName(this ListSubpath? listSubpath)
+ => AttributeHelper.GetHumanReadableAttribute(listSubpath)?.LongName;
+
+ ///
+ /// Get the URL path part for a ListSubpath
+ ///
+ ///
+ ///
+ public static string? ShortName(this ListSubpath listSubpath)
+ => AttributeHelper.GetHumanReadableAttribute(listSubpath)?.ShortName;
+
+ ///
+ /// Get the URL path part for a ListSubpath
+ ///
+ ///
+ ///
+ public static string? ShortName(this ListSubpath? listSubpath)
+ => AttributeHelper.GetHumanReadableAttribute(listSubpath)?.ShortName;
+
+ ///
+ /// Get the ListSubpath enum value for a given string
+ ///
+ /// String value to convert
+ /// ListSubpath represented by the string, if possible
+ public static ListSubpath? ToListSubpath(this string? listSubpath)
+ {
+ // No value means no match
+ if (listSubpath is null || listSubpath.Length == 0)
+ return null;
+
+ listSubpath = listSubpath.ToLowerInvariant();
+ var listSubpaths = (ListSubpath[])Enum.GetValues(typeof(ListSubpath));
+
+ // Check short names
+ int index = Array.FindIndex(listSubpaths, s => listSubpath == s.ShortName()?.ToLowerInvariant());
+ if (index > -1)
+ return listSubpaths[index];
+
+ // Check long names
+ index = Array.FindIndex(listSubpaths, s => listSubpath == s.LongName()?.ToLowerInvariant()
+ || listSubpath == s.LongName()?.Replace(" ", string.Empty)?.ToLowerInvariant());
+ if (index > -1)
+ return listSubpaths[index];
+
+ return null;
+ }
+
+ #endregion
+
#region Media Type
///
diff --git a/SabreTools.RedumpLib/Endpoints/List.cs b/SabreTools.RedumpLib/Endpoints/List.cs
new file mode 100644
index 0000000..391a8ba
--- /dev/null
+++ b/SabreTools.RedumpLib/Endpoints/List.cs
@@ -0,0 +1,52 @@
+using System;
+using Newtonsoft.Json;
+using SabreTools.RedumpLib.Data;
+
+namespace SabreTools.RedumpLib.Endpoints
+{
+ // TODO: Implement download and client extensions for these paths
+ [JsonObject("list")]
+ public class List
+ {
+ [JsonProperty("subpath")]
+ public ListSubpath? Subpath { get; set; }
+
+ [JsonProperty("username")]
+ public string Username { get; set; } = string.Empty;
+
+ [JsonProperty("syscode")]
+ public PhysicalSystem? System { get; set; }
+
+ ///
+ /// Build a /list/ path URL
+ ///
+ public string BuildUrl()
+ {
+ var ub = new UriBuilder
+ {
+ Scheme = "https",
+ Host = "redump.info",
+ Path = "list",
+ };
+
+ switch (Subpath)
+ {
+ case ListSubpath.Have:
+ case ListSubpath.Cuesheets:
+ case ListSubpath.Datfile:
+ ub.Path += $"/{Subpath.ShortName()}";
+ break;
+
+ // Nothing else is valid as a subpath
+ default:
+ break;
+ }
+
+ ub.Path += $"/{Username}";
+ if (System?.Code is not null)
+ ub.Path += $"/{System.Code}";
+
+ return ub.ToString();
+ }
+ }
+}