mirror of
https://github.com/SabreTools/SabreTools.RedumpLib.git
synced 2026-02-08 13:54:31 +00:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16d196c902 | ||
|
|
c93da92f19 | ||
|
|
a219d0c5de | ||
|
|
02e6f0e85f | ||
|
|
9dfec64e4e | ||
|
|
4c12693a33 | ||
|
|
acea06c05f | ||
|
|
067c5cfbbc | ||
|
|
ff04b8ec6f | ||
|
|
e09c895cf1 | ||
|
|
1b68253089 | ||
|
|
5b9a2d6b74 | ||
|
|
d5c7ef74d4 | ||
|
|
f60cd2985d | ||
|
|
7fcb6aa949 | ||
|
|
f22b1b036b | ||
|
|
ec045448c5 | ||
|
|
93873ea204 | ||
|
|
341edc56bd | ||
|
|
da4bdac6e2 | ||
|
|
7fe595ee0a | ||
|
|
8a9f62f5a4 | ||
|
|
dbb7cf7ef9 | ||
|
|
d591ee1550 | ||
|
|
9153c931a5 | ||
|
|
99ebd1f3ac | ||
|
|
844f5506f5 | ||
|
|
4be01b25ab | ||
|
|
22e2e73f65 | ||
|
|
831ea86d4f | ||
|
|
4475dba94c | ||
|
|
63a758c005 | ||
|
|
7e81f723ca | ||
|
|
f69c7e6bb2 | ||
|
|
9d2803a6df | ||
|
|
23f1ceac99 | ||
|
|
55c621b615 | ||
|
|
29cce4d4b9 | ||
|
|
1f37ece28d | ||
|
|
6acac60376 |
43
.github/workflows/build_nupkg.yml
vendored
Normal file
43
.github/workflows/build_nupkg.yml
vendored
Normal 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
17
.github/workflows/check_pr.yml
vendored
Normal 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
|
||||
201
SabreTools.RedumpLib.Test/EnumExtensionsTests.cs
Normal file
201
SabreTools.RedumpLib.Test/EnumExtensionsTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
717
SabreTools.RedumpLib.Test/ExtensionsTests.cs
Normal file
717
SabreTools.RedumpLib.Test/ExtensionsTests.cs
Normal 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
|
||||
}
|
||||
}
|
||||
37
SabreTools.RedumpLib.Test/SabreTools.RedumpLib.Test.csproj
Normal file
37
SabreTools.RedumpLib.Test/SabreTools.RedumpLib.Test.csproj
Normal file
@@ -0,0 +1,37 @@
|
||||
<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>
|
||||
<PackageReference Include="Microsoft.CodeCoverage" Version="17.10.0-release-24177-07" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0-release-24177-07" />
|
||||
<PackageReference Include="xunit" Version="2.8.0" />
|
||||
<PackageReference Include="xunit.abstractions" Version="2.0.3" />
|
||||
<PackageReference Include="xunit.analyzers" Version="1.13.0" />
|
||||
<PackageReference Include="xunit.assert" Version="2.8.0" />
|
||||
<PackageReference Include="xunit.core" Version="2.8.0" />
|
||||
<PackageReference Include="xunit.extensibility.core" Version="2.8.0" />
|
||||
<PackageReference Include="xunit.extensibility.execution" Version="2.8.0" />
|
||||
<PackageReference Include="xunit.runner.console" Version="2.8.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
180
SabreTools.RedumpLib.Test/SubmissionInfoTests.cs
Normal file
180
SabreTools.RedumpLib.Test/SubmissionInfoTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -246,25 +246,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;
|
||||
|
||||
@@ -386,6 +376,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
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -239,6 +239,7 @@ namespace SabreTools.RedumpLib.Data
|
||||
|
||||
// https://en.wikipedia.org/wiki/Game_Wave_Family_Entertainment_System
|
||||
case RedumpSystem.ZAPiTGamesGameWaveFamilyEntertainmentSystem:
|
||||
types.Add(MediaType.CDROM); // Firmware discs only(?)
|
||||
types.Add(MediaType.DVD);
|
||||
break;
|
||||
|
||||
@@ -1062,6 +1063,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>
|
||||
@@ -530,6 +530,10 @@ namespace SabreTools.RedumpLib.Data
|
||||
/// </summary>
|
||||
public class DumpingInfoSection : ICloneable
|
||||
{
|
||||
// Name not defined by Redump -- Only used with MPF
|
||||
[JsonProperty(PropertyName = "d_frontend_version", Required = Required.AllowNull)]
|
||||
public string? FrontendVersion { get; set; }
|
||||
|
||||
// Name not defined by Redump
|
||||
[JsonProperty(PropertyName = "d_dumping_program", Required = Required.AllowNull)]
|
||||
public string? DumpingProgram { get; set; }
|
||||
@@ -538,6 +542,10 @@ namespace SabreTools.RedumpLib.Data
|
||||
[JsonProperty(PropertyName = "d_dumping_date", Required = Required.AllowNull)]
|
||||
public string? DumpingDate { get; set; }
|
||||
|
||||
// Name not defined by Redump
|
||||
[JsonProperty(PropertyName = "d_dumping_params", Required = Required.AllowNull)]
|
||||
public string? DumpingParameters { get; set; }
|
||||
|
||||
// Name not defined by Redump
|
||||
[JsonProperty(PropertyName = "d_drive_manufacturer", Required = Required.AllowNull)]
|
||||
public string? Manufacturer { get; set; }
|
||||
@@ -554,16 +562,23 @@ namespace SabreTools.RedumpLib.Data
|
||||
[JsonProperty(PropertyName = "d_reported_disc_type", Required = Required.AllowNull)]
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
145
SabreTools.RedumpLib/Downloader.cs
Normal file
145
SabreTools.RedumpLib/Downloader.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,12 +229,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;
|
||||
@@ -562,8 +565,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 +578,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 +596,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 +658,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]));
|
||||
@@ -7,12 +7,12 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Version>1.3.2</Version>
|
||||
<Version>1.4.0</Version>
|
||||
|
||||
<!-- Package Properties -->
|
||||
<Authors>Matt Nadareski</Authors>
|
||||
<Description>Code to interact with redump.org</Description>
|
||||
<Copyright>Copyright (c) Matt Nadareski 2020-2023</Copyright>
|
||||
<Copyright>Copyright (c) Matt Nadareski 2020-2024</Copyright>
|
||||
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/SabreTools/SabreTools.RedumpLib</RepositoryUrl>
|
||||
@@ -22,7 +22,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath="" />
|
||||
<None Include="../README.md" Pack="true" PackagePath="" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Support for old .NET versions -->
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="SabreTools.Models" Version="1.3.0" />
|
||||
<PackageReference Include="SabreTools.Models" Version="1.4.8" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -79,15 +79,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 +109,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,18 +127,14 @@ 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
|
||||
public async static Task<(bool, List<int>?, string?)> 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)
|
||||
@@ -170,15 +156,11 @@ namespace SabreTools.RedumpLib
|
||||
/// <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
|
||||
public async static Task<(bool, List<int>?, string?)> ValidateUniversalHash(RedumpClient rc, SubmissionInfo info)
|
||||
{
|
||||
// If we don't have special fields
|
||||
if (info.CommonDiscInfo?.CommentsSpecialFields == null)
|
||||
@@ -193,7 +175,7 @@ namespace SabreTools.RedumpLib
|
||||
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)
|
||||
@@ -215,24 +197,14 @@ namespace SabreTools.RedumpLib
|
||||
/// <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;
|
||||
|
||||
40
SabreTools.RedumpLib/Web/CookieWebClient.cs
Normal file
40
SabreTools.RedumpLib/Web/CookieWebClient.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
SabreTools.RedumpLib/Web/DelayHelper.cs
Normal file
21
SabreTools.RedumpLib/Web/DelayHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
SabreTools.RedumpLib/Web/Discs.cs
Normal file
55
SabreTools.RedumpLib/Web/Discs.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
SabreTools.RedumpLib/Web/Packs.cs
Normal file
73
SabreTools.RedumpLib/Web/Packs.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
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 NETFRAMEWORK || 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
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
#if !NETFRAMEWORK
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
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 +29,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
|
||||
@@ -50,9 +81,9 @@ namespace SabreTools.RedumpLib.Web
|
||||
return (false, null);
|
||||
|
||||
// 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!");
|
||||
else if (loggedIn == false)
|
||||
@@ -86,7 +117,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 +129,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 +198,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."))
|
||||
@@ -200,8 +246,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."))
|
||||
@@ -256,8 +302,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."))
|
||||
@@ -294,8 +340,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."))
|
||||
@@ -341,7 +387,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 +443,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 +481,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 +605,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 +643,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 +739,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 +777,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 +794,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 +817,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 +886,4 @@ namespace SabreTools.RedumpLib.Web
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
93
SabreTools.RedumpLib/Web/Search.cs
Normal file
93
SabreTools.RedumpLib/Web/Search.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
100
SabreTools.RedumpLib/Web/User.cs
Normal file
100
SabreTools.RedumpLib/Web/User.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
SabreTools.RedumpLib/Web/WIP.cs
Normal file
46
SabreTools.RedumpLib/Web/WIP.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user