Compare commits

...

45 Commits
1.3.3 ... 1.4.2

Author SHA1 Message Date
Matt Nadareski
652270c8c7 Add publish scripts 2024-10-01 13:56:40 -04:00
Matt Nadareski
905d8a94fb Bump version 2024-10-01 13:55:33 -04:00
Matt Nadareski
3ee8416695 Remove unnecessary tuples 2024-10-01 13:53:03 -04:00
Matt Nadareski
49fa06da55 Remove threading bridge package (unused) 2024-10-01 04:27:31 -04:00
Matt Nadareski
70e29afd89 Remove Linq requirement from old .NET 2024-10-01 04:25:35 -04:00
Matt Nadareski
2a402a53db Remove ValueTuple packages (usused) 2024-10-01 03:21:07 -04:00
Matt Nadareski
66bb3b75b2 Bump version 2024-07-24 11:05:25 -04:00
Matt Nadareski
9d7d46673a Fix deserializing submission from file 2024-07-23 22:18:06 -04:00
Matt Nadareski
16d196c902 Bump version 2024-07-16 14:13:28 -04:00
Matt Nadareski
c93da92f19 Add new helper class for site interaction 2024-07-16 14:12:28 -04:00
Matt Nadareski
a219d0c5de Add some client helper classes 2024-07-16 14:07:18 -04:00
Matt Nadareski
02e6f0e85f Port tests from MPF 2024-07-16 13:08:56 -04:00
Deterous
9dfec64e4e Bump version (#6) 2024-07-09 11:09:28 -04:00
Deterous
4c12693a33 Update Sharp X68000 shortname to x68k (#5) 2024-07-08 22:35:28 -04:00
Deterous
acea06c05f Update Triforce shortname to trf (#4) 2024-07-08 21:43:40 -04:00
Matt Nadareski
067c5cfbbc Move project to subfolder 2024-06-27 16:49:18 -04:00
Matt Nadareski
ff04b8ec6f Fix helper classes 2024-06-27 09:43:01 -04:00
Matt Nadareski
e09c895cf1 Add retry count constructor to client 2024-06-27 09:34:58 -04:00
Matt Nadareski
1b68253089 Remove now-redundant classes 2024-06-27 01:41:17 -04:00
Matt Nadareski
5b9a2d6b74 CookieWebClient should be internal 2024-06-27 01:33:15 -04:00
Matt Nadareski
d5c7ef74d4 Reduce code complexity in new client (nw) 2024-06-27 01:30:30 -04:00
Matt Nadareski
f60cd2985d Add unified Client (nw) 2024-06-27 01:12:57 -04:00
Matt Nadareski
7fcb6aa949 Create separate cookie web client 2024-06-27 00:34:15 -04:00
Matt Nadareski
f22b1b036b Minor fixes to organization, add sleep 2024-06-27 00:18:33 -04:00
Matt Nadareski
ec045448c5 Make retry count into a field, sync clients 2024-06-27 00:05:53 -04:00
Matt Nadareski
93873ea204 Add dumping parameters field 2024-06-26 10:02:28 -04:00
Matt Nadareski
341edc56bd Bump version 2024-05-15 17:05:15 -04:00
Matt Nadareski
da4bdac6e2 Update Models 2024-05-15 17:04:55 -04:00
Matt Nadareski
7fe595ee0a Fix ordering of site codes 2024-05-15 16:44:07 -04:00
Matt Nadareski
8a9f62f5a4 Add new tags to formatting list 2024-05-15 16:40:01 -04:00
Matt Nadareski
dbb7cf7ef9 Add missing comment fields to listing 2024-05-15 16:38:17 -04:00
Matt Nadareski
d591ee1550 Bump version 2024-05-15 16:16:37 -04:00
Matt Nadareski
9153c931a5 Include comment/content markers 2024-05-15 16:16:07 -04:00
Matt Nadareski
99ebd1f3ac Add extensions for comment and content codes 2024-05-15 16:09:57 -04:00
Matt Nadareski
844f5506f5 Add Applications pseudotag 2024-05-15 16:09:19 -04:00
Matt Nadareski
4be01b25ab Add EidosID pseudotag 2024-05-15 16:07:00 -04:00
Matt Nadareski
22e2e73f65 Add BethesdaID pseudotag 2024-05-15 16:06:06 -04:00
Matt Nadareski
831ea86d4f Add CompatibleOS pseudotag 2024-05-15 16:04:47 -04:00
Matt Nadareski
4475dba94c Bump version 2024-03-15 12:48:36 -04:00
Matt Nadareski
63a758c005 Fix missing fields from output 2024-03-15 12:48:18 -04:00
Matt Nadareski
7e81f723ca Bump version 2024-03-05 12:30:59 -05:00
Matt Nadareski
f69c7e6bb2 Add new dumping info fields 2024-03-02 19:41:19 -05:00
Matt Nadareski
9d2803a6df Add nuget package and PR workflows 2024-02-27 19:16:31 -05:00
Matt Nadareski
23f1ceac99 Bump version 2024-02-26 20:15:29 -05:00
Deterous
55c621b615 Fill PS3 Disc Key from redump (#3)
* Fill PS3 Disc Key from redump

* Deny NULL rather than only allow hex keys

* Update Builder.cs

Co-authored-by: Matt Nadareski <mnadareski@outlook.com>

---------

Co-authored-by: Matt Nadareski <mnadareski@outlook.com>
2024-02-26 17:04:02 -08:00
45 changed files with 2870 additions and 1021 deletions

43
.github/workflows/build_nupkg.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Nuget Pack
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Pack
run: dotnet pack
- name: Upload build
uses: actions/upload-artifact@v4
with:
name: 'Nuget Package'
path: 'SabreTools.RedumpLib/bin/Release/*.nupkg'
- name: Upload to rolling
uses: ncipollo/release-action@v1.14.0
with:
allowUpdates: True
artifacts: 'SabreTools.RedumpLib/bin/Release/*.nupkg'
body: 'Last built commit: ${{ github.sha }}'
name: 'Rolling Release'
prerelease: True
replacesArtifacts: True
tag: "rolling"
updateOnlyUnreleased: True

17
.github/workflows/check_pr.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Build PR
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Build
run: dotnet build

View File

@@ -0,0 +1,28 @@
using System;
using System.IO;
using Xunit;
namespace SabreTools.RedumpLib.Test
{
public class BuilderTests
{
[Theory]
[InlineData("success_complete.json", false)]
[InlineData("success_invalid.json", false)] // Fully in valid returns a default object
[InlineData("success_partial.json", false)]
[InlineData("fail_invalid.json", true)]
public void CreateFromFileTest(string filename, bool expectNull)
{
// Get the full path to the test file
string path = Path.Combine(Environment.CurrentDirectory, "TestData", filename);
// Try to create the submission info from file
var si = Builder.CreateFromFile(path);
// Check for an expected result
Assert.Equal(expectNull, si == null);
}
}
}

View File

@@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SabreTools.RedumpLib.Data;
using Xunit;
namespace SabreTools.RedumpLib.Test
{
public class EnumExtensionsTests
{
/// <summary>
/// MediaType values that support drive speeds
/// </summary>
private static readonly MediaType?[] _supportDriveSpeeds =
[
MediaType.CDROM,
MediaType.DVD,
MediaType.GDROM,
MediaType.HDDVD,
MediaType.BluRay,
MediaType.NintendoGameCubeGameDisc,
MediaType.NintendoWiiOpticalDisc,
];
/// <summary>
/// RedumpSystem values that are considered Audio
/// </summary>
private static readonly RedumpSystem?[] _audioSystems =
[
RedumpSystem.AtariJaguarCDInteractiveMultimediaSystem,
RedumpSystem.AudioCD,
RedumpSystem.DVDAudio,
RedumpSystem.HasbroiONEducationalGamingSystem,
RedumpSystem.HasbroVideoNow,
RedumpSystem.HasbroVideoNowColor,
RedumpSystem.HasbroVideoNowJr,
RedumpSystem.HasbroVideoNowXP,
RedumpSystem.PlayStationGameSharkUpdates,
RedumpSystem.PhilipsCDi,
RedumpSystem.SuperAudioCD,
];
/// <summary>
/// RedumpSystem values that are considered markers
/// </summary>
private static readonly RedumpSystem?[] _markerSystems =
[
RedumpSystem.MarkerArcadeEnd,
RedumpSystem.MarkerComputerEnd,
RedumpSystem.MarkerDiscBasedConsoleEnd,
RedumpSystem.MarkerOtherEnd,
];
/// <summary>
/// RedumpSystem values that are have reversed ringcodes
/// </summary>
private static readonly RedumpSystem?[] _reverseRingcodeSystems =
[
RedumpSystem.SonyPlayStation2,
RedumpSystem.SonyPlayStation3,
RedumpSystem.SonyPlayStation4,
RedumpSystem.SonyPlayStation5,
RedumpSystem.SonyPlayStationPortable,
];
/// <summary>
/// RedumpSystem values that are considered XGD
/// </summary>
private static readonly RedumpSystem?[] _xgdSystems =
[
RedumpSystem.MicrosoftXbox,
RedumpSystem.MicrosoftXbox360,
RedumpSystem.MicrosoftXboxOne,
RedumpSystem.MicrosoftXboxSeriesXS,
];
/// <summary>
/// Check that all systems with reversed ringcodes are marked properly
/// </summary>
/// <param name="redumpSystem">RedumpSystem value to check</param>
/// <param name="expected">The expected value to come from the check</param>
[Theory]
[MemberData(nameof(GenerateReversedRingcodeSystemsTestData))]
public void HasReversedRingcodesTest(RedumpSystem? redumpSystem, bool expected)
{
bool actual = redumpSystem.HasReversedRingcodes();
Assert.Equal(expected, actual);
}
/// <summary>
/// Check that all audio systems are marked properly
/// </summary>
/// <param name="redumpSystem">RedumpSystem value to check</param>
/// <param name="expected">The expected value to come from the check</param>
[Theory]
[MemberData(nameof(GenerateAudioSystemsTestData))]
public void IsAudioTest(RedumpSystem? redumpSystem, bool expected)
{
bool actual = redumpSystem.IsAudio();
Assert.Equal(expected, actual);
}
/// <summary>
/// Check that all marker systems are marked properly
/// </summary>
/// <param name="redumpSystem">RedumpSystem value to check</param>
/// <param name="expected">The expected value to come from the check</param>
[Theory]
[MemberData(nameof(GenerateMarkerSystemsTestData))]
public void IsMarkerTest(RedumpSystem? redumpSystem, bool expected)
{
bool actual = redumpSystem.IsMarker();
Assert.Equal(expected, actual);
}
/// <summary>
/// Check that all XGD systems are marked properly
/// </summary>
/// <param name="redumpSystem">RedumpSystem value to check</param>
/// <param name="expected">The expected value to come from the check</param>
[Theory]
[MemberData(nameof(GenerateXGDSystemsTestData))]
public void IsXGDTest(RedumpSystem? redumpSystem, bool expected)
{
bool actual = redumpSystem.IsXGD();
Assert.Equal(expected, actual);
}
/// <summary>
/// Generate a test set of RedumpSystem values that are considered Audio
/// </summary>
/// <returns>MemberData-compatible list of RedumpSystem values</returns>
public static List<object?[]> GenerateAudioSystemsTestData()
{
var testData = new List<object?[]>() { new object?[] { null, false } };
foreach (RedumpSystem redumpSystem in Enum.GetValues(typeof(RedumpSystem)))
{
if (_audioSystems.Contains(redumpSystem))
testData.Add([redumpSystem, true]);
else
testData.Add([redumpSystem, false]);
}
return testData;
}
/// <summary>
/// Generate a test set of RedumpSystem values that are considered markers
/// </summary>
/// <returns>MemberData-compatible list of RedumpSystem values</returns>
public static List<object?[]> GenerateMarkerSystemsTestData()
{
var testData = new List<object?[]>() { new object?[] { null, false } };
foreach (RedumpSystem redumpSystem in Enum.GetValues(typeof(RedumpSystem)))
{
if (_markerSystems.Contains(redumpSystem))
testData.Add([redumpSystem, true]);
else
testData.Add([redumpSystem, false]);
}
return testData;
}
/// <summary>
/// Generate a test set of RedumpSystem values that are considered markers
/// </summary>
/// <returns>MemberData-compatible list of RedumpSystem values</returns>
public static List<object?[]> GenerateReversedRingcodeSystemsTestData()
{
var testData = new List<object?[]>() { new object?[] { null, false } };
foreach (RedumpSystem redumpSystem in Enum.GetValues(typeof(RedumpSystem)))
{
if (_reverseRingcodeSystems.Contains(redumpSystem))
testData.Add([redumpSystem, true]);
else
testData.Add([redumpSystem, false]);
}
return testData;
}
/// <summary>
/// Generate a test set of RedumpSystem values that are considered XGD
/// </summary>
/// <returns>MemberData-compatible list of RedumpSystem values</returns>
public static List<object?[]> GenerateXGDSystemsTestData()
{
var testData = new List<object?[]>() { new object?[] { null, false } };
foreach (RedumpSystem redumpSystem in Enum.GetValues(typeof(RedumpSystem)))
{
if (_xgdSystems.Contains(redumpSystem))
testData.Add([redumpSystem, true]);
else
testData.Add([redumpSystem, false]);
}
return testData;
}
}
}

View File

@@ -0,0 +1,717 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using SabreTools.RedumpLib.Data;
using Xunit;
namespace SabreTools.RedumpLib.Test
{
// TODO: Add tests for string-to-enum conversion
public class ExtensionsTests
{
#region Cross-Enumeration
/// <summary>
/// DiscType values that map to MediaType
/// </summary>
private static readonly DiscType?[] _mappableDiscTypes = new DiscType?[]
{
DiscType.BD25,
DiscType.BD33,
DiscType.BD50,
DiscType.BD66,
DiscType.BD100,
DiscType.BD128,
DiscType.CD,
DiscType.DVD5,
DiscType.DVD9,
DiscType.GDROM,
DiscType.HDDVDSL,
DiscType.HDDVDDL,
DiscType.NintendoGameCubeGameDisc,
DiscType.NintendoWiiOpticalDiscSL,
DiscType.NintendoWiiOpticalDiscDL,
DiscType.NintendoWiiUOpticalDiscSL,
DiscType.UMDSL,
DiscType.UMDDL,
};
/// <summary>
/// MediaType values that map to DiscType
/// </summary>
private static readonly MediaType?[] _mappableMediaTypes = new MediaType?[]
{
MediaType.BluRay,
MediaType.CDROM,
MediaType.DVD,
MediaType.GDROM,
MediaType.HDDVD,
MediaType.NintendoGameCubeGameDisc,
MediaType.NintendoWiiOpticalDisc,
MediaType.NintendoWiiUOpticalDisc,
MediaType.UMD,
};
/// <summary>
/// Check that every supported system has some set of MediaTypes supported
/// </summary>
/// <param name="redumpSystem">RedumpSystem value to check</param>
[Theory]
[MemberData(nameof(GenerateRedumpSystemMappingTestData))]
public void MediaTypesTest(RedumpSystem? redumpSystem)
{
var actual = redumpSystem.MediaTypes();
Assert.NotEmpty(actual);
}
/// <summary>
/// Check that both mappable and unmappable media types output correctly
/// </summary>
/// <param name="mediaType">MediaType value to check</param>
/// <param name="expectNull">True to expect a null mapping, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateMediaTypeMappingTestData))]
public void ToDiscTypeTest(MediaType? mediaType, bool expectNull)
{
DiscType? actual = mediaType.ToDiscType();
Assert.Equal(expectNull, actual == null);
}
/// <summary>
/// Check that DiscType values all map to something appropriate
/// </summary>
/// <param name="discType">DiscType value to check</param>
/// <param name="expectNull">True to expect a null mapping, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateDiscTypeMappingTestData))]
public void ToMediaTypeTest(DiscType? discType, bool expectNull)
{
MediaType? actual = discType.ToMediaType();
Assert.Equal(expectNull, actual == null);
}
/// <summary>
/// Generate a test set of DiscType values
/// </summary>
/// <returns>MemberData-compatible list of DiscType values</returns>
public static List<object?[]> GenerateDiscTypeMappingTestData()
{
var testData = new List<object?[]>() { new object?[] { null, true } };
foreach (DiscType? discType in Enum.GetValues(typeof(DiscType)))
{
if (_mappableDiscTypes.Contains(discType))
testData.Add(new object?[] { discType, false });
else
testData.Add(new object?[] { discType, true });
}
return testData;
}
/// <summary>
/// Generate a test set of RedumpSystem values
/// </summary>
/// <returns>MemberData-compatible list of RedumpSystem values</returns>
public static List<object?[]> GenerateRedumpSystemMappingTestData()
{
var testData = new List<object?[]>() { new object?[] { null } };
foreach (RedumpSystem? redumpSystem in Enum.GetValues(typeof(RedumpSystem)))
{
testData.Add(new object?[] { redumpSystem });
}
return testData;
}
/// <summary>
/// Generate a test set of mappable media types
/// </summary>
/// <returns>MemberData-compatible list of MediaTypes</returns>
public static List<object?[]> GenerateMediaTypeMappingTestData()
{
var testData = new List<object?[]>() { new object?[] { null, true } };
foreach (MediaType? mediaType in Enum.GetValues(typeof(MediaType)))
{
if (_mappableMediaTypes.Contains(mediaType))
testData.Add(new object?[] { mediaType, false });
else
testData.Add(new object?[] { mediaType, true });
}
return testData;
}
#endregion
#region Disc Category
/// <summary>
/// Check that every DiscCategory has a long name provided
/// </summary>
/// <param name="discCategory">DiscCategory value to check</param>
/// <param name="expectNull">True to expect a null value, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateDiscCategoryTestData))]
public void DiscCategoryLongNameTest(DiscCategory? discCategory, bool expectNull)
{
var actual = discCategory.LongName();
if (expectNull)
Assert.Null(actual);
else
Assert.NotNull(actual);
}
/// <summary>
/// Generate a test set of DiscCategory values
/// </summary>
/// <returns>MemberData-compatible list of DiscCategory values</returns>
public static List<object?[]> GenerateDiscCategoryTestData()
{
var testData = new List<object?[]>() { new object?[] { null, true } };
foreach (DiscCategory? discCategory in Enum.GetValues(typeof(DiscCategory)))
{
testData.Add(new object?[] { discCategory, false });
}
return testData;
}
#endregion
#region Disc Type
/// <summary>
/// Check that every DiscType has a long name provided
/// </summary>
/// <param name="discType">DiscType value to check</param>
/// <param name="expectNull">True to expect a null value, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateDiscTypeTestData))]
public void DiscTypeLongNameTest(DiscType? discType, bool expectNull)
{
var actual = discType.LongName();
if (expectNull)
Assert.Null(actual);
else
Assert.NotNull(actual);
}
/// <summary>
/// Generate a test set of DiscType values
/// </summary>
/// <returns>MemberData-compatible list of DiscType values</returns>
public static List<object?[]> GenerateDiscTypeTestData()
{
var testData = new List<object?[]>() { new object?[] { null, true } };
foreach (DiscType? discType in Enum.GetValues(typeof(DiscType)))
{
if (discType == DiscType.NONE)
testData.Add(new object?[] { discType, true });
else
testData.Add(new object?[] { discType, false });
}
return testData;
}
#endregion
#region Language
/// <summary>
/// Check that every Language has a long name provided
/// </summary>
/// <param name="language">Language value to check</param>
/// <param name="expectNull">True to expect a null value, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateLanguageTestData))]
public void LanguageLongNameTest(Language? language, bool expectNull)
{
var actual = language.LongName();
if (expectNull)
Assert.Null(actual);
else
Assert.NotNull(actual);
}
/// <summary>
/// Check that every Language has a short name provided
/// </summary>
/// <param name="language">Language value to check</param>
/// <param name="expectNull">True to expect a null value, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateLanguageTestData))]
public void LanguageShortNameTest(Language? language, bool expectNull)
{
var actual = language.ShortName();
if (expectNull)
Assert.Null(actual);
else
Assert.NotNull(actual);
}
/// <summary>
/// Ensure that every Language that has an ISO 639-1 code is unique
/// </summary>
[Fact]
public void LanguageNoDuplicateTwoLetterCodeTest()
{
var fullLanguages = Enum.GetValues(typeof(Language)).Cast<Language?>().ToList();
var filteredLanguages = new Dictionary<string, Language?>();
int totalCount = 0;
foreach (Language? language in fullLanguages)
{
var code = language.TwoLetterCode();
if (string.IsNullOrEmpty(code))
continue;
// Throw if the code already exists
if (filteredLanguages.ContainsKey(code))
throw new DuplicateNameException($"Code {code} already in dictionary");
filteredLanguages[code] = language;
totalCount++;
}
Assert.Equal(totalCount, filteredLanguages.Count);
}
/// <summary>
/// Ensure that every Language that has a standard/bibliographic ISO 639-2 code is unique
/// </summary>
[Fact]
public void LanguageNoDuplicateThreeLetterCodeTest()
{
var fullLanguages = Enum.GetValues(typeof(Language)).Cast<Language?>().ToList();
var filteredLanguages = new Dictionary<string, Language?>();
int totalCount = 0;
foreach (Language? language in fullLanguages)
{
var code = language.ThreeLetterCode();
if (string.IsNullOrEmpty(code))
continue;
// Throw if the code already exists
if (filteredLanguages.ContainsKey(code))
throw new DuplicateNameException($"Code {code} already in dictionary");
filteredLanguages[code] = language;
totalCount++;
}
Assert.Equal(totalCount, filteredLanguages.Count);
}
/// <summary>
/// Ensure that every Language that has a terminology ISO 639-2 code is unique
/// </summary>
[Fact]
public void LanguageNoDuplicateThreeLetterCodeAltTest()
{
var fullLanguages = Enum.GetValues(typeof(Language)).Cast<Language?>().ToList();
var filteredLanguages = new Dictionary<string, Language?>();
int totalCount = 0;
foreach (Language? language in fullLanguages)
{
var code = language.ThreeLetterCodeAlt();
if (string.IsNullOrEmpty(code))
continue;
// Throw if the code already exists
if (filteredLanguages.ContainsKey(code))
throw new DuplicateNameException($"Code {code} already in dictionary");
filteredLanguages[code] = language;
totalCount++;
}
Assert.Equal(totalCount, filteredLanguages.Count);
}
/// <summary>
/// Generate a test set of Language values
/// </summary>
/// <returns>MemberData-compatible list of Language values</returns>
public static List<object?[]> GenerateLanguageTestData()
{
var testData = new List<object?[]>() { new object?[] { null, true } };
foreach (Language? language in Enum.GetValues(typeof(Language)))
{
testData.Add(new object?[] { language, false });
}
return testData;
}
#endregion
#region Language Selection
/// <summary>
/// Check that every LanguageSelection has a long name provided
/// </summary>
/// <param name="languageSelection">LanguageSelection value to check</param>
/// <param name="expectNull">True to expect a null value, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateLanguageSelectionTestData))]
public void LanguageSelectionLongNameTest(LanguageSelection? languageSelection, bool expectNull)
{
var actual = languageSelection.LongName();
if (expectNull)
Assert.Null(actual);
else
Assert.NotNull(actual);
}
/// <summary>
/// Generate a test set of LanguageSelection values
/// </summary>
/// <returns>MemberData-compatible list of LanguageSelection values</returns>
public static List<object?[]> GenerateLanguageSelectionTestData()
{
var testData = new List<object?[]>() { new object?[] { null, true } };
foreach (LanguageSelection? languageSelection in Enum.GetValues(typeof(LanguageSelection)))
{
testData.Add(new object?[] { languageSelection, false });
}
return testData;
}
#endregion
#region Media Type
/// <summary>
/// Check that every MediaType has a long name provided
/// </summary>
/// <param name="mediaType">MediaType value to check</param>
/// <param name="expectNull">True to expect a null value, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateMediaTypeTestData))]
public void MediaTypeLongNameTest(MediaType? mediaType, bool expectNull)
{
var actual = mediaType.LongName();
if (expectNull)
Assert.Null(actual);
else
Assert.NotNull(actual);
}
/// <summary>
/// Check that every MediaType has a short name provided
/// </summary>
/// <param name="mediaType">MediaType value to check</param>
/// <param name="expectNull">True to expect a null value, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateMediaTypeTestData))]
public void MediaTypeShortNameTest(MediaType? mediaType, bool expectNull)
{
var actual = mediaType.ShortName();
if (expectNull)
Assert.Null(actual);
else
Assert.NotNull(actual);
}
/// <summary>
/// Generate a test set of MediaType values
/// </summary>
/// <returns>MemberData-compatible list of MediaType values</returns>
public static List<object?[]> GenerateMediaTypeTestData()
{
var testData = new List<object?[]>() { new object?[] { null, true } };
foreach (MediaType? mediaType in Enum.GetValues(typeof(MediaType)))
{
testData.Add(new object?[] { mediaType, false });
}
return testData;
}
#endregion
#region Region
/// <summary>
/// Check that every Region has a long name provided
/// </summary>
/// <param name="region">Region value to check</param>
/// <param name="expectNull">True to expect a null value, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateRegionTestData))]
public void RegionLongNameTest(Region? region, bool expectNull)
{
var actual = region.LongName();
if (expectNull)
Assert.Null(actual);
else
Assert.NotNull(actual);
}
/// <summary>
/// Check that every Region has a short name provided
/// </summary>
/// <param name="region">Region value to check</param>
/// <param name="expectNull">True to expect a null value, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateRegionTestData))]
public void RegionShortNameTest(Region? region, bool expectNull)
{
var actual = region.ShortName();
if (expectNull)
Assert.Null(actual);
else
Assert.NotNull(actual);
}
/// <summary>
/// Ensure that every Language that has an ISO 639-1 code is unique
/// </summary>
[Fact]
public void RegionNoDuplicateShortNameTest()
{
var fullRegions = Enum.GetValues(typeof(Region)).Cast<Region?>().ToList();
var filteredRegions = new Dictionary<string, Region?>();
int totalCount = 0;
foreach (Region? region in fullRegions)
{
var code = region.ShortName();
if (string.IsNullOrEmpty(code))
continue;
// Throw if the code already exists
if (filteredRegions.ContainsKey(code))
throw new DuplicateNameException($"Code {code} already in dictionary");
filteredRegions[code] = region;
totalCount++;
}
Assert.Equal(totalCount, filteredRegions.Count);
}
/// <summary>
/// Generate a test set of Region values
/// </summary>
/// <returns>MemberData-compatible list of Region values</returns>
public static List<object?[]> GenerateRegionTestData()
{
var testData = new List<object?[]>() { new object?[] { null, true } };
foreach (Region? region in Enum.GetValues(typeof(Region)))
{
testData.Add(new object?[] { region, false });
}
return testData;
}
#endregion
#region Site Code
/// <summary>
/// Check that every SiteCode has a long name provided
/// </summary>
/// <param name="siteCode">SiteCode value to check</param>
/// <param name="expectNull">True to expect a null value, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateSiteCodeTestData))]
public void SiteCodeLongNameTest(SiteCode? siteCode, bool expectNull)
{
var actual = siteCode.LongName();
if (expectNull)
Assert.Null(actual);
else
Assert.NotNull(actual);
}
/// <summary>
/// Check that every SiteCode has a short name provided
/// </summary>
/// <param name="siteCode">SiteCode value to check</param>
/// <param name="expectNull">True to expect a null value, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateSiteCodeTestData))]
public void SiteCodeShortNameTest(SiteCode? siteCode, bool expectNull)
{
var actual = siteCode.ShortName();
if (expectNull)
Assert.Null(actual);
else
Assert.NotNull(actual);
}
/// <summary>
/// Generate a test set of SiteCode values
/// </summary>
/// <returns>MemberData-compatible list of SiteCode values</returns>
public static List<object?[]> GenerateSiteCodeTestData()
{
var testData = new List<object?[]>() { new object?[] { null, true } };
foreach (SiteCode? siteCode in Enum.GetValues(typeof(SiteCode)))
{
testData.Add(new object?[] { siteCode, false });
}
return testData;
}
#endregion
#region System
/// <summary>
/// Check that every RedumpSystem has a long name provided
/// </summary>
/// <param name="redumpSystem">RedumpSystem value to check</param>
/// <param name="expectNull">True to expect a null value, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateRedumpSystemTestData))]
public void RedumpSystemLongNameTest(RedumpSystem? redumpSystem, bool expectNull)
{
var actual = redumpSystem.LongName();
if (expectNull)
Assert.Null(actual);
else
Assert.NotNull(actual);
}
// TODO: Re-enable the following test once non-Redump systems are accounted for
/// <summary>
/// Check that every RedumpSystem has a short name provided
/// </summary>
/// <param name="redumpSystem">RedumpSystem value to check</param>
/// <param name="expectNull">True to expect a null value, false otherwise</param>
//[Theory]
//[MemberData(nameof(GenerateRedumpSystemTestData))]
//public void RedumpSystemShortNameTest(RedumpSystem? redumpSystem, bool expectNull)
//{
// string actual = redumpSystem.ShortName();
// if (expectNull)
// Assert.Null(actual);
// else
// Assert.NotNull(actual);
//}
// TODO: Test the other attributes as well
// Most are bool checks so they're not as interesting to have unit tests around
// SystemCategory always returns something as well, so is it worth testing?
/// <summary>
/// Generate a test set of RedumpSystem values
/// </summary>
/// <returns>MemberData-compatible list of RedumpSystem values</returns>
public static List<object?[]> GenerateRedumpSystemTestData()
{
var testData = new List<object?[]>() { new object?[] { null, true } };
foreach (RedumpSystem? redumpSystem in Enum.GetValues(typeof(RedumpSystem)))
{
// We want to skip all markers for this
if (redumpSystem.IsMarker())
continue;
testData.Add(new object?[] { redumpSystem, false });
}
return testData;
}
#endregion
#region System Category
/// <summary>
/// Check that every SystemCategory has a long name provided
/// </summary>
/// <param name="systemCategory">SystemCategory value to check</param>
/// <param name="expectNull">True to expect a null value, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateSystemCategoryTestData))]
public void SystemCategoryLongNameTest(SystemCategory? systemCategory, bool expectNull)
{
var actual = systemCategory.LongName();
if (expectNull)
Assert.Null(actual);
else
Assert.NotNull(actual);
}
/// <summary>
/// Generate a test set of SystemCategory values
/// </summary>
/// <returns>MemberData-compatible list of SystemCategory values</returns>
public static List<object?[]> GenerateSystemCategoryTestData()
{
var testData = new List<object?[]>() { new object?[] { null, true } };
foreach (SystemCategory? systemCategory in Enum.GetValues(typeof(SystemCategory)))
{
if (systemCategory == SystemCategory.NONE)
testData.Add(new object?[] { systemCategory, true });
else
testData.Add(new object?[] { systemCategory, false });
}
return testData;
}
#endregion
#region Yes/No
/// <summary>
/// Check that every YesNo has a long name provided
/// </summary>
/// <param name="yesNo">YesNo value to check</param>
/// <param name="expectNull">True to expect a null value, false otherwise</param>
[Theory]
[MemberData(nameof(GenerateYesNoTestData))]
public void YesNoLongNameTest(YesNo? yesNo, bool expectNull)
{
string actual = yesNo.LongName();
if (expectNull)
Assert.Null(actual);
else
Assert.NotNull(actual);
}
/// <summary>
/// Generate a test set of YesNo values
/// </summary>
/// <returns>MemberData-compatible list of YesNo values</returns>
public static List<object?[]> GenerateYesNoTestData()
{
var testData = new List<object?[]>() { new object?[] { null, false } };
foreach (YesNo? yesNo in Enum.GetValues(typeof(YesNo)))
{
testData.Add(new object?[] { yesNo, false });
}
return testData;
}
#endregion
}
}

View File

@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.RedumpLib\SabreTools.RedumpLib.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="TestData\*" />
</ItemGroup>
<ItemGroup>
<Content Include="TestData\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeCoverage" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.abstractions" Version="2.0.3" />
<PackageReference Include="xunit.analyzers" Version="1.16.0" />
<PackageReference Include="xunit.assert" Version="2.9.2" />
<PackageReference Include="xunit.core" Version="2.9.2" />
<PackageReference Include="xunit.extensibility.core" Version="2.9.2" />
<PackageReference Include="xunit.extensibility.execution" Version="2.9.2" />
<PackageReference Include="xunit.runner.console" Version="2.9.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using SabreTools.RedumpLib.Data;
using Xunit;
namespace SabreTools.RedumpLib.Test
{
public class SubmissionInfoTests
{
[Fact]
public void EmptySerializationTest()
{
var submissionInfo = new SubmissionInfo();
string json = JsonConvert.SerializeObject(submissionInfo, Formatting.Indented);
Assert.NotNull(json);
}
[Fact]
public void PartialSerializationTest()
{
var submissionInfo = new SubmissionInfo()
{
CommonDiscInfo = new CommonDiscInfoSection(),
VersionAndEditions = new VersionAndEditionsSection(),
EDC = new EDCSection(),
ParentCloneRelationship = new ParentCloneRelationshipSection(),
Extras = new ExtrasSection(),
CopyProtection = new CopyProtectionSection(),
DumpersAndStatus = new DumpersAndStatusSection(),
TracksAndWriteOffsets = new TracksAndWriteOffsetsSection(),
SizeAndChecksums = new SizeAndChecksumsSection(),
};
string json = JsonConvert.SerializeObject(submissionInfo, Formatting.Indented);
Assert.NotNull(json);
}
[Fact]
public void FullSerializationTest()
{
var submissionInfo = new SubmissionInfo()
{
SchemaVersion = 1,
FullyMatchedID = 3,
PartiallyMatchedIDs = new List<int> { 0, 1, 2, 3 },
Added = DateTime.UtcNow,
LastModified = DateTime.UtcNow,
CommonDiscInfo = new CommonDiscInfoSection()
{
System = RedumpSystem.IBMPCcompatible,
Media = DiscType.CD,
Title = "Game Title",
ForeignTitleNonLatin = "Foreign Game Title",
DiscNumberLetter = "1",
DiscTitle = "Install Disc",
Category = DiscCategory.Games,
Region = Region.World,
Languages = new Language?[] { Language.English, Language.Spanish, Language.French },
LanguageSelection = new LanguageSelection?[] { LanguageSelection.BiosSettings },
Serial = "Disc Serial",
Layer0MasteringRing = "L0 Mastering Ring",
Layer0MasteringSID = "L0 Mastering SID",
Layer0ToolstampMasteringCode = "L0 Toolstamp",
Layer0MouldSID = "L0 Mould SID",
Layer0AdditionalMould = "L0 Additional Mould",
Layer1MasteringRing = "L1 Mastering Ring",
Layer1MasteringSID = "L1 Mastering SID",
Layer1ToolstampMasteringCode = "L1 Toolstamp",
Layer1MouldSID = "L1 Mould SID",
Layer1AdditionalMould = "L1 Additional Mould",
Layer2MasteringRing = "L2 Mastering Ring",
Layer2MasteringSID = "L2 Mastering SID",
Layer2ToolstampMasteringCode = "L2 Toolstamp",
Layer3MasteringRing = "L3 Mastering Ring",
Layer3MasteringSID = "L3 Mastering SID",
Layer3ToolstampMasteringCode = "L3 Toolstamp",
RingWriteOffset = "+12",
Barcode = "UPC Barcode",
EXEDateBuildDate = "19xx-xx-xx",
ErrorsCount = "0",
Comments = "Comment data line 1\r\nComment data line 2",
CommentsSpecialFields = new Dictionary<SiteCode, string>()
{
[SiteCode.ISBN] = "ISBN",
},
Contents = "Special contents 1\r\nSpecial contents 2",
ContentsSpecialFields = new Dictionary<SiteCode, string>()
{
[SiteCode.PlayableDemos] = "Game Demo 1",
},
},
VersionAndEditions = new VersionAndEditionsSection()
{
Version = "Original",
VersionDatfile = "Alt",
CommonEditions = new string[] { "Taikenban" },
OtherEditions = "Rerelease",
},
EDC = new EDCSection()
{
EDC = YesNo.Yes,
},
ParentCloneRelationship = new ParentCloneRelationshipSection()
{
ParentID = "12345",
RegionalParent = false,
},
Extras = new ExtrasSection()
{
PVD = "PVD",
DiscKey = "Disc key",
DiscID = "Disc ID",
PIC = "PIC",
Header = "Header",
BCA = "BCA",
SecuritySectorRanges = "SSv1 Ranges",
},
CopyProtection = new CopyProtectionSection()
{
AntiModchip = YesNo.Yes,
LibCrypt = YesNo.No,
LibCryptData = "LibCrypt data",
Protection = "List of protections",
SecuROMData = "SecuROM data",
},
DumpersAndStatus = new DumpersAndStatusSection()
{
Status = DumpStatus.TwoOrMoreGreen,
Dumpers = new string[] { "Dumper1", "Dumper2" },
OtherDumpers = "Dumper3",
},
TracksAndWriteOffsets = new TracksAndWriteOffsetsSection()
{
ClrMameProData = "Datfile",
Cuesheet = "Cuesheet",
CommonWriteOffsets = new int[] { 0, 12, -12 },
OtherWriteOffsets = "-2",
},
SizeAndChecksums = new SizeAndChecksumsSection()
{
Layerbreak = 0,
Layerbreak2 = 1,
Layerbreak3 = 2,
Size = 12345,
CRC32 = "CRC32",
MD5 = "MD5",
SHA1 = "SHA1",
},
DumpingInfo = new DumpingInfoSection()
{
DumpingProgram = "DiscImageCreator 20500101",
DumpingDate = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"),
Manufacturer = "ATAPI",
Model = "Optical Drive",
Firmware = "1.23",
ReportedDiscType = "CD-R",
},
Artifacts = new Dictionary<string, string>()
{
["Sample Artifact"] = "Sample Data",
},
};
string json = JsonConvert.SerializeObject(submissionInfo, Formatting.Indented);
Assert.NotNull(json);
}
}
}

View File

@@ -0,0 +1 @@
This isn't even JSON, I lied.

View File

@@ -0,0 +1,96 @@
{
"schema_version": 3,
"common_disc_info":
{
"d_system": "ajcd",
"d_media": "cd",
"d_title": "Test Title",
"d_title_foreign": "Foreign Title",
"d_number": "1",
"d_label": "Install",
"d_category": "Games",
"d_region": "U",
"d_languages":
[
"en",
"fr",
"es"
],
"d_languages_selection": [],
"d_serial": "Serial",
"d_ring_0_ma1": "Ringcode 0 Layer 0",
"d_ring_0_ma1_sid": "SID 0 Layer 0",
"d_ring_0_ts1": "Toolstamp 0 Layer 0",
"d_ring_0_mo1_sid": "Mould SID 0 Layer 0",
"d_ring_0_mo1": "Additional Mould 0 Layer 0",
"d_ring_0_ma2": "Ringcode 0 Layer 1",
"d_ring_0_ma2_sid": "SID 0 Layer 1",
"d_ring_0_ts2": "Toolstamp 0 Layer 1",
"d_ring_0_mo2_sid": "Mould SID 0 Layer 1",
"d_ring_0_mo2": "Additional Mould 0 Layer 1",
"d_ring_0_ma3": "Ringcode 0 Layer 2",
"d_ring_0_ma3_sid": "SID 0 Layer 2",
"d_ring_0_ts3": "Toolstamp 0 Layer 2",
"d_ring_0_ma4": "Ringcode 0 Layer 3",
"d_ring_0_ma4_sid": "SID 0 Layer 2",
"d_ring_0_ts4": "Toolstamp 0 Layer 2",
"d_ring_0_offsets": "-22",
"d_ring_0_0_value": "-21",
"d_barcode": "0 12345 67890 1",
"d_date": "1980-01-01",
"d_errors": "0",
"d_comments": "This is a comment\nwith a newline",
"d_contents": "These are contents, sorry"
},
"versions_and_editions":
{
"d_version": "1.0.0.0",
"d_version_datfile": "1.00",
"d_editions_text": "Demo"
},
"edc":
{
"d_edc": false
},
"parent_clone_relationship":
{
"d_parent_id": "12345",
"d_is_regional_parent": false
},
"extras":
{
"d_pvd": "Pretend\nthis\nis\na\nPVD",
"d_d1_key": "Disc key",
"d_d2_key": "Disc ID",
"d_pic_data": "Pretend\nthis\nis\na\nPIC",
"d_header": "Pretend\nthis\nis\na\nHeader",
"d_bca": "Pretend\nthis\nis\na\nBCA",
"d_ssranges": "Pretend\nthis\nis\na\nsecurity_range"
},
"copy_protection":
{
"d_protection_a": false,
"d_protection_1": false,
"d_libcrypt": "Definitely\nLibCrypt\nData",
"d_protection": "Super easy to find protection",
"d_securom": "Definitely\nSecuROM\nData"
},
"tracks_and_write_offsets":
{
"d_tracks": "Hash data",
"d_cue": "Real cuesheet",
"d_offset_text": "-22"
},
"size_and_checksums":
{
"d_layerbreak": 1,
"d_layerbreak_2": 2,
"d_layerbreak_3": 3,
"d_pic_identifier": "Pretend\nthis\nis\na\nPIC",
"d_size": 123456,
"d_crc32": "cbf43926",
"d_md5": "d41d8cd98f00b204e9800998ecf8427e",
"d_sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709"
}
}

View File

@@ -0,0 +1,4 @@
{
"invalid_key": "invalid_value",
"invalid_x": 12345
}

View File

@@ -0,0 +1,7 @@
{
"schema_version": 3,
"common_disc_info":
{
"d_title": "Test Title"
}
}

View File

@@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.RedumpLib", "SabreTools.RedumpLib.csproj", "{235D3A36-CA69-4348-9EC4-649B27ACFBB8}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.RedumpLib", "SabreTools.RedumpLib\SabreTools.RedumpLib.csproj", "{235D3A36-CA69-4348-9EC4-649B27ACFBB8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.RedumpLib.Test", "SabreTools.RedumpLib.Test\SabreTools.RedumpLib.Test.csproj", "{63519DEA-0C3D-4F0E-95EB-E9B6E1D55378}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -18,5 +20,9 @@ Global
{235D3A36-CA69-4348-9EC4-649B27ACFBB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{235D3A36-CA69-4348-9EC4-649B27ACFBB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{235D3A36-CA69-4348-9EC4-649B27ACFBB8}.Release|Any CPU.Build.0 = Release|Any CPU
{63519DEA-0C3D-4F0E-95EB-E9B6E1D55378}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{63519DEA-0C3D-4F0E-95EB-E9B6E1D55378}.Debug|Any CPU.Build.0 = Debug|Any CPU
{63519DEA-0C3D-4F0E-95EB-E9B6E1D55378}.Release|Any CPU.ActiveCfg = Release|Any CPU
{63519DEA-0C3D-4F0E-95EB-E9B6E1D55378}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -1,5 +1,7 @@
using System;
#if NET40_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
namespace SabreTools.RedumpLib.Attributes
{
@@ -25,24 +27,36 @@ namespace SabreTools.RedumpLib.Attributes
string? valueStr = value?.ToString();
if (string.IsNullOrEmpty(valueStr))
return null;
// Get the member info array
var memberInfos = enumType?.GetMember(valueStr);
if (memberInfos == null)
return null;
// Get the enum value info from the array, if possible
#if NET20 || NET35
System.Reflection.MemberInfo? enumValueMemberInfo = null;
foreach (var m in memberInfos)
{
if (m.DeclaringType != enumType)
continue;
enumValueMemberInfo = m;
break;
}
#else
var enumValueMemberInfo = memberInfos.FirstOrDefault(m => m.DeclaringType == enumType);
#endif
if (enumValueMemberInfo == null)
return null;
// Try to get the relevant attribute
var attributes = enumValueMemberInfo.GetCustomAttributes(typeof(HumanReadableAttribute), true);
if (attributes == null)
if (attributes == null || attributes.Length == 0)
return null;
// Return the first attribute, if possible
return attributes.FirstOrDefault() as HumanReadableAttribute;
return attributes[0] as HumanReadableAttribute;
}
}
}

View File

@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
#if NET40_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
@@ -246,25 +248,15 @@ namespace SabreTools.RedumpLib
/// <summary>
/// Fill out an existing SubmissionInfo object based on a disc page
/// </summary>
/// <param name="wc">RedumpWebClient for making the connection</param>
/// <param name="rc">RedumpClient for making the connection</param>
/// <param name="info">Existing SubmissionInfo object to fill</param>
/// <param name="id">Redump disc ID to retrieve</param>
/// <param name="includeAllData">True to include all pullable information, false to do bare minimum</param>
#if NETFRAMEWORK
public async static Task<bool> FillFromId(RedumpWebClient wc, SubmissionInfo info, int id, bool includeAllData)
#else
public async static Task<bool> FillFromId(RedumpHttpClient wc, SubmissionInfo info, int id, bool includeAllData)
#endif
public async static Task<bool> FillFromId(RedumpClient rc, SubmissionInfo info, int id, bool includeAllData)
{
// Ensure that required sections exist
info = EnsureAllSections(info);
#if NET40
var discData = await Task.Factory.StartNew(() => wc.DownloadSingleSiteID(id));
#elif NETFRAMEWORK
var discData = await Task.Run(() => wc.DownloadSingleSiteID(id));
#else
var discData = await wc.DownloadSingleSiteID(id);
#endif
var discData = await rc.DownloadSingleSiteID(id);
if (string.IsNullOrEmpty(discData))
return false;
@@ -279,22 +271,30 @@ namespace SabreTools.RedumpLib
if (title != null && firstParenLocation >= 0)
{
info.CommonDiscInfo!.Title = title.Substring(0, firstParenLocation);
var subMatches = Constants.DiscNumberLetterRegex.Matches(title);
foreach (Match subMatch in subMatches.Cast<Match>())
var submatches = Constants.DiscNumberLetterRegex.Matches(title);
#if NET20 || NET35
foreach (Match submatch in submatches)
#else
foreach (Match submatch in submatches.Cast<Match>())
#endif
{
var subMatchValue = subMatch.Groups[1].Value;
var submatchValue = submatch.Groups[1].Value;
// Disc number or letter
if (subMatchValue.StartsWith("Disc"))
info.CommonDiscInfo.DiscNumberLetter = subMatchValue.Remove(0, "Disc ".Length);
if (submatchValue.StartsWith("Disc"))
info.CommonDiscInfo.DiscNumberLetter = submatchValue.Remove(0, "Disc ".Length);
// Issue number
else if (subMatchValue.All(c => char.IsNumber(c)))
info.CommonDiscInfo.Title += $" ({subMatchValue})";
#if NET20 || NET35
else if (long.TryParse(submatchValue, out _))
#else
else if (submatchValue.All(c => char.IsNumber(c)))
#endif
info.CommonDiscInfo.Title += $" ({submatchValue})";
// Disc title
else
info.CommonDiscInfo.DiscTitle = subMatchValue;
info.CommonDiscInfo.DiscTitle = submatchValue;
}
}
// Otherwise, leave the title as-is
@@ -331,12 +331,18 @@ namespace SabreTools.RedumpLib
if (matches.Count > 0)
{
var tempLanguages = new List<Language?>();
#if NET20 || NET35
foreach (Match submatch in matches)
#else
foreach (Match submatch in matches.Cast<Match>())
#endif
{
tempLanguages.Add(Extensions.ToLanguage(submatch.Groups[1].Value));
var language = Extensions.ToLanguage(submatch.Groups[1].Value);
if (language != null)
tempLanguages.Add(language);
}
info.CommonDiscInfo.Languages = tempLanguages.Where(l => l != null).ToArray();
info.CommonDiscInfo.Languages = [.. tempLanguages];
}
// Serial
@@ -376,7 +382,11 @@ namespace SabreTools.RedumpLib
tempDumpers.Add(dumper);
}
#if NET20 || NET35
foreach (Match submatch in matches)
#else
foreach (Match submatch in matches.Cast<Match>())
#endif
{
string? dumper = WebUtility.HtmlDecode(submatch.Groups[1].Value);
if (dumper != null)
@@ -386,6 +396,15 @@ namespace SabreTools.RedumpLib
info.DumpersAndStatus.Dumpers = [.. tempDumpers];
}
// PS3 DiscKey
if (string.IsNullOrEmpty(info.Extras!.DiscKey))
{
// Validate key is not NULL
match = Constants.PS3DiscKey.Match(discData);
if (match.Success && match.Groups[1].Value != "<span class=\"null\">NULL</span>")
info.Extras.DiscKey = match.Groups[1].Value;
}
// TODO: Unify handling of fields that can include site codes (Comments/Contents)
// Comments

View File

@@ -10,11 +10,21 @@ namespace SabreTools.RedumpLib.Converters
/// </summary>
public class DiscCategoryConverter : JsonConverter<DiscCategory?>
{
public override bool CanRead { get { return false; } }
public override bool CanRead { get { return true; } }
public override DiscCategory? ReadJson(JsonReader reader, Type objectType, DiscCategory? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
// If we have a value already, don't overwrite it
if (hasExistingValue)
return existingValue;
// Read the value
string? value = reader.Value as string;
if (value == null)
return null;
// Try to parse the value
return Data.Extensions.ToDiscCategory(value);
}
public override void WriteJson(JsonWriter writer, DiscCategory? value, JsonSerializer serializer)

View File

@@ -10,11 +10,21 @@ namespace SabreTools.RedumpLib.Converters
/// </summary>
public class DiscTypeConverter : JsonConverter<DiscType?>
{
public override bool CanRead { get { return false; } }
public override bool CanRead { get { return true; } }
public override DiscType? ReadJson(JsonReader reader, Type objectType, DiscType? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
// If we have a value already, don't overwrite it
if (hasExistingValue)
return existingValue;
// Read the value
string? value = reader.Value as string;
if (value == null)
return null;
// Try to parse the value
return Data.Extensions.ToDiscType(value);
}
public override void WriteJson(JsonWriter writer, DiscType? value, JsonSerializer serializer)

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SabreTools.RedumpLib.Data;
@@ -10,11 +11,31 @@ namespace SabreTools.RedumpLib.Converters
/// </summary>
public class LanguageConverter : JsonConverter<Language?[]>
{
public override bool CanRead { get { return false; } }
public override bool CanRead { get { return true; } }
public override Language?[] ReadJson(JsonReader reader, Type objectType, Language?[]? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
// If we have a value already, don't overwrite it
if (hasExistingValue)
return existingValue ?? [];
// Get the current depth for checking
int currentDepth = reader.Depth;
// Read the array while it exists
List<Language> languages = [];
while (reader.Read() && reader.Depth > currentDepth)
{
string? value = reader.Value as string;
if (value == null)
continue;
Language? lang = Data.Extensions.ToLanguage(value);
if (lang != null)
languages.Add(lang.Value);
}
return [.. languages];
}
public override void WriteJson(JsonWriter writer, Language?[]? value, JsonSerializer serializer)

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SabreTools.RedumpLib.Data;
@@ -10,11 +11,31 @@ namespace SabreTools.RedumpLib.Converters
/// </summary>
public class LanguageSelectionConverter : JsonConverter<LanguageSelection?[]>
{
public override bool CanRead { get { return false; } }
public override bool CanRead { get { return true; } }
public override LanguageSelection?[] ReadJson(JsonReader reader, Type objectType, LanguageSelection?[]? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
// If we have a value already, don't overwrite it
if (hasExistingValue)
return existingValue ?? [];
// Get the current depth for checking
int currentDepth = reader.Depth;
// Read the array while it exists
List<LanguageSelection> selections = [];
while (reader.Read() && reader.Depth > currentDepth)
{
string? value = reader.Value as string;
if (value == null)
continue;
LanguageSelection? sel = Data.Extensions.ToLanguageSelection(value);
if (sel != null)
selections.Add(sel.Value);
}
return [.. selections];
}
public override void WriteJson(JsonWriter writer, LanguageSelection?[]? value, JsonSerializer serializer)

View File

@@ -10,11 +10,21 @@ namespace SabreTools.RedumpLib.Converters
/// </summary>
public class RegionConverter : JsonConverter<Region?>
{
public override bool CanRead { get { return false; } }
public override bool CanRead { get { return true; } }
public override Region? ReadJson(JsonReader reader, Type objectType, Region? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
// If we have a value already, don't overwrite it
if (hasExistingValue)
return existingValue;
// Read the value
string? value = reader.Value as string;
if (value == null)
return null;
// Try to parse the value
return Data.Extensions.ToRegion(value);
}
public override void WriteJson(JsonWriter writer, Region? value, JsonSerializer serializer)

View File

@@ -10,11 +10,21 @@ namespace SabreTools.RedumpLib.Converters
/// </summary>
public class SystemConverter : JsonConverter<RedumpSystem?>
{
public override bool CanRead { get { return false; } }
public override bool CanRead { get { return true; } }
public override RedumpSystem? ReadJson(JsonReader reader, Type objectType, RedumpSystem? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
// If we have a value already, don't overwrite it
if (hasExistingValue)
return existingValue;
// Read the value
string? value = reader.Value as string;
if (value == null)
return null;
// Try to parse the value
return Data.Extensions.ToRedumpSystem(value);
}
public override void WriteJson(JsonWriter writer, RedumpSystem? value, JsonSerializer serializer)

View File

@@ -10,11 +10,21 @@ namespace SabreTools.RedumpLib.Converters
/// </summary>
public class YesNoConverter : JsonConverter<YesNo?>
{
public override bool CanRead { get { return false; } }
public override bool CanRead { get { return true; } }
public override YesNo? ReadJson(JsonReader reader, Type objectType, YesNo? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
// If we have a value already, don't overwrite it
if (hasExistingValue)
return existingValue;
// Read the value
if (reader.Value is bool bVal)
return Data.Extensions.ToYesNo(bVal);
else if (reader.Value is string sVal)
return Data.Extensions.ToYesNo(sVal);
return null;
}
public override void WriteJson(JsonWriter writer, YesNo? value, JsonSerializer serializer)

View File

@@ -102,6 +102,11 @@ namespace SabreTools.RedumpLib.Data
/// </summary>
public static Regex PartialMatchRegex = new(@"<td class=""static"">partial match ids: (.*?)</td>");
/// <summary>
/// Regex matching the disc key on a PS3 disc page
/// </summary>
public static Regex PS3DiscKey = new(@"<th>Disc Key</th><th>Disc ID</th><th>Permanent Information & Control \(PIC\)</th></tr><tr><td>(.*?)</td><td>");
/// <summary>
/// Regex matching the PVD field on a disc page
/// </summary>

View File

@@ -2234,7 +2234,7 @@ namespace SabreTools.RedumpLib.Data
[System(Category = SystemCategory.Computer, LongName = "NEC PC-98 series", ShortName = "pc-98", HasCues = true, HasDat = true)]
NECPC98series,
[System(Category = SystemCategory.Computer, LongName = "Sharp X68000", ShortName = "x86kcd", HasCues = true, HasDat = true)]
[System(Category = SystemCategory.Computer, LongName = "Sharp X68000", ShortName = "x68k", HasCues = true, HasDat = true)]
SharpX68000,
// End of computer section delimiter
@@ -2331,7 +2331,7 @@ namespace SabreTools.RedumpLib.Data
[System(Category = SystemCategory.Arcade, Available = false, LongName = "Merit Industries MegaTouch XL")]
MeritIndustriesMegaTouchXL,
[System(Category = SystemCategory.Arcade, LongName = "Namco · Sega · Nintendo Triforce", ShortName = "triforce", HasCues = true, HasDat = true, HasGdi = true)]
[System(Category = SystemCategory.Arcade, LongName = "Namco · Sega · Nintendo Triforce", ShortName = "trf", HasCues = true, HasDat = true, HasGdi = true)]
NamcoSegaNintendoTriforce,
[System(Category = SystemCategory.Arcade, LongName = "Namco System 12", ShortName = "ns12")]
@@ -3526,16 +3526,28 @@ namespace SabreTools.RedumpLib.Data
[HumanReadable(ShortName = "[T:ALTF]", LongName = "<b>Alternative Foreign Title</b>:")]
AlternativeForeignTitle,
// TODO: This doesn't have a site tag yet
[HumanReadable(ShortName = "<b>Applications</b>:", LongName = "<b>Applications</b>:")]
Applications,
[HumanReadable(ShortName = "[T:BID]", LongName = "<b>Bandai ID</b>:")]
BandaiID,
[HumanReadable(ShortName = "[T:BBFC]", LongName = "<b>BBFC Reg. No.</b>:")]
BBFCRegistrationNumber,
// TODO: This doesn't have a site tag yet
[HumanReadable(ShortName = "<b>Bethesda ID</b>:", LongName = "<b>Bethesda ID</b>:")]
BethesdaID,
// TODO: This doesn't have a site tag yet
[HumanReadable(ShortName = "<b>CD Projekt ID</b>:", LongName = "<b>CD Projekt ID</b>:")]
CDProjektID,
// TODO: This doesn't have a site tag yet
[HumanReadable(ShortName = "<b>Compatible OS</b>:", LongName = "<b>Compatible OS</b>:")]
CompatibleOS,
// TODO: This doesn't have a site tag yet
[HumanReadable(ShortName = "<b>Disc Hologram ID</b>:", LongName = "<b>Disc Hologram ID</b>:")]
DiscHologramID,
@@ -3547,6 +3559,10 @@ namespace SabreTools.RedumpLib.Data
[HumanReadable(ShortName = "[T:DNAS]", LongName = "<b>DNAS Disc ID</b>:")]
DNASDiscID,
// TODO: This doesn't have a site tag yet
[HumanReadable(ShortName = "<b>Eidos ID</b>:", LongName = "<b>Eidos ID</b>:")]
EidosID,
[HumanReadable(ShortName = "[T:EAID]", LongName = "<b>Electronic Arts ID</b>:")]
ElectronicArtsID,

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
#if NET40_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
using SabreTools.RedumpLib.Attributes;
namespace SabreTools.RedumpLib.Data
@@ -923,28 +925,71 @@ namespace SabreTools.RedumpLib.Data
/// <returns>Language represented by the string, if possible</returns>
public static Language? ToLanguage(string lang)
{
var languages = Enum.GetValues(typeof(Language)).Cast<Language?>().ToList();
#if NET20 || NET35
var languages = new List<Language?>();
foreach (Language l in Enum.GetValues(typeof(Language)))
{
languages.Add(new Nullable<Language>(l));
}
#else
var languages = Enum.GetValues(typeof(Language))
.Cast<Language?>()
.ToList();
#endif
// Check ISO 639-1 codes
#if NET20 || NET35
var languageMapping = new Dictionary<string, Language?>();
foreach (var l in languages)
{
if (l.TwoLetterCode() == null)
continue;
languageMapping[l.TwoLetterCode() ?? string.Empty] = l;
}
#else
Dictionary<string, Language?> languageMapping = languages
.Where(l => l.TwoLetterCode() != null)
.ToDictionary(l => l.TwoLetterCode() ?? string.Empty, l => l);
#endif
if (languageMapping.ContainsKey(lang))
return languageMapping[lang];
// Check standard ISO 639-2 codes
#if NET20 || NET35
languageMapping = new Dictionary<string, Language?>();
foreach (var l in languages)
{
if (l.ThreeLetterCode() == null)
continue;
languageMapping[l.ThreeLetterCode() ?? string.Empty] = l;
}
#else
languageMapping = languages
.Where(l => l.ThreeLetterCode() != null)
.ToDictionary(l => l.ThreeLetterCode() ?? string.Empty, l => l);
#endif
if (languageMapping.ContainsKey(lang))
return languageMapping[lang];
// Check alternate ISO 639-2 codes
#if NET20 || NET35
languageMapping = new Dictionary<string, Language?>();
foreach (var l in languages)
{
if (l.ThreeLetterCodeAlt() == null)
continue;
languageMapping[l.ThreeLetterCodeAlt() ?? string.Empty] = l;
}
#else
languageMapping = languages
.Where(l => l.ThreeLetterCodeAlt() != null)
.ToDictionary(l => l.ThreeLetterCodeAlt() ?? string.Empty, l => l);
#endif
if (languageMapping.ContainsKey(lang))
return languageMapping[lang];
@@ -984,6 +1029,29 @@ namespace SabreTools.RedumpLib.Data
/// <returns>String representing the value, if possible</returns>
public static string? LongName(this LanguageSelection? langSelect) => AttributeHelper<LanguageSelection?>.GetAttribute(langSelect)?.LongName;
/// <summary>
/// Get the LanguageSelection enum value for a given string
/// </summary>
/// <param name="langSelect">String value to convert</param>
/// <returns>LanguageSelection represented by the string, if possible</returns>
public static LanguageSelection? ToLanguageSelection(string langSelect)
{
return (langSelect?.ToLowerInvariant()) switch
{
"bios"
or "biossettings"
or "bios settings" => (LanguageSelection?)LanguageSelection.BiosSettings,
"selector"
or "langselector"
or "lang selector"
or "langauge selector" => (LanguageSelection?)LanguageSelection.LanguageSelector,
"options"
or "optionsmenu"
or "options menu" => (LanguageSelection?)LanguageSelection.OptionsMenu,
_ => null,
};
}
#endregion
#region Media Type
@@ -1046,12 +1114,33 @@ namespace SabreTools.RedumpLib.Data
public static Region? ToRegion(string region)
{
region = region.ToLowerInvariant();
var regions = Enum.GetValues(typeof(Region)).Cast<Region?>().ToList();
#if NET20 || NET35
var regions = new List<Region?>();
foreach (Region r in Enum.GetValues(typeof(Region)))
{
regions.Add(new Nullable<Region>(r));
}
#else
var regions = Enum.GetValues(typeof(Region))
.Cast<Region?>()
.ToList();
#endif
// Check ISO 3166-1 alpha-2 codes
#if NET20 || NET35
var regionMapping = new Dictionary<string, Region?>();
foreach (var r in regions)
{
if (r.ShortName() == null)
continue;
regionMapping[r.ShortName()?.ToLowerInvariant() ?? string.Empty] = r;
}
#else
Dictionary<string, Region?> regionMapping = regions
.Where(r => r.ShortName() != null)
.ToDictionary(r => r.ShortName()?.ToLowerInvariant() ?? string.Empty, r => r);
#endif
if (regionMapping.ContainsKey(region))
return regionMapping[region];
@@ -1063,6 +1152,148 @@ namespace SabreTools.RedumpLib.Data
#region Site Code
/// <summary>
/// List all site codes with their short usable names
/// </summary>
public static List<string> ListSiteCodes()
{
var siteCodes = new List<string>();
foreach (var val in Enum.GetValues(typeof(SiteCode)))
{
string? shortName = ((SiteCode?)val).ShortName()?.TrimEnd(':');
string? longName = ((SiteCode?)val).LongName()?.TrimEnd(':');
bool isCommentCode = ((SiteCode?)val).IsCommentCode();
bool isContentCode = ((SiteCode?)val).IsContentCode();
bool isMultiline = ((SiteCode?)val).IsMultiLine();
// Invalid codes should be skipped
if (shortName == null || longName == null)
continue;
// Handle site tags
string siteCode;
if (shortName == longName)
siteCode = "***".PadRight(16, ' ');
else
siteCode = shortName.PadRight(16, ' ');
// Handle expanded tags
siteCode += longName.PadRight(32, ' ');
// Include special indicators, if necessary
var additionalInfo = new List<string>();
if (isCommentCode)
additionalInfo.Add("Comment Field");
if (isContentCode)
additionalInfo.Add("Content Field");
if (isMultiline)
additionalInfo.Add("Multiline");
if (additionalInfo.Count > 0)
siteCode += $"[{string.Join(", ", [.. additionalInfo])}]";
// Add the formatted site code
siteCodes.Add(siteCode);
}
return siteCodes;
}
/// <summary>
/// Check if a site code should live in the comments section
/// </summary>
/// <param name="siteCode">SiteCode to check</param>
/// <returns>True if the code field is in comments by default, false otherwise</returns>
public static bool IsCommentCode(this SiteCode? siteCode)
{
return siteCode switch
{
// Identifying Info
SiteCode.AlternativeTitle => true,
SiteCode.AlternativeForeignTitle => true,
SiteCode.BBFCRegistrationNumber => true,
SiteCode.CompatibleOS => true,
SiteCode.DiscHologramID => true,
SiteCode.DMIHash => true,
SiteCode.DNASDiscID => true,
SiteCode.Filename => true,
SiteCode.Genre => true,
SiteCode.InternalName => true,
SiteCode.InternalSerialName => true,
SiteCode.ISBN => true,
SiteCode.ISSN => true,
SiteCode.Multisession => true,
SiteCode.PFIHash => true,
SiteCode.PostgapType => true,
SiteCode.PPN => true,
SiteCode.RingNonZeroDataStart => true,
SiteCode.Series => true,
SiteCode.SSHash => true,
SiteCode.SSVersion => true,
SiteCode.UniversalHash => true,
SiteCode.VCD => true,
SiteCode.VFCCode => true,
SiteCode.VolumeLabel => true,
SiteCode.XeMID => true,
SiteCode.XMID => true,
// Publisher / Company IDs
SiteCode.AcclaimID => true,
SiteCode.ActivisionID => true,
SiteCode.BandaiID => true,
SiteCode.BethesdaID => true,
SiteCode.CDProjektID => true,
SiteCode.EidosID => true,
SiteCode.ElectronicArtsID => true,
SiteCode.FoxInteractiveID => true,
SiteCode.GTInteractiveID => true,
SiteCode.JASRACID => true,
SiteCode.KingRecordsID => true,
SiteCode.KoeiID => true,
SiteCode.KonamiID => true,
SiteCode.LucasArtsID => true,
SiteCode.MicrosoftID => true,
SiteCode.NaganoID => true,
SiteCode.NamcoID => true,
SiteCode.NipponIchiSoftwareID => true,
SiteCode.OriginID => true,
SiteCode.PonyCanyonID => true,
SiteCode.SegaID => true,
SiteCode.SelenID => true,
SiteCode.SierraID => true,
SiteCode.TaitoID => true,
SiteCode.UbisoftID => true,
SiteCode.ValveID => true,
_ => false,
};
}
/// <summary>
/// Check if a site code should live in the contents section
/// </summary>
/// <param name="siteCode">SiteCode to check</param>
/// <returns>True if the code field is in contents by default, false otherwise</returns>
public static bool IsContentCode(this SiteCode? siteCode)
{
return siteCode switch
{
SiteCode.Applications => true,
SiteCode.Extras => true,
SiteCode.GameFootage => true,
SiteCode.Games => true,
SiteCode.NetYarozeGames => true,
SiteCode.Patches => true,
SiteCode.PlayableDemos => true,
SiteCode.RollingDemos => true,
SiteCode.Savegames => true,
SiteCode.TechDemos => true,
SiteCode.Videos => true,
_ => false,
};
}
/// <summary>
/// Check if a site code is multi-line or not
/// </summary>
@@ -1220,10 +1451,22 @@ namespace SabreTools.RedumpLib.Data
{
var systems = new List<string>();
#if NET20 || NET35
var knownSystems = new List<RedumpSystem?>();
foreach (RedumpSystem s in Enum.GetValues(typeof(RedumpSystem)))
{
var ns = new Nullable<RedumpSystem>(s);
if (ns != null && !ns.IsMarker() && ns.GetCategory() != SystemCategory.NONE)
knownSystems.Add(ns);
}
knownSystems.Sort((x, y) => (x.LongName() ?? string.Empty).CompareTo(y.LongName() ?? string.Empty));
#else
var knownSystems = Enum.GetValues(typeof(RedumpSystem))
.OfType<RedumpSystem?>()
.Where(s => s != null && !s.IsMarker() && s.GetCategory() != SystemCategory.NONE)
.OrderBy(s => s.LongName() ?? string.Empty);
#endif
foreach (var val in knownSystems)
{

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
#if NET40_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
using Newtonsoft.Json;
using SabreTools.RedumpLib.Converters;
@@ -73,6 +75,20 @@ namespace SabreTools.RedumpLib.Data
public object Clone()
{
#if NET20 || NET35
Dictionary<string, string>? artifacts = null;
if (this.Artifacts != null)
{
artifacts = new Dictionary<string, string>();
foreach (var kvp in this.Artifacts)
{
artifacts[kvp.Key] = kvp.Value;
}
}
#else
var artifacts = this.Artifacts?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
#endif
return new SubmissionInfo
{
SchemaVersion = this.SchemaVersion,
@@ -90,7 +106,7 @@ namespace SabreTools.RedumpLib.Data
TracksAndWriteOffsets = this.TracksAndWriteOffsets?.Clone() as TracksAndWriteOffsetsSection,
SizeAndChecksums = this.SizeAndChecksums?.Clone() as SizeAndChecksumsSection,
DumpingInfo = this.DumpingInfo?.Clone() as DumpingInfoSection,
Artifacts = this.Artifacts?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
Artifacts = artifacts,
};
}
}
@@ -101,16 +117,16 @@ namespace SabreTools.RedumpLib.Data
public class CommonDiscInfoSection : ICloneable
{
// Name not defined by Redump
[JsonProperty(PropertyName = "d_system", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_system", DefaultValueHandling = DefaultValueHandling.Include)]
[JsonConverter(typeof(SystemConverter))]
public RedumpSystem? System { get; set; }
// Name not defined by Redump
[JsonProperty(PropertyName = "d_media", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_media", DefaultValueHandling = DefaultValueHandling.Include)]
[JsonConverter(typeof(DiscTypeConverter))]
public DiscType? Media { get; set; }
[JsonProperty(PropertyName = "d_title", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_title", DefaultValueHandling = DefaultValueHandling.Include)]
public string? Title { get; set; }
[JsonProperty(PropertyName = "d_title_foreign", DefaultValueHandling = DefaultValueHandling.Ignore)]
@@ -122,15 +138,15 @@ namespace SabreTools.RedumpLib.Data
[JsonProperty(PropertyName = "d_label", NullValueHandling = NullValueHandling.Ignore)]
public string? DiscTitle { get; set; }
[JsonProperty(PropertyName = "d_category", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_category", DefaultValueHandling = DefaultValueHandling.Include)]
[JsonConverter(typeof(DiscCategoryConverter))]
public DiscCategory? Category { get; set; }
[JsonProperty(PropertyName = "d_region", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_region", DefaultValueHandling = DefaultValueHandling.Include)]
[JsonConverter(typeof(RegionConverter))]
public Region? Region { get; set; }
[JsonProperty(PropertyName = "d_languages", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_languages", DefaultValueHandling = DefaultValueHandling.Include)]
[JsonConverter(typeof(LanguageConverter))]
public Language?[]? Languages { get; set; }
@@ -147,7 +163,7 @@ namespace SabreTools.RedumpLib.Data
[JsonProperty(PropertyName = "d_ring_0_id", NullValueHandling = NullValueHandling.Ignore)]
public string? RingId { get; private set; }
[JsonProperty(PropertyName = "d_ring_0_ma1", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_ring_0_ma1", DefaultValueHandling = DefaultValueHandling.Include)]
public string? Layer0MasteringRing { get; set; }
[JsonProperty(PropertyName = "d_ring_0_ma1_sid", NullValueHandling = NullValueHandling.Ignore)]
@@ -162,7 +178,7 @@ namespace SabreTools.RedumpLib.Data
[JsonProperty(PropertyName = "d_ring_0_mo1", NullValueHandling = NullValueHandling.Ignore)]
public string? Layer0AdditionalMould { get; set; }
[JsonProperty(PropertyName = "d_ring_0_ma2", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_ring_0_ma2", DefaultValueHandling = DefaultValueHandling.Include)]
public string? Layer1MasteringRing { get; set; }
[JsonProperty(PropertyName = "d_ring_0_ma2_sid", NullValueHandling = NullValueHandling.Ignore)]
@@ -177,7 +193,7 @@ namespace SabreTools.RedumpLib.Data
[JsonProperty(PropertyName = "d_ring_0_mo2", NullValueHandling = NullValueHandling.Ignore)]
public string? Layer1AdditionalMould { get; set; }
[JsonProperty(PropertyName = "d_ring_0_ma3", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_ring_0_ma3", DefaultValueHandling = DefaultValueHandling.Include)]
public string? Layer2MasteringRing { get; set; }
[JsonProperty(PropertyName = "d_ring_0_ma3_sid", NullValueHandling = NullValueHandling.Ignore)]
@@ -186,7 +202,7 @@ namespace SabreTools.RedumpLib.Data
[JsonProperty(PropertyName = "d_ring_0_ts3", NullValueHandling = NullValueHandling.Ignore)]
public string? Layer2ToolstampMasteringCode { get; set; }
[JsonProperty(PropertyName = "d_ring_0_ma4", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_ring_0_ma4", DefaultValueHandling = DefaultValueHandling.Include)]
public string? Layer3MasteringRing { get; set; }
[JsonProperty(PropertyName = "d_ring_0_ma4_sid", NullValueHandling = NullValueHandling.Ignore)]
@@ -233,6 +249,31 @@ namespace SabreTools.RedumpLib.Data
public object Clone()
{
#if NET20 || NET35
Dictionary<SiteCode, string>? commentsSpecialFields = null;
if (this.CommentsSpecialFields != null)
{
commentsSpecialFields = new Dictionary<SiteCode, string>();
foreach (var kvp in this.CommentsSpecialFields)
{
commentsSpecialFields[kvp.Key] = kvp.Value;
}
}
Dictionary<SiteCode, string>? contentsSpecialFields = null;
if (this.ContentsSpecialFields != null)
{
contentsSpecialFields = new Dictionary<SiteCode, string>();
foreach (var kvp in this.ContentsSpecialFields)
{
contentsSpecialFields[kvp.Key] = kvp.Value;
}
}
#else
var commentsSpecialFields = this.CommentsSpecialFields?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
var contentsSpecialFields = this.ContentsSpecialFields?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
#endif
return new CommonDiscInfoSection
{
System = this.System,
@@ -271,9 +312,9 @@ namespace SabreTools.RedumpLib.Data
EXEDateBuildDate = this.EXEDateBuildDate,
ErrorsCount = this.ErrorsCount,
Comments = this.Comments,
CommentsSpecialFields = this.CommentsSpecialFields?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
CommentsSpecialFields = commentsSpecialFields,
Contents = this.Contents,
ContentsSpecialFields = this.ContentsSpecialFields?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
ContentsSpecialFields = contentsSpecialFields,
};
}
}
@@ -414,13 +455,27 @@ namespace SabreTools.RedumpLib.Data
public object Clone()
{
#if NET20 || NET35
Dictionary<string, List<string>?>? fullProtections = null;
if (this.FullProtections != null)
{
fullProtections = new Dictionary<string, List<string>?>();
foreach (var kvp in this.FullProtections)
{
fullProtections[kvp.Key] = kvp.Value;
}
}
#else
var fullProtections = this.FullProtections?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
#endif
return new CopyProtectionSection
{
AntiModchip = this.AntiModchip,
LibCrypt = this.LibCrypt,
LibCryptData = this.LibCryptData,
Protection = this.Protection,
FullProtections = this.FullProtections?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
FullProtections = fullProtections,
SecuROMData = this.SecuROMData,
};
}
@@ -530,40 +585,55 @@ namespace SabreTools.RedumpLib.Data
/// </summary>
public class DumpingInfoSection : ICloneable
{
// Name not defined by Redump -- Only used with MPF
[JsonProperty(PropertyName = "d_frontend_version", DefaultValueHandling = DefaultValueHandling.Include)]
public string? FrontendVersion { get; set; }
// Name not defined by Redump
[JsonProperty(PropertyName = "d_dumping_program", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_dumping_program", DefaultValueHandling = DefaultValueHandling.Include)]
public string? DumpingProgram { get; set; }
// Name not defined by Redump
[JsonProperty(PropertyName = "d_dumping_date", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_dumping_date", DefaultValueHandling = DefaultValueHandling.Include)]
public string? DumpingDate { get; set; }
// Name not defined by Redump
[JsonProperty(PropertyName = "d_drive_manufacturer", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_dumping_params", DefaultValueHandling = DefaultValueHandling.Include)]
public string? DumpingParameters { get; set; }
// Name not defined by Redump
[JsonProperty(PropertyName = "d_drive_manufacturer", DefaultValueHandling = DefaultValueHandling.Include)]
public string? Manufacturer { get; set; }
// Name not defined by Redump
[JsonProperty(PropertyName = "d_drive_model", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_drive_model", DefaultValueHandling = DefaultValueHandling.Include)]
public string? Model { get; set; }
// Name not defined by Redump
[JsonProperty(PropertyName = "d_drive_firmware", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_drive_firmware", DefaultValueHandling = DefaultValueHandling.Include)]
public string? Firmware { get; set; }
// Name not defined by Redump
[JsonProperty(PropertyName = "d_reported_disc_type", Required = Required.AllowNull)]
[JsonProperty(PropertyName = "d_reported_disc_type", DefaultValueHandling = DefaultValueHandling.Include)]
public string? ReportedDiscType { get; set; }
// Name not defined by Redump -- Only used with Redumper
[JsonProperty(PropertyName = "d_errors_c2", NullValueHandling = NullValueHandling.Ignore)]
public string? C2ErrorsCount { get; set; }
public object Clone()
{
return new DumpingInfoSection
{
FrontendVersion = this.FrontendVersion,
DumpingProgram = this.DumpingProgram,
DumpingDate = this.DumpingDate,
DumpingParameters = this.DumpingParameters,
Manufacturer = this.Manufacturer,
Model = this.Model,
Firmware = this.Firmware,
ReportedDiscType = this.ReportedDiscType,
C2ErrorsCount = this.C2ErrorsCount,
};
}
}

View File

@@ -35,12 +35,15 @@
// Automatic Information
public const string FrontendVersionField = "Frontend Version";
public const string DumpingProgramField = "Dumping Program";
public const string DumpingDateField = "Date";
public const string DumpingParametersField = "Parameters";
public const string DumpingDriveManufacturer = "Manufacturer";
public const string DumpingDriveModel = "Model";
public const string DumpingDriveFirmware = "Firmware";
public const string ReportedDiscType = "Reported Disc Type";
public const string C2ErrorCountField = "C2 Error Count";
public const string PVDField = "Primary Volume Descriptor (PVD)";
public const string DATField = "DAT";
public const string SizeField = "Size";

View File

@@ -0,0 +1,145 @@
using System.Threading.Tasks;
using SabreTools.RedumpLib.Data;
using SabreTools.RedumpLib.Web;
namespace SabreTools.RedumpLib
{
/// <summary>
/// Contains logic for dealing with downloads
/// </summary>
public class Downloader
{
#region Properties
/// <summary>
/// Which Redump feature is being used
/// </summary>
public Feature Feature { get; set; }
/// <summary>
/// Minimum ID for downloading page information (Feature.Site, Feature.WIP only)
/// </summary>
public int MinimumId { get; set; }
/// <summary>
/// Maximum ID for downloading page information (Feature.Site, Feature.WIP only)
/// </summary>
public int MaximumId { get; set; }
/// <summary>
/// Quicksearch text for downloading
/// </summary>
public string? QueryString { get; set; }
/// <summary>
/// Directory to save all outputted files to
/// </summary>
public string? OutDir { get; set; }
/// <summary>
/// Use named subfolders for discrete download sets (Feature.Packs only)
/// </summary>
public bool UseSubfolders { get; set; }
/// <summary>
/// Use the last modified page to try to grab all new discs (Feature.Site, Feature.WIP only)
/// </summary>
public bool OnlyNew { get; set; }
/// <summary>
/// Only list the page IDs but don't download
/// </summary>
public bool OnlyList { get; set; }
/// <summary>
/// Force continuing downloads until user cancels or pages run out
/// </summary>
public bool Force { get; set; }
/// <summary>
/// Redump username
/// </summary>
public string? Username { get; set; }
/// <summary>
/// Redump password
/// </summary>
public string? Password { get; set; }
#endregion
#region Private Vars
/// <summary>
/// Current HTTP rc to use
/// </summary>
private readonly RedumpClient _client;
#endregion
/// <summary>
/// Constructor
/// </summary>
public Downloader()
{
_client = new RedumpClient();
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="client">Preconfigured client</param>
public Downloader(RedumpClient client)
{
_client = client;
}
/// <summary>
/// Run the downloads that should go
/// </summary>
/// <returns>True if there was a valid download type, false otherwise</returns>
public async Task<bool> Download()
{
// Login to Redump, if possible
if (!_client.LoggedIn)
await _client.Login(Username ?? string.Empty, Password ?? string.Empty);
switch (Feature)
{
case Feature.Site:
if (OnlyNew)
await Discs.DownloadLastModified(_client, OutDir, Force);
else
await Discs.DownloadSiteRange(_client, OutDir, MinimumId, MaximumId);
break;
case Feature.WIP:
if (OnlyNew)
await WIP.DownloadLastSubmitted(_client, OutDir);
else
await WIP.DownloadWIPRange(_client, OutDir, MinimumId, MaximumId);
break;
case Feature.Packs:
await Packs.DownloadPacks(_client, OutDir, UseSubfolders);
break;
case Feature.User:
if (OnlyList)
await User.ListUser(_client, Username);
else if (OnlyNew)
await User.DownloadUserLastModified(_client, Username, OutDir);
else
await User.DownloadUser(_client, Username, OutDir);
break;
case Feature.Quicksearch:
if (OnlyList)
await Search.ListSearchResults(_client, QueryString);
else
await Search.DownloadSearchResults(_client, QueryString, OutDir);
break;
default:
return false;
}
return true;
}
}
}

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
#if NET40_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
using System.Text.RegularExpressions;
using SabreTools.RedumpLib.Data;
@@ -14,11 +16,14 @@ namespace SabreTools.RedumpLib
/// <param name="info">Information object that should contain normalized values</param>
/// <param name="enableRedumpCompatibility">True to enable Redump compatiblity, false otherwise</param>
/// <returns>List of strings representing each line of an output file, null on error</returns>
public static (List<string>?, string?) FormatOutputData(SubmissionInfo? info, bool enableRedumpCompatibility)
public static List<string>? FormatOutputData(SubmissionInfo? info, bool enableRedumpCompatibility, out string? status)
{
// Check to see if the inputs are valid
if (info == null)
return (null, "Submission information was missing");
{
status = "Submission information was missing";
return null;
}
try
{
@@ -55,8 +60,28 @@ namespace SabreTools.RedumpLib
AddIfExists(output, Template.FullyMatchingIDField, info.FullyMatchedID?.ToString(), 1);
AddIfExists(output, Template.PartiallyMatchingIDsField, info.PartiallyMatchedIDs, 1);
AddIfExists(output, Template.RegionField, info.CommonDiscInfo?.Region.LongName() ?? "SPACE! (CHANGE THIS)", 1);
#if NET20 || NET35
var languages = info.CommonDiscInfo?.Languages ?? [null];
var languageStrings = new List<string>();
foreach (var l in languages)
{
languageStrings.Add(l.LongName() ?? "SILENCE! (CHANGE THIS)");
}
AddIfExists(output, Template.LanguagesField, languageStrings.ToArray(), 1);
var langaugeSelections = info.CommonDiscInfo?.LanguageSelection ?? [];
var languageSelectionStrings = new List<string?>();
foreach (var l in langaugeSelections)
{
languageSelectionStrings.Add(l.LongName());
}
AddIfExists(output, Template.PlaystationLanguageSelectionViaField, languageSelectionStrings.ToArray(), 1);
#else
AddIfExists(output, Template.LanguagesField, (info.CommonDiscInfo?.Languages ?? [null]).Select(l => l.LongName() ?? "SILENCE! (CHANGE THIS)").ToArray(), 1);
AddIfExists(output, Template.PlaystationLanguageSelectionViaField, (info.CommonDiscInfo?.LanguageSelection ?? []).Select(l => l.LongName()).ToArray(), 1);
#endif
AddIfExists(output, Template.DiscSerialField, info.CommonDiscInfo?.Serial, 1);
// All ringcode information goes in an indented area
@@ -229,12 +254,15 @@ namespace SabreTools.RedumpLib
// Dumping Info section
output.Add(""); output.Add("Dumping Info:");
AddIfExists(output, Template.FrontendVersionField, info.DumpingInfo?.FrontendVersion, 1);
AddIfExists(output, Template.DumpingProgramField, info.DumpingInfo?.DumpingProgram, 1);
AddIfExists(output, Template.DumpingDateField, info.DumpingInfo?.DumpingDate, 1);
AddIfExists(output, Template.DumpingParametersField, info.DumpingInfo?.DumpingParameters, 1);
AddIfExists(output, Template.DumpingDriveManufacturer, info.DumpingInfo?.Manufacturer, 1);
AddIfExists(output, Template.DumpingDriveModel, info.DumpingInfo?.Model, 1);
AddIfExists(output, Template.DumpingDriveFirmware, info.DumpingInfo?.Firmware, 1);
AddIfExists(output, Template.ReportedDiscType, info.DumpingInfo?.ReportedDiscType, 1);
AddIfExists(output, Template.C2ErrorCountField, info.DumpingInfo?.C2ErrorsCount, 1);
// Make sure there aren't any instances of two blank lines in a row
string? last = null;
@@ -251,11 +279,13 @@ namespace SabreTools.RedumpLib
}
}
return (output, "Formatting complete!");
status = "Formatting complete!";
return output;
}
catch (Exception ex)
{
return (null, $"Error formatting submission info: {ex}");
status = $"Error formatting submission info: {ex}";
return null;
}
}
@@ -270,13 +300,31 @@ namespace SabreTools.RedumpLib
return;
// Process the comments field
if (info.CommonDiscInfo?.CommentsSpecialFields != null && info.CommonDiscInfo.CommentsSpecialFields?.Any() == true)
if (info.CommonDiscInfo?.CommentsSpecialFields != null && info.CommonDiscInfo.CommentsSpecialFields.Count > 0)
{
// If the field is missing, add an empty one to fill in
if (info.CommonDiscInfo.Comments == null)
info.CommonDiscInfo.Comments = string.Empty;
// Add all special fields before any comments
#if NET20 || NET35
var orderedCommentTags = OrderCommentTags(info.CommonDiscInfo.CommentsSpecialFields);
var commentTagStrings = new List<string>();
foreach (var kvp in orderedCommentTags)
{
if (string.IsNullOrEmpty(kvp.Value))
continue;
string? formatted = FormatSiteTag(kvp);
if (formatted == null)
continue;
commentTagStrings.Add(formatted);
}
info.CommonDiscInfo.Comments = string.Join("\n", commentTagStrings.ToArray())
+ "\n" + info.CommonDiscInfo.Comments;
#else
info.CommonDiscInfo.Comments = string.Join(
"\n", OrderCommentTags(info.CommonDiscInfo.CommentsSpecialFields)
.Where(kvp => !string.IsNullOrEmpty(kvp.Value))
@@ -284,6 +332,7 @@ namespace SabreTools.RedumpLib
.Where(s => !string.IsNullOrEmpty(s))
.ToArray()
) + "\n" + info.CommonDiscInfo.Comments;
#endif
// Normalize newlines
info.CommonDiscInfo.Comments = info.CommonDiscInfo.Comments.Replace("\r\n", "\n");
@@ -296,13 +345,31 @@ namespace SabreTools.RedumpLib
}
// Process the contents field
if (info.CommonDiscInfo?.ContentsSpecialFields != null && info.CommonDiscInfo.ContentsSpecialFields?.Any() == true)
if (info.CommonDiscInfo?.ContentsSpecialFields != null && info.CommonDiscInfo.ContentsSpecialFields.Count > 0)
{
// If the field is missing, add an empty one to fill in
if (info.CommonDiscInfo.Contents == null)
info.CommonDiscInfo.Contents = string.Empty;
// Add all special fields before any contents
#if NET20 || NET35
var orderedContentTags = OrderContentTags(info.CommonDiscInfo.ContentsSpecialFields);
var contentTagStrings = new List<string>();
foreach (var kvp in orderedContentTags)
{
if (string.IsNullOrEmpty(kvp.Value))
continue;
string? formatted = FormatSiteTag(kvp);
if (formatted == null)
continue;
contentTagStrings.Add(formatted);
}
info.CommonDiscInfo.Contents = string.Join("\n", contentTagStrings.ToArray())
+ "\n" + info.CommonDiscInfo.Contents;
#else
info.CommonDiscInfo.Contents = string.Join(
"\n", OrderContentTags(info.CommonDiscInfo.ContentsSpecialFields)
.Where(kvp => !string.IsNullOrEmpty(kvp.Value))
@@ -310,6 +377,7 @@ namespace SabreTools.RedumpLib
.Where(s => !string.IsNullOrEmpty(s))
.ToArray()
) + "\n" + info.CommonDiscInfo.Contents;
#endif
// Normalize newlines
info.CommonDiscInfo.Contents = info.CommonDiscInfo.Contents.Replace("\r\n", "\n");
@@ -357,7 +425,11 @@ namespace SabreTools.RedumpLib
// If the value contains a newline
value = value.Replace("\r\n", "\n");
#if NET20 || NET35
if (value.Contains("\n"))
#else
if (value.Contains('\n'))
#endif
{
output.Add(prefix + key + ":"); output.Add("");
string[] values = value.Split('\n');
@@ -423,7 +495,17 @@ namespace SabreTools.RedumpLib
if (value == null || value.Count == 0)
return;
#if NET20 || NET35
var valueStrings = new List<string>();
foreach (int o in value)
{
valueStrings.Add(o.ToString());
}
AddIfExists(output, key, string.Join(", ", valueStrings.ToArray()), indent);
#else
AddIfExists(output, key, string.Join(", ", value.Select(o => o.ToString()).ToArray()), indent);
#endif
}
/// <summary>
@@ -562,8 +644,6 @@ namespace SabreTools.RedumpLib
if (tags.ContainsKey(SiteCode.BBFCRegistrationNumber))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.BBFCRegistrationNumber, tags[SiteCode.BBFCRegistrationNumber]));
if (tags.ContainsKey(SiteCode.CDProjektID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.CDProjektID, tags[SiteCode.CDProjektID]));
if (tags.ContainsKey(SiteCode.DiscHologramID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.DiscHologramID, tags[SiteCode.DiscHologramID]));
if (tags.ContainsKey(SiteCode.DNASDiscID))
@@ -577,6 +657,8 @@ namespace SabreTools.RedumpLib
if (tags.ContainsKey(SiteCode.VFCCode))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.VFCCode, tags[SiteCode.VFCCode]));
if (tags.ContainsKey(SiteCode.CompatibleOS))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.CompatibleOS, tags[SiteCode.CompatibleOS]));
if (tags.ContainsKey(SiteCode.Genre))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Genre, tags[SiteCode.Genre]));
if (tags.ContainsKey(SiteCode.Series))
@@ -593,6 +675,12 @@ namespace SabreTools.RedumpLib
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.ActivisionID, tags[SiteCode.ActivisionID]));
if (tags.ContainsKey(SiteCode.BandaiID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.BandaiID, tags[SiteCode.BandaiID]));
if (tags.ContainsKey(SiteCode.BethesdaID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.BethesdaID, tags[SiteCode.BethesdaID]));
if (tags.ContainsKey(SiteCode.CDProjektID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.CDProjektID, tags[SiteCode.CDProjektID]));
if (tags.ContainsKey(SiteCode.EidosID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.EidosID, tags[SiteCode.EidosID]));
if (tags.ContainsKey(SiteCode.ElectronicArtsID))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.ElectronicArtsID, tags[SiteCode.ElectronicArtsID]));
if (tags.ContainsKey(SiteCode.FoxInteractiveID))
@@ -649,6 +737,10 @@ namespace SabreTools.RedumpLib
if (tags == null || tags.Count == 0)
return sorted;
// Applications
if (tags.ContainsKey(SiteCode.Applications))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Applications, tags[SiteCode.Applications]));
// Games
if (tags.ContainsKey(SiteCode.Games))
sorted.Add(new KeyValuePair<SiteCode?, string>(SiteCode.Games, tags[SiteCode.Games]));

View File

@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace SabreTools.RedumpLib
@@ -40,8 +39,13 @@ namespace SabreTools.RedumpLib
new StringBuilder(256) :
new StringBuilder(value.Length);
sb.Append(valueSpan.Take(index).ToArray());
HtmlDecode(valueSpan.Skip(index).ToArray(), ref sb);
char[] take = new char[index];
Array.Copy(valueSpan, take, index);
sb.Append(take);
char[] skip = new char[valueSpan.Length - index];
Array.Copy(valueSpan, index, skip, 0, skip.Length);
HtmlDecode(skip, ref sb);
return sb.ToString();
}
@@ -57,7 +61,8 @@ namespace SabreTools.RedumpLib
// We found a '&'. Now look for the next ';' or '&'. The idea is that
// if we find another '&' before finding a ';', then this is not an entity,
// and the next '&' might start a real entity (VSWhidbey 275184)
char[] inputSlice = input.Skip(i + 1).ToArray();
char[] inputSlice = new char[input.Length - (i + 1)];
Array.Copy(input, i + 1, inputSlice, 0, inputSlice.Length);
int semicolonPos = Array.IndexOf(inputSlice, ';');
int ampersandPos = Array.IndexOf(inputSlice, '&');
@@ -81,9 +86,13 @@ namespace SabreTools.RedumpLib
// &#xE5; --> same char in hex
// See http://www.w3.org/TR/REC-html40/charset.html#entities
int offset = inputSlice[1] == 'x' || inputSlice[1] == 'X' ? 2 : 1;
char[] inputSliceNoPrefix = new char[entityLength - offset];
Array.Copy(inputSlice, offset, inputSliceNoPrefix, 0, inputSliceNoPrefix.Length);
bool parsedSuccessfully = inputSlice[1] == 'x' || inputSlice[1] == 'X'
? uint.TryParse(new string(inputSlice.Skip(2).Take(entityLength - 2).ToArray()), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out uint parsedValue)
: uint.TryParse(new string(inputSlice.Skip(1).Take(entityLength - 1).ToArray()), NumberStyles.Integer, CultureInfo.InvariantCulture, out parsedValue);
? uint.TryParse(new string(inputSliceNoPrefix), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out uint parsedValue)
: uint.TryParse(new string(inputSliceNoPrefix), NumberStyles.Integer, CultureInfo.InvariantCulture, out parsedValue);
if (parsedSuccessfully)
{
@@ -112,7 +121,8 @@ namespace SabreTools.RedumpLib
}
else
{
char[] entity = inputSlice.Take(entityLength).ToArray();
char[] entity = new char[entityLength];
Array.Copy(inputSlice, entity, entityLength);
i = entityEndPosition; // already looked at everything until semicolon
char entityChar = HtmlEntities.Lookup(entity);
@@ -414,7 +424,10 @@ namespace SabreTools.RedumpLib
ulong key = BitConverter.ToUInt64(tableData, 0);
char value = (char)BitConverter.ToUInt16(tableData, sizeof(ulong));
dictionary[key] = value;
tableData = tableData.Skip((sizeof(ulong) + sizeof(char))).ToArray();
byte[] tempTableData = new byte[tableData.Length - (sizeof(ulong) + sizeof(char))];
Array.Copy(tableData, (sizeof(ulong) + sizeof(char)), tempTableData, 0, tempTableData.Length);
tableData = tempTableData;
}
return dictionary;
}

View File

@@ -7,7 +7,7 @@
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.3.3</Version>
<Version>1.4.2</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
@@ -22,25 +22,20 @@
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="" />
<None Include="../README.md" Pack="true" PackagePath="" />
</ItemGroup>
<!-- Support for old .NET versions -->
<ItemGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`))">
<PackageReference Include="MinValueTupleBridge" Version="0.2.1" />
<ItemGroup Condition="$(TargetFramework.StartsWith(`net2`))">
<PackageReference Include="Net30.LinqBridge" Version="1.3.0" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR $(TargetFramework.StartsWith(`net40`))">
<PackageReference Include="MinAsyncBridge" Version="0.12.4" />
<PackageReference Include="MinThreadingBridge" Version="0.11.4" />
<PackageReference Include="Net30.LinqBridge" Version="1.3.0" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith(`net4`))">
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="SabreTools.Models" Version="1.3.0" />
<PackageReference Include="SabreTools.Models" Version="1.4.10" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
#if NET40_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
using System.Threading.Tasks;
using SabreTools.RedumpLib.Data;
using SabreTools.RedumpLib.Web;
@@ -79,15 +81,11 @@ namespace SabreTools.RedumpLib
/// <summary>
/// List the disc IDs associated with a given quicksearch query
/// </summary>
/// <param name="wc">RedumpWebClient for making the connection</param>
/// <param name="rc">RedumpClient for making the connection</param>
/// <param name="query">Query string to attempt to search for</param>
/// <param name="filterForwardSlashes">True to filter forward slashes, false otherwise</param>
/// <returns>All disc IDs for the given query, null on error</returns>
#if NETFRAMEWORK
public async static Task<List<int>?> ListSearchResults(RedumpWebClient wc, string? query, bool filterForwardSlashes = true)
#else
public async static Task<List<int>?> ListSearchResults(RedumpHttpClient wc, string? query, bool filterForwardSlashes = true)
#endif
public async static Task<List<int>?> ListSearchResults(RedumpClient rc, string? query, bool filterForwardSlashes = true)
{
// If there is an invalid query
if (string.IsNullOrEmpty(query))
@@ -113,13 +111,7 @@ namespace SabreTools.RedumpLib
int pageNumber = 1;
while (true)
{
#if NET40
List<int> pageIds = await Task.Factory.StartNew(() => wc.CheckSingleSitePage(string.Format(Constants.QuickSearchUrl, query, pageNumber++)));
#elif NETFRAMEWORK
List<int> pageIds = await Task.Run(() => wc.CheckSingleSitePage(string.Format(Constants.QuickSearchUrl, query, pageNumber++)));
#else
List<int> pageIds = await wc.CheckSingleSitePage(string.Format(Constants.QuickSearchUrl, query, pageNumber++));
#endif
List<int> pageIds = await rc.CheckSingleSitePage(string.Format(Constants.QuickSearchUrl, query, pageNumber++));
ids.AddRange(pageIds);
if (pageIds.Count <= 1)
break;
@@ -137,102 +129,84 @@ namespace SabreTools.RedumpLib
/// <summary>
/// Validate a single track against Redump, if possible
/// </summary>
/// <param name="wc">RedumpWebClient for making the connection</param>
/// <param name="rc">RedumpClient for making the connection</param>
/// <param name="info">Existing SubmissionInfo object to fill</param>
/// <param name="sha1">SHA-1 hash to check against</param>
/// <returns>True if the track was found, false otherwise; List of found values, if possible</returns>
#if NETFRAMEWORK
public async static Task<(bool, List<int>?, string?)> ValidateSingleTrack(RedumpWebClient wc, SubmissionInfo info, string? sha1)
#else
public async static Task<(bool, List<int>?, string?)> ValidateSingleTrack(RedumpHttpClient wc, SubmissionInfo info, string? sha1)
#endif
/// <returns>List of found values, if possible</returns>
public async static Task<List<int>?> ValidateSingleTrack(RedumpClient rc, SubmissionInfo info, string? sha1)
{
// Get all matching IDs for the track
var newIds = await ListSearchResults(wc, sha1);
var newIds = await ListSearchResults(rc, sha1);
// If we got null back, there was an error
if (newIds == null)
return (false, null, "There was an unknown error retrieving information from Redump");
return null;
// If no IDs match, just return
if (!newIds.Any())
return (false, null, $"There were no matching IDs for track with SHA-1 of '{sha1}'");
if (newIds.Count == 0)
return null;
// Join the list of found IDs to the existing list, if possible
if (info.PartiallyMatchedIDs != null && info.PartiallyMatchedIDs.Any())
if (info.PartiallyMatchedIDs != null && info.PartiallyMatchedIDs.Count > 0)
info.PartiallyMatchedIDs.AddRange(newIds);
else
info.PartiallyMatchedIDs = newIds;
return (true, newIds, $"There were matching ID(s) found for track with SHA-1 of '{sha1}'");
return newIds;
}
/// <summary>
/// Validate a universal hash against Redump, if possible
/// </summary>
/// <param name="wc">RedumpWebClient for making the connection</param>
/// <param name="rc">RedumpClient for making the connection</param>
/// <param name="info">Existing SubmissionInfo object to fill</param>
/// <param name="resultProgress">Optional result progress callback</param>
/// <returns>True if the track was found, false otherwise; List of found values, if possible</returns>
#if NETFRAMEWORK
public async static Task<(bool, List<int>?, string?)> ValidateUniversalHash(RedumpWebClient wc, SubmissionInfo info)
#else
public async static Task<(bool, List<int>?, string?)> ValidateUniversalHash(RedumpHttpClient wc, SubmissionInfo info)
#endif
/// <returns>List of found values, if possible</returns>
public async static Task<List<int>?> ValidateUniversalHash(RedumpClient rc, SubmissionInfo info)
{
// If we don't have special fields
if (info.CommonDiscInfo?.CommentsSpecialFields == null)
return (false, null, "Universal hash was missing");
return null;
// If we don't have a universal hash
string? universalHash = info.CommonDiscInfo.CommentsSpecialFields[SiteCode.UniversalHash];
if (string.IsNullOrEmpty(universalHash))
return (false, null, "Universal hash was missing");
return null;
// Format the universal hash for finding within the comments
string universalHashQuery = $"{universalHash.Substring(0, universalHash.Length - 1)}/comments/only";
// Get all matching IDs for the hash
var newIds = await ListSearchResults(wc, universalHashQuery, filterForwardSlashes: false);
var newIds = await ListSearchResults(rc, universalHashQuery, filterForwardSlashes: false);
// If we got null back, there was an error
if (newIds == null)
return (false, null, "There was an unknown error retrieving information from Redump");
return null;
// If no IDs match, just return
if (!newIds.Any())
return (false, null, $"There were no matching IDs for universal hash of '{universalHash}'");
if (newIds.Count == 0)
return null;
// Join the list of found IDs to the existing list, if possible
if (info.PartiallyMatchedIDs != null && info.PartiallyMatchedIDs.Any())
if (info.PartiallyMatchedIDs != null && info.PartiallyMatchedIDs.Count > 0)
info.PartiallyMatchedIDs.AddRange(newIds);
else
info.PartiallyMatchedIDs = newIds;
return (true, newIds, $"There were matching ID(s) found for universal hash of '{universalHash}'");
return newIds;
}
/// <summary>
/// Validate that the current track count and remote track count match
/// </summary>
/// <param name="wc">RedumpWebClient for making the connection</param>
/// <param name="rc">RedumpClient for making the connection</param>
/// <param name="id">Redump disc ID to retrieve</param>
/// <param name="localCount">Local count of tracks for the current disc</param>
/// <returns>True if the track count matches, false otherwise</returns>
#if NETFRAMEWORK
public async static Task<bool> ValidateTrackCount(RedumpWebClient wc, int id, int localCount)
#else
public async static Task<bool> ValidateTrackCount(RedumpHttpClient wc, int id, int localCount)
#endif
public async static Task<bool> ValidateTrackCount(RedumpClient rc, int id, int localCount)
{
// If we can't pull the remote data, we can't match
#if NET40
string? discData = await Task.Factory.StartNew(() => wc.DownloadSingleSiteID(id));
#elif NETFRAMEWORK
string? discData = await Task.Run(() => wc.DownloadSingleSiteID(id));
#else
string? discData = await wc.DownloadSingleSiteID(id);
#endif
string? discData = await rc.DownloadSingleSiteID(id);
if (string.IsNullOrEmpty(discData))
return false;

View File

@@ -0,0 +1,40 @@
using System;
using System.Net;
#pragma warning disable SYSLIB0014 // 'WebClient.WebClient()' is obsolete
namespace SabreTools.RedumpLib.Web
{
internal class CookieWebClient : WebClient
{
// https://stackoverflow.com/questions/1777221/using-cookiecontainer-with-webclient-class
private readonly CookieContainer _container = new();
/// <summary>
/// Get the last downloaded filename, if possible
/// </summary>
public string? GetLastFilename()
{
// If the response headers are null or empty
if (ResponseHeaders == null || ResponseHeaders.Count == 0)
return null;
// If we don't have the response header we care about
string? headerValue = ResponseHeaders.Get("Content-Disposition");
if (string.IsNullOrEmpty(headerValue))
return null;
// Extract the filename from the value
return headerValue.Substring(headerValue.IndexOf("filename=") + 9).Replace("\"", "");
}
/// <inheritdoc/>
protected override WebRequest GetWebRequest(Uri address)
{
WebRequest request = base.GetWebRequest(address);
if (request is HttpWebRequest webRequest)
webRequest.CookieContainer = _container;
return request;
}
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.Threading;
namespace SabreTools.RedumpLib.Web
{
/// <summary>
/// Helper class for delaying
/// </summary>
internal static class DelayHelper
{
/// <summary>
/// Delay a random amount of time up to 5 seconds
/// </summary>
public static void DelayRandom()
{
var r = new Random();
int delay = r.Next(0, 50);
Thread.Sleep(delay * 100);
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Threading.Tasks;
using SabreTools.RedumpLib.Data;
namespace SabreTools.RedumpLib.Web
{
/// <summary>
/// Contains logic for dealing with disc pages
/// </summary>
public static class Discs
{
/// <summary>
/// Download the last modified disc pages, until first failure
/// </summary>
/// <param name="rc">RedumpClient for connectivity</param>
/// <param name="outDir">Output directory to save data to</param>
/// <param name="force">Force continuation of download</param>
public static async Task<bool> DownloadLastModified(RedumpClient rc, string? outDir, bool force)
{
// Keep getting last modified pages until there are none left
int pageNumber = 1;
while (true)
{
if (!await rc.CheckSingleSitePage(string.Format(Constants.LastModifiedUrl, pageNumber++), outDir, !force))
break;
}
return true;
}
/// <summary>
/// Download the specified range of site disc pages
/// </summary>
/// <param name="rc">RedumpClient for connectivity</param>
/// <param name="outDir">Output directory to save data to</param>
/// <param name="minId">Starting ID for the range</param>
/// <param name="maxId">Ending ID for the range (inclusive)</param>
public static async Task<bool> DownloadSiteRange(RedumpClient rc, string? outDir, int minId = 0, int maxId = 0)
{
if (!rc.LoggedIn)
{
Console.WriteLine("Site download functionality is only available to Redump members");
return false;
}
for (int id = minId; id <= maxId; id++)
{
if (await rc.DownloadSingleSiteID(id, outDir, true))
DelayHelper.DelayRandom(); // Intentional sleep here so we don't flood the server
}
return true;
}
}
}

View File

@@ -0,0 +1,151 @@
using System;
#if NET20 || NET35
using System.Collections.Generic;
#endif
#if NET40_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
using System.Threading.Tasks;
using SabreTools.RedumpLib.Data;
namespace SabreTools.RedumpLib.Web
{
/// <summary>
/// Contains logic for dealing with packs
/// </summary>
internal static class Packs
{
/// <summary>
/// Download premade packs
/// </summary>
/// <param name="rc">RedumpClient for connectivity</param>
/// <param name="outDir">Output directory to save data to</param>
/// <param name="useSubfolders">True to use named subfolders to store downloads, false to store directly in the output directory</param>
public static async Task<bool> DownloadPacks(RedumpClient rc, string? outDir, bool useSubfolders)
{
#if NET20 || NET35
var systems = new List<RedumpSystem?>();
foreach (RedumpSystem s in Enum.GetValues(typeof(RedumpSystem)))
{
systems.Add(new Nullable<RedumpSystem>(s));
}
#elif NET40_OR_GREATER || NETCOREAPP3_1
var systems = Enum.GetValues(typeof(RedumpSystem))
.OfType<RedumpSystem>()
.Select(s => new Nullable<RedumpSystem>(s));
#else
var systems = Enum.GetValues<RedumpSystem>().Select(s => new RedumpSystem?(s));
#endif
#if NET20 || NET35
var filtered = new List<RedumpSystem?>();
foreach (var s in systems)
{
if (s.HasCues())
filtered.Add(s);
}
await rc.DownloadPacks(Constants.PackCuesUrl, filtered.ToArray(), "CUEs", outDir, useSubfolders ? "cue" : null);
filtered = new List<RedumpSystem?>();
foreach (var s in systems)
{
if (s.HasDat())
filtered.Add(s);
}
await rc.DownloadPacks(Constants.PackDatfileUrl, filtered.ToArray(), "DATs", outDir, useSubfolders ? "dat" : null);
filtered = new List<RedumpSystem?>();
foreach (var s in systems)
{
if (s.HasCues())
filtered.Add(s);
}
filtered = new List<RedumpSystem?>();
foreach (var s in systems)
{
if (s.HasDkeys())
filtered.Add(s);
}
await rc.DownloadPacks(Constants.PackDkeysUrl, filtered.ToArray(), "Decrypted KEYS", outDir, useSubfolders ? "dkey" : null);
filtered = new List<RedumpSystem?>();
foreach (var s in systems)
{
if (s.HasGdi())
filtered.Add(s);
}
await rc.DownloadPacks(Constants.PackGdiUrl, filtered.ToArray(), "GDIs", outDir, useSubfolders ? "gdi" : null);
filtered = new List<RedumpSystem?>();
foreach (var s in systems)
{
if (s.HasKeys())
filtered.Add(s);
}
await rc.DownloadPacks(Constants.PackKeysUrl, filtered.ToArray(), "KEYS", outDir, useSubfolders ? "keys" : null);
filtered = new List<RedumpSystem?>();
foreach (var s in systems)
{
if (s.HasLsd())
filtered.Add(s);
}
await rc.DownloadPacks(Constants.PackLsdUrl, filtered.ToArray(), "LSD", outDir, useSubfolders ? "lsd" : null);
filtered = new List<RedumpSystem?>();
foreach (var s in systems)
{
if (s.HasSbi())
filtered.Add(s);
}
await rc.DownloadPacks(Constants.PackSbiUrl, filtered.ToArray(), "SBIs", outDir, useSubfolders ? "sbi" : null);
#else
await rc.DownloadPacks(Constants.PackCuesUrl, systems.Where(s => s.HasCues()).ToArray(), "CUEs", outDir, useSubfolders ? "cue" : null);
await rc.DownloadPacks(Constants.PackDatfileUrl, systems.Where(s => s.HasDat()).ToArray(), "DATs", outDir, useSubfolders ? "dat" : null);
await rc.DownloadPacks(Constants.PackDkeysUrl, systems.Where(s => s.HasDkeys()).ToArray(), "Decrypted KEYS", outDir, useSubfolders ? "dkey" : null);
await rc.DownloadPacks(Constants.PackGdiUrl, systems.Where(s => s.HasGdi()).ToArray(), "GDIs", outDir, useSubfolders ? "gdi" : null);
await rc.DownloadPacks(Constants.PackKeysUrl, systems.Where(s => s.HasKeys()).ToArray(), "KEYS", outDir, useSubfolders ? "keys" : null);
await rc.DownloadPacks(Constants.PackLsdUrl, systems.Where(s => s.HasLsd()).ToArray(), "LSD", outDir, useSubfolders ? "lsd" : null);
await rc.DownloadPacks(Constants.PackSbiUrl, systems.Where(s => s.HasSbi()).ToArray(), "SBIs", outDir, useSubfolders ? "sbi" : null);
#endif
return true;
}
/// <summary>
/// Download premade packs for an individual system
/// </summary>
/// <param name="rc">RedumpClient for connectivity</param>
/// <param name="system">RedumpSystem to get all possible packs for</param>
/// <param name="outDir">Output directory to save data to</param>
/// <param name="useSubfolders">True to use named subfolders to store downloads, false to store directly in the output directory</param>
public static async Task<bool> DownloadPacksForSystem(RedumpClient rc, RedumpSystem? system, string? outDir, bool useSubfolders)
{
var systemAsArray = new RedumpSystem?[] { system };
if (system.HasCues())
await rc.DownloadPacks(Constants.PackCuesUrl, systemAsArray, "CUEs", outDir, useSubfolders ? "cue" : null);
if (system.HasDat())
await rc.DownloadPacks(Constants.PackDatfileUrl, systemAsArray, "DATs", outDir, useSubfolders ? "dat" : null);
if (system.HasDkeys())
await rc.DownloadPacks(Constants.PackDkeysUrl, systemAsArray, "Decrypted KEYS", outDir, useSubfolders ? "dkey" : null);
if (system.HasGdi())
await rc.DownloadPacks(Constants.PackGdiUrl, systemAsArray, "GDIs", outDir, useSubfolders ? "gdi" : null);
if (system.HasKeys())
await rc.DownloadPacks(Constants.PackKeysUrl, systemAsArray, "KEYS", outDir, useSubfolders ? "keys" : null);
if (system.HasLsd())
await rc.DownloadPacks(Constants.PackLsdUrl, systemAsArray, "LSD", outDir, useSubfolders ? "lsd" : null);
if (system.HasSbi())
await rc.DownloadPacks(Constants.PackSbiUrl, systemAsArray, "SBIs", outDir, useSubfolders ? "sbi" : null);
return true;
}
}
}

View File

@@ -1,20 +1,23 @@
#if !NETFRAMEWORK
using System;
using System.Collections.Generic;
using System.IO;
#if NET40_OR_GREATER || NETCOREAPP
using System.Linq;
#endif
using System.Net;
#if NETCOREAPP
using System.Net.Http;
using System.Net.Http.Headers;
#endif
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using SabreTools.RedumpLib.Data;
namespace SabreTools.RedumpLib.Web
{
public class RedumpHttpClient : HttpClient
public class RedumpClient
{
#region Properties
@@ -28,14 +31,44 @@ namespace SabreTools.RedumpLib.Web
/// </summary>
public bool IsStaff { get; private set; } = false;
/// <summary>
/// Maximum retry count for any operation
/// </summary>
public int RetryCount { get; private set; } = 3;
/// <summary>
/// Internal client for interaction
/// </summary>
#if NETFRAMEWORK
private CookieWebClient _internalClient;
#else
private HttpClient _internalClient;
#endif
#endregion
/// <summary>
/// Constructor
/// </summary>
public RedumpHttpClient()
: base(new HttpClientHandler { UseCookies = true })
public RedumpClient()
{
#if NETFRAMEWORK
_internalClient = new CookieWebClient();
#else
_internalClient = new HttpClient(new HttpClientHandler { UseCookies = true });
#endif
}
/// <summary>
/// Constructor
/// </summary>
public RedumpClient(int retryCount) : this()
{
// Ensure there are a positive number of retries
if (retryCount <= 0)
retryCount = 3;
RetryCount = retryCount;
}
#region Credentials
@@ -43,22 +76,22 @@ namespace SabreTools.RedumpLib.Web
/// <summary>
/// Validate supplied credentials
/// </summary>
public async static Task<(bool?, string?)> ValidateCredentials(string username, string password)
public async static Task<bool?> ValidateCredentials(string username, string password)
{
// If options are invalid or we're missing something key, just return
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
return (false, null);
return false;
// Try logging in with the supplied credentials otherwise
using RedumpHttpClient httpClient = new();
var redumpClient = new RedumpClient();
bool? loggedIn = await httpClient.Login(username, password);
bool? loggedIn = await redumpClient.Login(username, password);
if (loggedIn == true)
return (true, "Redump username and password accepted!");
return true;
else if (loggedIn == false)
return (false, "Redump username and password denied!");
return false;
else
return (null, "An error occurred validating your credentials!");
return null;
}
/// <summary>
@@ -86,7 +119,11 @@ namespace SabreTools.RedumpLib.Web
}
// HTTP encode the password
#if NET20 || NET35 || NET40
password = Uri.EscapeUriString(password);
#else
password = WebUtility.UrlEncode(password);
#endif
// Attempt to login up to 3 times
for (int i = 0; i < 3; i++)
@@ -94,25 +131,36 @@ namespace SabreTools.RedumpLib.Web
try
{
// Get the current token from the login page
var loginPage = await GetStringAsync(Constants.LoginUrl);
string token = Constants.TokenRegex.Match(loginPage).Groups[1].Value;
var loginPage = await DownloadStringWithRetries(Constants.LoginUrl);
string token = Constants.TokenRegex.Match(loginPage ?? string.Empty).Groups[1].Value;
#if NETFRAMEWORK
// Construct the login request
_internalClient.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded";
_internalClient.Encoding = Encoding.UTF8;
// Send the login request and get the result
string? responseContent = _internalClient.UploadString(Constants.LoginUrl, $"form_sent=1&redirect_url=&csrf_token={token}&req_username={username}&req_password={password}&save_pass=0");
#else
// Construct the login request
var postContent = new StringContent($"form_sent=1&redirect_url=&csrf_token={token}&req_username={username}&req_password={password}&save_pass=0", Encoding.UTF8);
postContent.Headers.ContentType = MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded");
// Send the login request and get the result
var response = await PostAsync(Constants.LoginUrl, postContent);
var response = await _internalClient.PostAsync(Constants.LoginUrl, postContent);
string? responseContent = null;
if (response?.Content != null)
responseContent = await response.Content.ReadAsStringAsync();
#endif
// An empty response indicates an error
if (string.IsNullOrEmpty(responseContent))
{
Console.WriteLine($"An error occurred while trying to log in on attempt {i}: No response");
continue;
}
// Explcit confirmation the login was wrong
if (responseContent.Contains("Incorrect username and/or password."))
{
Console.WriteLine("Invalid credentials entered, continuing without logging in...");
@@ -152,8 +200,8 @@ namespace SabreTools.RedumpLib.Web
{
List<int> ids = [];
// Try up to 3 times to retrieve the data
string? dumpsPage = await DownloadString(url, retries: 3);
// Try to retrieve the data
string? dumpsPage = await DownloadStringWithRetries(url);
// If we have no dumps left
if (dumpsPage == null || dumpsPage.Contains("No discs found."))
@@ -171,7 +219,11 @@ namespace SabreTools.RedumpLib.Web
// Otherwise, traverse each dump on the page
var matches = Constants.DiscRegex.Matches(dumpsPage);
#if NET20 || NET35
foreach (Match? match in matches)
#else
foreach (Match? match in matches.Cast<Match?>())
#endif
{
if (match == null)
continue;
@@ -200,8 +252,8 @@ namespace SabreTools.RedumpLib.Web
/// <returns>True if the page could be downloaded, false otherwise</returns>
public async Task<bool> CheckSingleSitePage(string url, string? outDir, bool failOnSingle)
{
// Try up to 3 times to retrieve the data
string? dumpsPage = await DownloadString(url, retries: 3);
// Try to retrieve the data
string? dumpsPage = await DownloadStringWithRetries(url);
// If we have no dumps left
if (dumpsPage == null || dumpsPage.Contains("No discs found."))
@@ -223,7 +275,11 @@ namespace SabreTools.RedumpLib.Web
// Otherwise, traverse each dump on the page
var matches = Constants.DiscRegex.Matches(dumpsPage);
#if NET20 || NET35
foreach (Match? match in matches)
#else
foreach (Match? match in matches.Cast<Match?>())
#endif
{
if (match == null)
continue;
@@ -256,8 +312,8 @@ namespace SabreTools.RedumpLib.Web
{
List<int> ids = [];
// Try up to 3 times to retrieve the data
string? dumpsPage = await DownloadString(url, retries: 3);
// Try to retrieve the data
string? dumpsPage = await DownloadStringWithRetries(url);
// If we have no dumps left
if (dumpsPage == null || dumpsPage.Contains("No discs found."))
@@ -265,7 +321,11 @@ namespace SabreTools.RedumpLib.Web
// Otherwise, traverse each dump on the page
var matches = Constants.NewDiscRegex.Matches(dumpsPage);
#if NET20 || NET35
foreach (Match? match in matches)
#else
foreach (Match? match in matches.Cast<Match?>())
#endif
{
if (match == null)
continue;
@@ -294,8 +354,8 @@ namespace SabreTools.RedumpLib.Web
/// <returns>True if the page could be downloaded, false otherwise</returns>
public async Task<bool> CheckSingleWIPPage(string url, string? outDir, bool failOnSingle)
{
// Try up to 3 times to retrieve the data
string? dumpsPage = await DownloadString(url, retries: 3);
// Try to retrieve the data
string? dumpsPage = await DownloadStringWithRetries(url);
// If we have no dumps left
if (dumpsPage == null || dumpsPage.Contains("No discs found."))
@@ -303,7 +363,11 @@ namespace SabreTools.RedumpLib.Web
// Otherwise, traverse each dump on the page
var matches = Constants.NewDiscRegex.Matches(dumpsPage);
#if NET20 || NET35
foreach (Match? match in matches)
#else
foreach (Match? match in matches.Cast<Match?>())
#endif
{
if (match == null)
continue;
@@ -341,7 +405,13 @@ namespace SabreTools.RedumpLib.Web
{
try
{
return await GetByteArrayAsync(string.Format(url, system.ShortName()));
#if NET40
return await Task.Factory.StartNew(() => _internalClient.DownloadData(string.Format(url, system.ShortName())));
#elif NETFRAMEWORK
return await Task.Run(() => _internalClient.DownloadData(string.Format(url, system.ShortName())));
#else
return await _internalClient.GetByteArrayAsync(string.Format(url, system.ShortName()));
#endif
}
catch (Exception ex)
{
@@ -391,9 +461,9 @@ namespace SabreTools.RedumpLib.Web
Console.WriteLine($"Processing ID: {paddedId}");
try
{
// Try up to 3 times to retrieve the data
// Try to retrieve the data
string discPageUri = string.Format(Constants.DiscPageUrl, +id);
string? discPage = await DownloadString(discPageUri, retries: 3);
string? discPage = await DownloadStringWithRetries(discPageUri);
if (discPage == null || discPage.Contains($"Disc with ID \"{id}\" doesn't exist"))
{
@@ -429,9 +499,9 @@ namespace SabreTools.RedumpLib.Web
Console.WriteLine($"Processing ID: {paddedId}");
try
{
// Try up to 3 times to retrieve the data
// Try to retrieve the data
string discPageUri = string.Format(Constants.DiscPageUrl, +id);
string? discPage = await DownloadString(discPageUri, retries: 3);
string? discPage = await DownloadStringWithRetries(discPageUri);
if (discPage == null || discPage.Contains($"Disc with ID \"{id}\" doesn't exist"))
{
@@ -553,9 +623,9 @@ namespace SabreTools.RedumpLib.Web
Console.WriteLine($"Processing ID: {paddedId}");
try
{
// Try up to 3 times to retrieve the data
// Try to retrieve the data
string discPageUri = string.Format(Constants.WipDiscPageUrl, +id);
string? discPage = await DownloadString(discPageUri, retries: 3);
string? discPage = await DownloadStringWithRetries(discPageUri);
if (discPage == null || discPage.Contains($"WIP disc with ID \"{id}\" doesn't exist"))
{
@@ -591,9 +661,9 @@ namespace SabreTools.RedumpLib.Web
Console.WriteLine($"Processing ID: {paddedId}");
try
{
// Try up to 3 times to retrieve the data
// Try to retrieve the data
string discPageUri = string.Format(Constants.WipDiscPageUrl, +id);
string? discPage = await DownloadString(discPageUri, retries: 3);
string? discPage = await DownloadStringWithRetries(discPageUri);
if (discPage == null || discPage.Contains($"WIP disc with ID \"{id}\" doesn't exist"))
{
@@ -687,7 +757,7 @@ namespace SabreTools.RedumpLib.Web
if (string.IsNullOrEmpty(longName))
continue;
Console.Write($"\r{longName}{new string(' ', Console.BufferWidth - longName.Length - 1)}");
Console.Write($"\r{longName}{new string(' ', Console.BufferWidth - longName!.Length - 1)}");
byte[]? pack = await DownloadSinglePack(url, system);
if (pack != null)
packsDictionary.Add(system.Value, pack);
@@ -725,7 +795,7 @@ namespace SabreTools.RedumpLib.Web
if (string.IsNullOrEmpty(longName))
continue;
Console.Write($"\r{longName}{new string(' ', Console.BufferWidth - longName.Length - 1)}");
Console.Write($"\r{longName}{new string(' ', Console.BufferWidth - longName!.Length - 1)}");
await DownloadSinglePack(url, system, outDir, subfolder);
}
@@ -742,8 +812,15 @@ namespace SabreTools.RedumpLib.Web
/// <returns>The remote filename from the URI, null on error</returns>
private async Task<string?> DownloadFile(string uri, string fileName)
{
#if NET40
await Task.Factory.StartNew(() => { _internalClient.DownloadFile(uri, fileName); return true; });
return _internalClient.GetLastFilename();
#elif NETFRAMEWORK
await Task.Run(() => _internalClient.DownloadFile(uri, fileName));
return _internalClient.GetLastFilename();
#else
// Make the call to get the file
var response = await GetAsync(uri);
var response = await _internalClient.GetAsync(uri);
if (response?.Content?.Headers == null || !response.IsSuccessStatusCode)
{
Console.WriteLine($"Could not download {uri}");
@@ -758,27 +835,36 @@ namespace SabreTools.RedumpLib.Web
}
return response.Content.Headers.ContentDisposition?.FileName?.Replace("\"", "");
#endif
}
/// <summary>
/// Download from a URI to a string
/// </summary>
/// <param name="uri">Remote URI to retrieve</param>
/// <param name="retries">Number of times to retry on error</param>
/// <returns>String from the URI, null on error</returns>
private async Task<string?> DownloadString(string uri, int retries = 3)
private async Task<string?> DownloadStringWithRetries(string uri)
{
// Only retry a positive number of times
if (retries <= 0)
if (RetryCount <= 0)
return null;
for (int i = 0; i < retries; i++)
for (int i = 0; i < RetryCount; i++)
{
try
{
return await GetStringAsync(uri);
#if NET40
return await Task.Factory.StartNew(() => _internalClient.DownloadString(uri));
#elif NETFRAMEWORK
return await Task.Run(() => _internalClient.DownloadString(uri));
#else
return await _internalClient.GetStringAsync(uri);
#endif
}
catch { }
// Sleep for 100ms if the last attempt failed
Thread.Sleep(100);
}
return null;
@@ -818,6 +904,4 @@ namespace SabreTools.RedumpLib.Web
#endregion
}
}
#endif
}

View File

@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using SabreTools.RedumpLib.Data;
namespace SabreTools.RedumpLib.Web
{
/// <summary>
/// Contains logic for dealing with searches
/// </summary>
internal static class Search
{
/// <summary>
/// List the disc IDs associated with a given quicksearch query
/// </summary>
/// <param name="rc">RedumpClient for connectivity</param>
/// <param name="query">Query string to attempt to search for</param>
/// <returns>All disc IDs for the given query, null on error</returns>
public static async Task<List<int>?> ListSearchResults(RedumpClient rc, string? query)
{
// If the query is invalid
if (string.IsNullOrEmpty(query))
return null;
List<int> ids = [];
// Strip quotes
query = query!.Trim('"', '\'');
// Special characters become dashes
query = query.Replace(' ', '-');
query = query.Replace('/', '-');
query = query.Replace('\\', '/');
// Lowercase is defined per language
query = query.ToLowerInvariant();
// Keep getting quicksearch pages until there are none left
try
{
int pageNumber = 1;
while (true)
{
List<int> pageIds = await rc.CheckSingleSitePage(string.Format(Constants.QuickSearchUrl, query, pageNumber++));
ids.AddRange(pageIds);
if (pageIds.Count <= 1)
break;
}
}
catch (Exception ex)
{
Console.WriteLine($"An exception occurred while trying to log in: {ex}");
return null;
}
return ids;
}
/// <summary>
/// Download the disc pages associated with a given quicksearch query
/// </summary>
/// <param name="rc">RedumpClient for connectivity</param>
/// <param name="query">Query string to attempt to search for</param>
/// <param name="outDir">Output directory to save data to</param>
public static async Task<bool> DownloadSearchResults(RedumpClient rc, string? query, string? outDir)
{
// If the query is invalid
if (string.IsNullOrEmpty(query))
return false;
// Strip quotes
query = query!.Trim('"', '\'');
// Special characters become dashes
query = query.Replace(' ', '-');
query = query.Replace('/', '-');
query = query.Replace('\\', '/');
// Lowercase is defined per language
query = query.ToLowerInvariant();
// Keep getting quicksearch pages until there are none left
int pageNumber = 1;
while (true)
{
if (!await rc.CheckSingleSitePage(string.Format(Constants.QuickSearchUrl, query, pageNumber++), outDir, false))
break;
}
return true;
}
}
}

View File

@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using SabreTools.RedumpLib.Data;
namespace SabreTools.RedumpLib.Web
{
/// <summary>
/// Contains logic for dealing with users
/// </summary>
public static class User
{
/// <summary>
/// Download the disc pages associated with the given user
/// </summary>
/// <param name="rc">RedumpClient for connectivity</param>
/// <param name="username">Username to check discs for</param>
/// <param name="outDir">Output directory to save data to</param>
public static async Task<bool> DownloadUser(RedumpClient rc, string? username, string? outDir)
{
if (!rc.LoggedIn || string.IsNullOrEmpty(username))
{
Console.WriteLine("User download functionality is only available to Redump members");
return false;
}
// Keep getting user pages until there are none left
int pageNumber = 1;
while (true)
{
if (!await rc.CheckSingleSitePage(string.Format(Constants.UserDumpsUrl, username, pageNumber++), outDir, false))
break;
}
return true;
}
/// <summary>
/// Download the last modified disc pages associated with the given user, until first failure
/// </summary>
/// <param name="rc">RedumpClient for connectivity</param>
/// <param name="username">Username to check discs for</param>
/// <param name="outDir">Output directory to save data to</param>
public static async Task<bool> DownloadUserLastModified(RedumpClient rc, string? username, string? outDir)
{
if (!rc.LoggedIn || string.IsNullOrEmpty(username))
{
Console.WriteLine("User download functionality is only available to Redump members");
return false;
}
// Keep getting last modified user pages until there are none left
int pageNumber = 1;
while (true)
{
if (!await rc.CheckSingleSitePage(string.Format(Constants.UserDumpsLastModifiedUrl, username, pageNumber++), outDir, true))
break;
}
return true;
}
/// <summary>
/// List the disc IDs associated with the given user
/// </summary>
/// <param name="rc">RedumpClient for connectivity</param>
/// <param name="username">Username to check discs for</param>
/// <returns>All disc IDs for the given user, null on error</returns>
public static async Task<List<int>?> ListUser(RedumpClient rc, string? username)
{
List<int> ids = [];
if (!rc.LoggedIn || string.IsNullOrEmpty(username))
{
Console.WriteLine("User download functionality is only available to Redump members");
return ids;
}
// Keep getting user pages until there are none left
try
{
int pageNumber = 1;
while (true)
{
List<int> pageIds = await rc.CheckSingleSitePage(string.Format(Constants.UserDumpsUrl, username, pageNumber++));
ids.AddRange(pageIds);
if (pageIds.Count <= 1)
break;
}
}
catch (Exception ex)
{
Console.WriteLine($"An exception occurred while trying to log in: {ex}");
return null;
}
return ids;
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Threading.Tasks;
using SabreTools.RedumpLib.Data;
namespace SabreTools.RedumpLib.Web
{
/// <summary>
/// Contains logic for dealing with WIP queue
/// </summary>
public static class WIP
{
/// <summary>
/// Download the last submitted WIP disc pages
/// </summary>
/// <param name="rc">RedumpClient for connectivity</param>
/// <param name="outDir">Output directory to save data to</param>
public static async Task<bool> DownloadLastSubmitted(RedumpClient rc, string? outDir)
{
return await rc.CheckSingleWIPPage(Constants.WipDumpsUrl, outDir, false);
}
/// <summary>
/// Download the specified range of WIP disc pages
/// </summary>
/// <param name="rc">RedumpClient for connectivity</param>
/// <param name="outDir">Output directory to save data to</param>
/// <param name="minId">Starting ID for the range</param>
/// <param name="maxId">Ending ID for the range (inclusive)</param>
public static async Task<bool> DownloadWIPRange(RedumpClient rc, string? outDir, int minId = 0, int maxId = 0)
{
if (!rc.LoggedIn || !rc.IsStaff)
{
Console.WriteLine("WIP download functionality is only available to Redump moderators");
return false;
}
for (int id = minId; id <= maxId; id++)
{
if (await rc.DownloadSingleWIPID(id, outDir, true))
DelayHelper.DelayRandom(); // Intentional sleep here so we don't flood the server
}
return true;
}
}
}

View File

@@ -1,831 +0,0 @@
#if NETFRAMEWORK
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using SabreTools.RedumpLib.Data;
namespace SabreTools.RedumpLib.Web
{
// https://stackoverflow.com/questions/1777221/using-cookiecontainer-with-webclient-class
public class RedumpWebClient : WebClient
{
private readonly CookieContainer m_container = new();
/// <summary>
/// Determines if user is logged into Redump
/// </summary>
public bool LoggedIn { get; private set; } = false;
/// <summary>
/// Determines if the user is a staff member
/// </summary>
public bool IsStaff { get; private set; } = false;
/// <summary>
/// Get the last downloaded filename, if possible
/// </summary>
/// <returns></returns>
public string? GetLastFilename()
{
// If the response headers are null or empty
if (ResponseHeaders == null || ResponseHeaders.Count == 0)
return null;
// If we don't have the response header we care about
string headerValue = ResponseHeaders.Get("Content-Disposition");
if (string.IsNullOrEmpty(headerValue))
return null;
// Extract the filename from the value
return headerValue.Substring(headerValue.IndexOf("filename=") + 9).Replace("\"", "");
}
/// <inheritdoc/>
protected override WebRequest GetWebRequest(Uri address)
{
WebRequest request = base.GetWebRequest(address);
if (request is HttpWebRequest webRequest)
webRequest.CookieContainer = m_container;
return request;
}
/// <summary>
/// Validate supplied credentials
/// </summary>
public static (bool?, string?) ValidateCredentials(string username, string password)
{
// If options are invalid or we're missing something key, just return
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
return (false, null);
// Try logging in with the supplied credentials otherwise
using RedumpWebClient wc = new();
bool? loggedIn = wc.Login(username, password);
if (loggedIn == true)
return (true, "Redump username and password accepted!");
else if (loggedIn == false)
return (false, "Redump username and password denied!");
else
return (null, "An error occurred validating your credentials!");
}
/// <summary>
/// Login to Redump, if possible
/// </summary>
/// <param name="username">Redump username</param>
/// <param name="password">Redump password</param>
/// <returns>True if the user could be logged in, false otherwise, null on error</returns>
public bool? Login(string username, string password)
{
// Credentials verification
if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password))
{
Console.WriteLine("Credentials entered, will attempt Redump login...");
}
else if (!string.IsNullOrEmpty(username) && string.IsNullOrEmpty(password))
{
Console.WriteLine("Only a username was specified, will not attempt Redump login...");
return false;
}
else if (string.IsNullOrEmpty(username))
{
Console.WriteLine("No credentials entered, will not attempt Redump login...");
return false;
}
// HTTP encode the password
#if NET20 || NET35 || NET40
password = Uri.EscapeUriString(password);
#else
password = WebUtility.UrlEncode(password);
#endif
// Attempt to login up to 3 times
for (int i = 0; i < 3; i++)
{
try
{
// Get the current token from the login page
var loginPage = DownloadString(Constants.LoginUrl);
string token = Constants.TokenRegex.Match(loginPage).Groups[1].Value;
// Construct the login request
Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded";
Encoding = Encoding.UTF8;
var response = UploadString(Constants.LoginUrl, $"form_sent=1&redirect_url=&csrf_token={token}&req_username={username}&req_password={password}&save_pass=0");
if (response.Contains("Incorrect username and/or password."))
{
Console.WriteLine("Invalid credentials entered, continuing without logging in...");
return false;
}
// The user was able to be logged in
Console.WriteLine("Credentials accepted! Logged into Redump...");
LoggedIn = true;
// If the user is a moderator or staff, set accordingly
if (response.Contains("http://forum.redump.org/forum/9/staff/"))
IsStaff = true;
return true;
}
catch (Exception ex)
{
Console.WriteLine($"An exception occurred while trying to log in on attempt {i}: {ex}");
}
}
Console.WriteLine("Could not login to Redump in 3 attempts, continuing without logging in...");
return false;
}
#region Single Page Helpers
/// <summary>
/// Process a Redump site page as a list of possible IDs or disc page
/// </summary>
/// <param name="url">Base URL to download using</param>
/// <returns>List of IDs from the page, empty on error</returns>
public List<int> CheckSingleSitePage(string url)
{
List<int> ids = [];
string dumpsPage = string.Empty;
// Try up to 3 times to retrieve the data
for (int i = 0; i < 3; i++)
{
try
{
dumpsPage = DownloadString(url);
break;
}
catch { }
}
// If we have no dumps left
if (dumpsPage.Contains("No discs found."))
return ids;
// If we have a single disc page already
if (dumpsPage.Contains("<b>Download:</b>"))
{
var value = Regex.Match(dumpsPage, @"/disc/(\d+)/sfv/").Groups[1].Value;
if (int.TryParse(value, out int id))
ids.Add(id);
return ids;
}
// Otherwise, traverse each dump on the page
var matches = Constants.DiscRegex.Matches(dumpsPage);
foreach (Match match in matches)
{
try
{
if (int.TryParse(match.Groups[1].Value, out int value))
ids.Add(value);
}
catch (Exception ex)
{
Console.WriteLine($"An exception has occurred: {ex}");
continue;
}
}
return ids;
}
/// <summary>
/// Process a Redump site page as a list of possible IDs or disc page
/// </summary>
/// <param name="url">Base URL to download using</param>
/// <param name="outDir">Output directory to save data to</param>
/// <param name="failOnSingle">True to return on first error, false otherwise</param>
/// <returns>True if the page could be downloaded, false otherwise</returns>
public bool CheckSingleSitePage(string url, string? outDir, bool failOnSingle)
{
string dumpsPage = string.Empty;
// Try up to 3 times to retrieve the data
for (int i = 0; i < 3; i++)
{
try
{
dumpsPage = DownloadString(url);
break;
}
catch { }
}
// If we have no dumps left
if (dumpsPage.Contains("No discs found."))
return false;
// If we have a single disc page already
if (dumpsPage.Contains("<b>Download:</b>"))
{
var value = Regex.Match(dumpsPage, @"/disc/(\d+)/sfv/").Groups[1].Value;
if (int.TryParse(value, out int id))
{
bool downloaded = DownloadSingleSiteID(id, outDir, false);
if (!downloaded && failOnSingle)
return false;
}
return false;
}
// Otherwise, traverse each dump on the page
var matches = Constants.DiscRegex.Matches(dumpsPage);
foreach (Match match in matches)
{
try
{
if (int.TryParse(match.Groups[1].Value, out int value))
{
bool downloaded = DownloadSingleSiteID(value, outDir, false);
if (!downloaded && failOnSingle)
return false;
}
}
catch (Exception ex)
{
Console.WriteLine($"An exception has occurred: {ex}");
continue;
}
}
return true;
}
/// <summary>
/// Process a Redump WIP page as a list of possible IDs or disc page
/// </summary>
/// <param name="wc">RedumpWebClient to access the packs</param>
/// <returns>List of IDs from the page, empty on error</returns>
public List<int> CheckSingleWIPPage(string url)
{
List<int> ids = [];
string dumpsPage = string.Empty;
// Try up to 3 times to retrieve the data
for (int i = 0; i < 3; i++)
{
try
{
dumpsPage = DownloadString(url);
break;
}
catch { }
}
// If we have no dumps left
if (dumpsPage.Contains("No discs found."))
return ids;
// Otherwise, traverse each dump on the page
var matches = Constants.NewDiscRegex.Matches(dumpsPage);
foreach (Match match in matches)
{
try
{
if (int.TryParse(match.Groups[2].Value, out int value))
ids.Add(value);
}
catch (Exception ex)
{
Console.WriteLine($"An exception has occurred: {ex}");
continue;
}
}
return ids;
}
/// <summary>
/// Process a Redump WIP page as a list of possible IDs or disc page
/// </summary>
/// <param name="wc">RedumpWebClient to access the packs</param>
/// <param name="outDir">Output directory to save data to</param>
/// <param name="failOnSingle">True to return on first error, false otherwise</param>
/// <returns>True if the page could be downloaded, false otherwise</returns>
public bool CheckSingleWIPPage(string url, string? outDir, bool failOnSingle)
{
string dumpsPage = string.Empty;
// Try up to 3 times to retrieve the data
for (int i = 0; i < 3; i++)
{
try
{
dumpsPage = DownloadString(url);
break;
}
catch { }
}
// If we have no dumps left
if (dumpsPage.Contains("No discs found."))
return false;
// Otherwise, traverse each dump on the page
var matches = Constants.NewDiscRegex.Matches(dumpsPage);
foreach (Match match in matches)
{
try
{
if (int.TryParse(match.Groups[2].Value, out int value))
{
bool downloaded = DownloadSingleWIPID(value, outDir, false);
if (!downloaded && failOnSingle)
return false;
}
}
catch (Exception ex)
{
Console.WriteLine($"An exception has occurred: {ex}");
continue;
}
}
return true;
}
#endregion
#region Download Helpers
/// <summary>
/// Download a single pack
/// </summary>
/// <param name="url">Base URL to download using</param>
/// <param name="system">System to download packs for</param>
/// <returns>Byte array containing the downloaded pack, null on error</returns>
public byte[]? DownloadSinglePack(string url, RedumpSystem? system)
{
try
{
return DownloadData(string.Format(url, system.ShortName()));
}
catch (Exception ex)
{
Console.WriteLine($"An exception has occurred: {ex}");
return null;
}
}
/// <summary>
/// Download a single pack
/// </summary>
/// <param name="url">Base URL to download using</param>
/// <param name="system">System to download packs for</param>
/// <param name="outDir">Output directory to save data to</param>
/// <param name="subfolder">Named subfolder for the pack, used optionally</param>
public void DownloadSinglePack(string url, RedumpSystem? system, string? outDir, string? subfolder)
{
try
{
// If no output directory is defined, use the current directory instead
if (string.IsNullOrEmpty(outDir))
outDir = Environment.CurrentDirectory;
string tempfile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString());
DownloadFile(string.Format(url, system.ShortName()), tempfile);
MoveOrDelete(tempfile, GetLastFilename(), outDir!, subfolder);
}
catch (Exception ex)
{
Console.WriteLine($"An exception has occurred: {ex}");
}
}
/// <summary>
/// Download an individual site ID data, if possible
/// </summary>
/// <param name="id">Redump disc ID to retrieve</param>
/// <returns>String containing the page contents if successful, null on error</returns>
public string? DownloadSingleSiteID(int id)
{
string paddedId = id.ToString().PadLeft(6, '0');
Console.WriteLine($"Processing ID: {paddedId}");
try
{
string discPage = string.Empty;
// Try up to 3 times to retrieve the data
for (int i = 0; i < 3; i++)
{
try
{
discPage = DownloadString(string.Format(Constants.DiscPageUrl, +id));
break;
}
catch { }
}
if (discPage.Contains($"Disc with ID \"{id}\" doesn't exist"))
{
Console.WriteLine($"ID {paddedId} could not be found!");
return null;
}
Console.WriteLine($"ID {paddedId} has been successfully downloaded");
return discPage;
}
catch (Exception ex)
{
Console.WriteLine($"An exception has occurred: {ex}");
return null;
}
}
/// <summary>
/// Download an individual site ID data, if possible
/// </summary>
/// <param name="id">Redump disc ID to retrieve</param>
/// <param name="outDir">Output directory to save data to</param>
/// <param name="rename">True to rename deleted entries, false otherwise</param>
/// <returns>True if all data was downloaded, false otherwise</returns>
public bool DownloadSingleSiteID(int id, string? outDir, bool rename)
{
// If no output directory is defined, use the current directory instead
if (string.IsNullOrEmpty(outDir))
outDir = Environment.CurrentDirectory;
string paddedId = id.ToString().PadLeft(6, '0');
string paddedIdDir = Path.Combine(outDir, paddedId);
Console.WriteLine($"Processing ID: {paddedId}");
try
{
string discPage = string.Empty;
// Try up to 3 times to retrieve the data
for (int i = 0; i < 3; i++)
{
try
{
discPage = DownloadString(string.Format(Constants.DiscPageUrl, +id));
break;
}
catch { }
}
if (discPage.Contains($"Disc with ID \"{id}\" doesn't exist"))
{
try
{
if (rename)
{
if (Directory.Exists(paddedIdDir) && rename)
Directory.Move(paddedIdDir, paddedIdDir + "-deleted");
else
Directory.CreateDirectory(paddedIdDir + "-deleted");
}
}
catch { }
Console.WriteLine($"ID {paddedId} could not be found!");
return false;
}
// Check if the page has been updated since the last time it was downloaded, if possible
if (File.Exists(Path.Combine(paddedIdDir, "disc.html")))
{
// Read in the cached file
var oldDiscPage = File.ReadAllText(Path.Combine(paddedIdDir, "disc.html"));
// Check for the last modified date in both pages
var oldResult = Constants.LastModifiedRegex.Match(oldDiscPage);
var newResult = Constants.LastModifiedRegex.Match(discPage);
// If both pages contain the same modified date, skip it
if (oldResult.Success && newResult.Success && oldResult.Groups[1].Value == newResult.Groups[1].Value)
{
Console.WriteLine($"ID {paddedId} has not been changed since last download");
return false;
}
// If neither page contains a modified date, skip it
else if (!oldResult.Success && !newResult.Success)
{
Console.WriteLine($"ID {paddedId} has not been changed since last download");
return false;
}
}
// Create ID subdirectory
Directory.CreateDirectory(paddedIdDir);
// View Edit History
if (discPage.Contains($"<a href=\"/disc/{id}/changes/\""))
DownloadFile(string.Format(Constants.DiscPageUrl, +id) + Constants.ChangesExt, Path.Combine(paddedIdDir, "changes.html"));
// CUE
if (discPage.Contains($"<a href=\"/disc/{id}/cue/\""))
DownloadFile(string.Format(Constants.DiscPageUrl, +id) + Constants.CueExt, Path.Combine(paddedIdDir, paddedId + ".cue"));
// Edit disc
if (discPage.Contains($"<a href=\"/disc/{id}/edit/\""))
DownloadFile(string.Format(Constants.DiscPageUrl, +id) + Constants.EditExt, Path.Combine(paddedIdDir, "edit.html"));
// GDI
if (discPage.Contains($"<a href=\"/disc/{id}/gdi/\""))
DownloadFile(string.Format(Constants.DiscPageUrl, +id) + Constants.GdiExt, Path.Combine(paddedIdDir, paddedId + ".gdi"));
// KEYS
if (discPage.Contains($"<a href=\"/disc/{id}/key/\""))
DownloadFile(string.Format(Constants.DiscPageUrl, +id) + Constants.KeyExt, Path.Combine(paddedIdDir, paddedId + ".key"));
// LSD
if (discPage.Contains($"<a href=\"/disc/{id}/lsd/\""))
DownloadFile(string.Format(Constants.DiscPageUrl, +id) + Constants.LsdExt, Path.Combine(paddedIdDir, paddedId + ".lsd"));
// MD5
if (discPage.Contains($"<a href=\"/disc/{id}/md5/\""))
DownloadFile(string.Format(Constants.DiscPageUrl, +id) + Constants.Md5Ext, Path.Combine(paddedIdDir, paddedId + ".md5"));
// Review WIP entry
if (Constants.NewDiscRegex.IsMatch(discPage))
{
var match = Constants.NewDiscRegex.Match(discPage);
DownloadFile(string.Format(Constants.WipDiscPageUrl, match.Groups[2].Value), Path.Combine(paddedIdDir, "newdisc.html"));
}
// SBI
if (discPage.Contains($"<a href=\"/disc/{id}/sbi/\""))
DownloadFile(string.Format(Constants.DiscPageUrl, +id) + Constants.SbiExt, Path.Combine(paddedIdDir, paddedId + ".sbi"));
// SFV
if (discPage.Contains($"<a href=\"/disc/{id}/sfv/\""))
DownloadFile(string.Format(Constants.DiscPageUrl, +id) + Constants.SfvExt, Path.Combine(paddedIdDir, paddedId + ".sfv"));
// SHA1
if (discPage.Contains($"<a href=\"/disc/{id}/sha1/\""))
DownloadFile(string.Format(Constants.DiscPageUrl, +id) + Constants.Sha1Ext, Path.Combine(paddedIdDir, paddedId + ".sha1"));
// HTML (Last in case of errors)
using (var discStreamWriter = File.CreateText(Path.Combine(paddedIdDir, "disc.html")))
{
discStreamWriter.Write(discPage);
}
Console.WriteLine($"ID {paddedId} has been successfully downloaded");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"An exception has occurred: {ex}");
return false;
}
}
/// <summary>
/// Download an individual WIP ID data, if possible
/// </summary>
/// <param name="id">Redump WIP disc ID to retrieve</param>
/// <returns>String containing the page contents if successful, null on error</returns>
public string? DownloadSingleWIPID(int id)
{
string paddedId = id.ToString().PadLeft(6, '0');
Console.WriteLine($"Processing ID: {paddedId}");
try
{
string discPage = string.Empty;
// Try up to 3 times to retrieve the data
for (int i = 0; i < 3; i++)
{
try
{
discPage = DownloadString(string.Format(Constants.WipDiscPageUrl, +id));
break;
}
catch { }
}
if (discPage.Contains($"WIP disc with ID \"{id}\" doesn't exist"))
{
Console.WriteLine($"ID {paddedId} could not be found!");
return null;
}
Console.WriteLine($"ID {paddedId} has been successfully downloaded");
return discPage;
}
catch (Exception ex)
{
Console.WriteLine($"An exception has occurred: {ex}");
return null;
}
}
/// <summary>
/// Download an individual WIP ID data, if possible
/// </summary>
/// <param name="id">Redump WIP disc ID to retrieve</param>
/// <param name="outDir">Output directory to save data to</param>
/// <param name="rename">True to rename deleted entries, false otherwise</param>
/// <returns>True if all data was downloaded, false otherwise</returns>
public bool DownloadSingleWIPID(int id, string? outDir, bool rename)
{
// If no output directory is defined, use the current directory instead
if (string.IsNullOrEmpty(outDir))
outDir = Environment.CurrentDirectory;
string paddedId = id.ToString().PadLeft(6, '0');
string paddedIdDir = Path.Combine(outDir, paddedId);
Console.WriteLine($"Processing ID: {paddedId}");
try
{
string discPage = string.Empty;
// Try up to 3 times to retrieve the data
for (int i = 0; i < 3; i++)
{
try
{
discPage = DownloadString(string.Format(Constants.WipDiscPageUrl, +id));
break;
}
catch { }
}
if (discPage.Contains($"WIP disc with ID \"{id}\" doesn't exist"))
{
try
{
if (rename)
{
if (Directory.Exists(paddedIdDir) && rename)
Directory.Move(paddedIdDir, paddedIdDir + "-deleted");
else
Directory.CreateDirectory(paddedIdDir + "-deleted");
}
}
catch { }
Console.WriteLine($"ID {paddedId} could not be found!");
return false;
}
// Check if the page has been updated since the last time it was downloaded, if possible
if (File.Exists(Path.Combine(paddedIdDir, "disc.html")))
{
// Read in the cached file
var oldDiscPage = File.ReadAllText(Path.Combine(paddedIdDir, "disc.html"));
// Check for the full match ID in both pages
var oldResult = Constants.FullMatchRegex.Match(oldDiscPage);
var newResult = Constants.FullMatchRegex.Match(discPage);
// If both pages contain the same ID, skip it
if (oldResult.Success && newResult.Success && oldResult.Groups[1].Value == newResult.Groups[1].Value)
{
Console.WriteLine($"ID {paddedId} has not been changed since last download");
return false;
}
// If neither page contains an ID, skip it
else if (!oldResult.Success && !newResult.Success)
{
Console.WriteLine($"ID {paddedId} has not been changed since last download");
return false;
}
}
// Create ID subdirectory
Directory.CreateDirectory(paddedIdDir);
// HTML
using (var discStreamWriter = File.CreateText(Path.Combine(paddedIdDir, "disc.html")))
{
discStreamWriter.Write(discPage);
}
Console.WriteLine($"ID {paddedId} has been successfully downloaded");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"An exception has occurred: {ex}");
return false;
}
}
#endregion
#region Helpers
/// <summary>
/// Download a set of packs
/// </summary>
/// <param name="url">Base URL to download using</param>
/// <param name="system">Systems to download packs for</param>
/// <param name="title">Name of the pack that is downloading</param>
public Dictionary<RedumpSystem, byte[]> DownloadPacks(string url, RedumpSystem?[] systems, string title)
{
var packsDictionary = new Dictionary<RedumpSystem, byte[]>();
Console.WriteLine($"Downloading {title}");
foreach (var system in systems)
{
// If the system is invalid, we can't do anything
if (system == null || !system.IsAvailable())
continue;
// If we didn't have credentials
if (!LoggedIn && system.IsBanned())
continue;
// If the system is unknown, we can't do anything
string? longName = system.LongName();
if (string.IsNullOrEmpty(longName))
continue;
Console.Write($"\r{longName}{new string(' ', Console.BufferWidth - longName!.Length - 1)}");
byte[]? pack = DownloadSinglePack(url, system);
if (pack != null)
packsDictionary.Add(system.Value, pack);
}
Console.Write($"\rComplete!{new string(' ', Console.BufferWidth - 10)}");
Console.WriteLine();
return packsDictionary;
}
/// <summary>
/// Download a set of packs
/// </summary>
/// <param name="url">Base URL to download using</param>
/// <param name="system">Systems to download packs for</param>
/// <param name="title">Name of the pack that is downloading</param>
/// <param name="outDir">Output directory to save data to</param>
/// <param name="subfolder">Named subfolder for the pack, used optionally</param>
public void DownloadPacks(string url, RedumpSystem?[] systems, string title, string? outDir, string? subfolder)
{
Console.WriteLine($"Downloading {title}");
foreach (var system in systems)
{
// If the system is invalid, we can't do anything
if (system == null || !system.IsAvailable())
continue;
// If we didn't have credentials
if (!LoggedIn && system.IsBanned())
continue;
// If the system is unknown, we can't do anything
string? longName = system.LongName();
if (string.IsNullOrEmpty(longName))
continue;
Console.Write($"\r{longName}{new string(' ', Console.BufferWidth - longName!.Length - 1)}");
DownloadSinglePack(url, system, outDir, subfolder);
}
Console.Write($"\rComplete!{new string(' ', Console.BufferWidth - 10)}");
Console.WriteLine();
}
/// <summary>
/// Move a tempfile to a new name unless it aleady exists, in which case, delete the tempfile
/// </summary>
/// <param name="tempfile">Path to existing temporary file</param>
/// <param name="newfile">Path to new output file</param>
/// <param name="outDir">Output directory to save data to</param>
/// <param name="subfolder">Optional subfolder to append to the path</param>
private static void MoveOrDelete(string tempfile, string? newfile, string outDir, string? subfolder)
{
if (!string.IsNullOrEmpty(newfile))
{
if (!string.IsNullOrEmpty(subfolder))
{
if (!Directory.Exists(Path.Combine(outDir, subfolder)))
Directory.CreateDirectory(Path.Combine(outDir, subfolder));
newfile = Path.Combine(subfolder, newfile);
}
if (File.Exists(Path.Combine(outDir, newfile)))
File.Delete(tempfile);
else
File.Move(tempfile, Path.Combine(outDir, newfile));
}
else
{
File.Delete(tempfile);
}
}
#endregion
}
}
#endif

36
publish-nix.sh Normal file
View File

@@ -0,0 +1,36 @@
#! /bin/bash
# This batch file assumes the following:
# - .NET 8.0 (or newer) SDK is installed and in PATH
#
# If any of these are not satisfied, the operation may fail
# in an unpredictable way and result in an incomplete output.
# Optional parameters
NO_BUILD=false
while getopts "uba" OPTION
do
case $OPTION in
b)
NO_BUILD=true
;;
*)
echo "Invalid option provided"
exit 1
;;
esac
done
# Set the current directory as a variable
BUILD_FOLDER=$PWD
# Only build if requested
if [ $NO_BUILD = false ]
then
# Restore Nuget packages for all builds
echo "Restoring Nuget packages"
dotnet restore
# Create Nuget Package
dotnet pack SabreTools.RedumpLib/SabreTools.RedumpLib.csproj --output $BUILD_FOLDER
fi

26
publish-win.ps1 Normal file
View File

@@ -0,0 +1,26 @@
# This batch file assumes the following:
# - .NET 8.0 (or newer) SDK is installed and in PATH
#
# If any of these are not satisfied, the operation may fail
# in an unpredictable way and result in an incomplete output.
# Optional parameters
param(
[Parameter(Mandatory = $false)]
[Alias("NoBuild")]
[switch]$NO_BUILD
)
# Set the current directory as a variable
$BUILD_FOLDER = $PSScriptRoot
# Only build if requested
if (!$NO_BUILD.IsPresent)
{
# Restore Nuget packages for all builds
Write-Host "Restoring Nuget packages"
dotnet restore
# Create Nuget Package
dotnet pack SabreTools.RedumpLib\SabreTools.RedumpLib.csproj --output $BUILD_FOLDER
}