diff --git a/hasheous-lib/Classes/Config.cs b/hasheous-lib/Classes/Config.cs
index b605c280..12d19d67 100644
--- a/hasheous-lib/Classes/Config.cs
+++ b/hasheous-lib/Classes/Config.cs
@@ -1,10 +1,10 @@
using System;
using System.Data;
-using Newtonsoft.Json;
-using IGDB.Models;
using hasheous_server.Classes.Metadata;
-using StackExchange.Redis;
+using IGDB.Models;
using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Newtonsoft.Json;
+using StackExchange.Redis;
namespace Classes
{
@@ -135,6 +135,17 @@ public static ConfigFile.GiantBomb GiantBomb
}
}
+ ///
+ /// Gets the ScreenScraper configuration settings.
+ ///
+ public static ConfigFile.ScreenScraper ScreenScraperConfiguration
+ {
+ get
+ {
+ return _config.ScreenScraperConfiguration;
+ }
+ }
+
///
/// Gets the social authentication configuration settings.
///
@@ -567,6 +578,11 @@ public class ConfigFile
///
public GiantBomb GiantBombConfiguration = new GiantBomb();
+ ///
+ /// Gets or sets the ScreenScraper configuration settings.
+ ///
+ public ScreenScraper ScreenScraperConfiguration = new ScreenScraper();
+
///
/// Gets or sets the social authentication configuration settings.
///
@@ -923,6 +939,14 @@ public string LibraryMetadataDirectory_WHDLoad
}
}
+ public string LibraryMetadataDirectory_Screenscraper
+ {
+ get
+ {
+ return Path.Combine(LibraryMetadataDirectory, "Screenscraper");
+ }
+ }
+
///
/// Gets the directory path for MAME Redump metadata within the library metadata directory.
///
@@ -1290,6 +1314,89 @@ private static string _DefaultAPIKey
public string BaseURL = "https://www.giantbomb.com/";
}
+ ///
+ /// Represents the ScreenScraper configuration settings.
+ ///
+ public class ScreenScraper
+ {
+ private static string _DefaultClientId
+ {
+ get
+ {
+ if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("screenscraperclientid")))
+ {
+ return Environment.GetEnvironmentVariable("screenscraperclientid");
+ }
+ else
+ {
+ return "";
+ }
+ }
+ }
+
+ private static string _DefaultSecret
+ {
+ get
+ {
+ if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("screenscraperclientsecret")))
+ {
+ return Environment.GetEnvironmentVariable("screenscraperclientsecret");
+ }
+ else
+ {
+ return "";
+ }
+ }
+ }
+
+ private static string _DefaultDevClientId
+ {
+ get
+ {
+ if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("screenscraperdevclientid")))
+ {
+ return Environment.GetEnvironmentVariable("screenscraperdevclientid");
+ }
+ else
+ {
+ return "";
+ }
+ }
+ }
+
+ private static string _DefaultDevSecret
+ {
+ get
+ {
+ if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("screenscraperdevclientsecret")))
+ {
+ return Environment.GetEnvironmentVariable("screenscraperdevclientsecret");
+ }
+ else
+ {
+ return "";
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the ScreenScraper client ID used for authenticating API requests.
+ ///
+ public string ClientId = _DefaultClientId;
+ ///
+ /// Gets or sets the ScreenScraper client secret used for authenticating API requests.
+ ///
+ public string Secret = _DefaultSecret;
+ ///
+ /// Gets or sets the ScreenScraper developer client ID used for authenticating API requests in development environments.
+ ///
+ public string DevClientId = _DefaultDevClientId;
+ ///
+ /// Gets or sets the ScreenScraper developer client secret used for authenticating API requests in development environments.
+ ///
+ public string DevSecret = _DefaultDevSecret;
+ }
+
///
/// Represents the social authentication configuration settings, including Google and Microsoft OAuth credentials.
///
diff --git a/hasheous-lib/Classes/DataObjects.cs b/hasheous-lib/Classes/DataObjects.cs
index 2ec62da6..03bd94a6 100644
--- a/hasheous-lib/Classes/DataObjects.cs
+++ b/hasheous-lib/Classes/DataObjects.cs
@@ -1148,7 +1148,7 @@ public async Task GetRoms(List> GameSi
return attribute;
}
- public async Task NewDataObject(DataObjectType objectType, Models.DataObjectItemModel model, ApplicationUser? user = null)
+ public async Task NewDataObject(DataObjectType objectType, Models.DataObjectItemModel model, ApplicationUser? user = null, bool allowSearch = true)
{
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = "INSERT INTO DataObject (`Name`, `ObjectType`, `CreatedDate`, `UpdatedDate`) VALUES (@name, @objecttype, @createddate, @updateddate); SELECT LAST_INSERT_ID();";
@@ -1184,7 +1184,10 @@ public async Task GetRoms(List> GameSi
}
}
- await DataObjectMetadataSearch(objectType, (long)(ulong)data.Rows[0][0]);
+ if (allowSearch)
+ {
+ await DataObjectMetadataSearch(objectType, (long)(ulong)data.Rows[0][0]);
+ }
break;
case DataObjectType.App:
@@ -1894,13 +1897,25 @@ public async Task DataObjectMetadataSearch(DataObjectType objectType, long? id,
// get all metadata sources
private static MetadataSources[] allMetadataSources = (MetadataSources[])Enum.GetValues(typeof(MetadataSources));
+ // Tokens commonly found in titles/platform names that should not be interpreted as Roman numerals.
+ private static readonly List RomanConversionAcronymExclusions = new List
+ {
+ "CD",
+ "DVD",
+ "PS",
+ "PSP",
+ "VR",
+ "3DO"
+ };
+
private async Task _DataObjectMetadataSearch(DataObjectType objectType, long? id, bool ForceSearch)
{
HashSet ProcessSources = [
MetadataSources.IGDB,
MetadataSources.TheGamesDb,
MetadataSources.RetroAchievements,
- MetadataSources.GiantBomb
+ MetadataSources.GiantBomb,
+ MetadataSources.ScreenScraper
];
// set now time
@@ -2138,6 +2153,13 @@ private async Task _DataObjectMetadataSearch_Apply(DataObjectItem item, string l
continue;
}
+ // check if the provider is enabled
+ if (!metadataHandler.Enabled)
+ {
+ Logging.Log(Logging.LogType.Warning, "Metadata Match", $"Metadata provider for source {metadataSource} is not enabled. Skipping.");
+ continue;
+ }
+
// get the metadataitem from the dataobject - if not present, create a new one
// default to new
DataObjectItem.MetadataItem metadata = new DataObjectItem.MetadataItem(objectType)
@@ -2195,6 +2217,12 @@ private async Task _DataObjectMetadataSearch_Apply(DataObjectItem item, string l
Logging.Log(Logging.LogType.Information, "Metadata Match", $"{processedObjectCount} / {objectTotalCount} - {item.ObjectType} {item.Name} {metadata.MatchMethod} to {metadata.Source} metadata: {metadata.Id}");
}
+ catch (MetadataLib.MetadataRateLimitException ex)
+ {
+ Logging.Log(Logging.LogType.Warning, "Metadata Match", $"{processedObjectCount} / {objectTotalCount} - Rate limit reached, retry after {ex.RetryAfter}", ex);
+ metadata.NextSearch = ex.RetryAfter;
+ metadataUpdates.Add(metadata);
+ }
catch (Exception ex)
{
Logging.Log(Logging.LogType.Warning, "Metadata Match", $"{processedObjectCount} / {objectTotalCount} - Error processing metadata search", ex);
@@ -2328,6 +2356,7 @@ public static List GetSearchCandidates(string GameName)
}
List searchCandidates = new List();
+ List lowPriorityCandidates = new List();
string NormalizeWhitespace(string value)
{
@@ -2348,6 +2377,15 @@ void AddCandidate(string value)
}
}
+ void AddLowPriorityCandidate(string value)
+ {
+ string normalized = NormalizeWhitespace(value);
+ if (!string.IsNullOrWhiteSpace(normalized))
+ {
+ lowPriorityCandidates.Add(normalized);
+ }
+ }
+
string baseName = NormalizeWhitespace(GameName);
AddCandidate(baseName);
@@ -2379,15 +2417,46 @@ void AddCandidate(string value)
void AddDelimiterVariants(string value)
{
- if (value.Contains(" - ", StringComparison.Ordinal))
+ // normalize and expand delimiter variations so comparisons can match API results
+ // regardless of whether separators are spaces, hyphens, or colons.
+ string hyphenTight = Regex.Replace(value, @"\s*-\s*", "-");
+ string hyphenSpaced = Regex.Replace(value, @"\s*-\s*", " - ");
+ string hyphenToSpace = Regex.Replace(value, @"\s*-\s*", " ");
+ string hyphenToColon = Regex.Replace(value, @"\s*-\s*", ": ");
+
+ AddCandidate(hyphenTight);
+ AddCandidate(hyphenSpaced);
+ AddCandidate(hyphenToSpace);
+ AddCandidate(hyphenToColon);
+
+ // if a value has spaces but no hyphens, generate a hyphenated variant.
+ if (!value.Contains("-", StringComparison.Ordinal) && value.Contains(" ", StringComparison.Ordinal))
{
- AddCandidate(value.Replace(" - ", ": "));
- AddCandidate(value.Replace(" - ", " "));
+ AddCandidate(Regex.Replace(value, @"\s+", "-"));
}
- if (value.Contains(": ", StringComparison.Ordinal))
+ if (value.Contains(":", StringComparison.Ordinal))
{
- AddCandidate(value.Replace(": ", " "));
+ AddCandidate(Regex.Replace(value, @"\s*:\s*", " "));
+ AddCandidate(Regex.Replace(value, @"\s*:\s*", " - "));
+ }
+
+ if (value.Contains("/", StringComparison.Ordinal))
+ {
+ // Slash variants are useful but noisy; keep these as lower-priority candidates.
+ AddLowPriorityCandidate(Regex.Replace(value, @"\s*/\s*", "/"));
+ AddLowPriorityCandidate(Regex.Replace(value, @"\s*/\s*", " "));
+ AddLowPriorityCandidate(Regex.Replace(value, @"\s*/\s*", " - "));
+
+ string[] slashParts = Regex.Split(value, @"\s*/\s*")
+ .Where(part => !string.IsNullOrWhiteSpace(part))
+ .ToArray();
+
+ if (slashParts.Length == 2)
+ {
+ AddLowPriorityCandidate(slashParts[0]);
+ AddLowPriorityCandidate(slashParts[1]);
+ }
}
}
@@ -2448,7 +2517,20 @@ void AddArticleVariants(string value)
{
string romanConverted = Regex.Replace(candidate, @"\b[IVXLCDM]+\b", match =>
{
- return Common.RomanNumerals.RomanToInt(match.Value).ToString();
+ string token = match.Value;
+
+ if (RomanConversionAcronymExclusions.Contains(token, StringComparer.OrdinalIgnoreCase))
+ {
+ return token;
+ }
+
+ int parsed = Common.RomanNumerals.RomanToInt(token);
+ if (parsed >= 1 && parsed <= 30)
+ {
+ return parsed.ToString();
+ }
+
+ return token;
}, RegexOptions.IgnoreCase);
if (!string.Equals(romanConverted, candidate, StringComparison.Ordinal))
@@ -2462,7 +2544,7 @@ void AddArticleVariants(string value)
{
string numberToRoman = Regex.Replace(candidate, @"\b(\d+)\b", match =>
{
- if (int.TryParse(match.Groups[1].Value, out int num) && num >= 1 && num <= 3999)
+ if (int.TryParse(match.Groups[1].Value, out int num) && num >= 1 && num <= 30)
{
return Common.RomanNumerals.IntToRoman(num);
}
@@ -2481,7 +2563,7 @@ void AddArticleVariants(string value)
// Convert numbers to words
string numberToWords = Regex.Replace(candidate, @"\b(\d+)\b", match =>
{
- if (int.TryParse(match.Groups[1].Value, out int num) && num >= 0 && num <= 999999999)
+ if (int.TryParse(match.Groups[1].Value, out int num) && num >= 1 && num <= 30)
{
return Common.Numbers.NumberToWords(num);
}
@@ -2497,7 +2579,12 @@ void AddArticleVariants(string value)
string wordsToNumber = Regex.Replace(candidate, @"\b(?:Zero|One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|Eleven|Twelve|Thirteen|Fourteen|Fifteen|Sixteen|Seventeen|Eighteen|Nineteen|Twenty|Thirty|Forty|Fifty|Sixty|Seventy|Eighty|Ninety|Hundred|Thousand|Million|Billion)(?:\s+(?:Zero|One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|Eleven|Twelve|Thirteen|Fourteen|Fifteen|Sixteen|Seventeen|Eighteen|Nineteen|Twenty|Thirty|Forty|Fifty|Sixty|Seventy|Eighty|Ninety|Hundred|Thousand|Million|Billion))*\b", match =>
{
var result = Common.Numbers.WordsToNumbers(match.Value);
- return result.HasValue ? result.Value.ToString() : match.Value;
+ if (result.HasValue && result.Value >= 1 && result.Value <= 30)
+ {
+ return result.Value.ToString();
+ }
+
+ return match.Value;
}, RegexOptions.IgnoreCase);
if (!string.Equals(wordsToNumber, candidate, StringComparison.Ordinal))
@@ -2506,6 +2593,9 @@ void AddArticleVariants(string value)
}
}
+ // keep lower-confidence slash-derived candidates at the end to emulate weighting.
+ searchCandidates.AddRange(lowPriorityCandidates);
+
// remove duplicates while preserving order
List distinctCandidates = new List();
HashSet seen = new HashSet(StringComparer.OrdinalIgnoreCase);
diff --git a/hasheous-lib/Classes/Database.cs b/hasheous-lib/Classes/Database.cs
index 21d8ab1d..ae2a6974 100644
--- a/hasheous-lib/Classes/Database.cs
+++ b/hasheous-lib/Classes/Database.cs
@@ -703,6 +703,9 @@ public void BuildTableFromType(string databaseName, string prefix, Type type, st
case "IdentitiesOrValues`1":
columnType = "LONGTEXT";
break;
+ default:
+ columnType = "LONGTEXT";
+ break;
}
}
diff --git a/hasheous-lib/Classes/HashLookup2.cs b/hasheous-lib/Classes/HashLookup2.cs
index c5110b42..096e455a 100644
--- a/hasheous-lib/Classes/HashLookup2.cs
+++ b/hasheous-lib/Classes/HashLookup2.cs
@@ -161,7 +161,8 @@ public async Task PerformLookup()
publisher = await dataObjects.NewDataObject(DataObjects.DataObjectType.Company, new DataObjectItemModel
{
Name = discoveredSignature.Game.Publisher
- });
+ }, allowSearch: false);
+
// add signature mappinto to publisher
dataObjects.AddSignature(publisher.Id, DataObjects.DataObjectType.Company, discoveredSignature.Game.PublisherId);
@@ -212,7 +213,8 @@ public async Task PerformLookup()
platform = await dataObjects.NewDataObject(DataObjects.DataObjectType.Platform, new DataObjectItemModel
{
Name = discoveredSignature.Game.System
- });
+ }, allowSearch: false);
+
// add signature mapping to platform
dataObjects.AddSignature(platform.Id, DataObjects.DataObjectType.Platform, discoveredSignature.Game.SystemId);
@@ -279,7 +281,7 @@ public async Task PerformLookup()
game = await dataObjects.NewDataObject(DataObjects.DataObjectType.Game, new DataObjectItemModel
{
Name = gameName
- });
+ }, allowSearch: false);
// add platform reference
await dataObjects.AddAttribute(game.Id, new AttributeItem
@@ -300,8 +302,6 @@ public async Task PerformLookup()
Value = publisher.Id
});
}
- // force metadata search
- await dataObjects.DataObjectMetadataSearch(DataObjects.DataObjectType.Game, game.Id, true);
}
else if (game == null && !this.ForceSearch)
{
@@ -360,6 +360,9 @@ public async Task PerformLookup()
}
}
+ // force metadata search
+ await dataObjects.DataObjectMetadataSearch(DataObjects.DataObjectType.Game, game.Id, true);
+
// re-get the game
game = await dataObjects.GetDataObject(DataObjects.DataObjectType.Game, game.Id);
}
diff --git a/hasheous-lib/Classes/Metadata/Communications.cs b/hasheous-lib/Classes/Metadata/Communications.cs
index 25cf98e3..5089f3ac 100644
--- a/hasheous-lib/Classes/Metadata/Communications.cs
+++ b/hasheous-lib/Classes/Metadata/Communications.cs
@@ -206,7 +206,12 @@ public enum MetadataSources
///
/// SteamGridDb - queries SteamGridDb service for metadata
///
- SteamGridDb
+ SteamGridDb,
+
+ ///
+ /// ScreenScraper - queries ScreenScraper service for metadata
+ ///
+ ScreenScraper
}
///
diff --git a/hasheous-lib/Classes/Metadata/GiantBomb/IMetadata_GiantBomb.cs b/hasheous-lib/Classes/Metadata/GiantBomb/IMetadata_GiantBomb.cs
index 053ca8b0..a2639cc3 100644
--- a/hasheous-lib/Classes/Metadata/GiantBomb/IMetadata_GiantBomb.cs
+++ b/hasheous-lib/Classes/Metadata/GiantBomb/IMetadata_GiantBomb.cs
@@ -10,6 +10,15 @@ public class MetadataGiantBomb : IMetadata
///
public Metadata.Communications.MetadataSources MetadataSource => Metadata.Communications.MetadataSources.GiantBomb;
+ ///
+ public bool Enabled
+ {
+ get
+ {
+ return !String.IsNullOrEmpty(Config.GiantBomb.APIKey);
+ }
+ }
+
///
public async Task FindMatchItemAsync(hasheous_server.Models.DataObjectItem item, List searchCandidates, Dictionary? options = null)
{
diff --git a/hasheous-lib/Classes/Metadata/IGDB/IMetadata_IGDB.cs b/hasheous-lib/Classes/Metadata/IGDB/IMetadata_IGDB.cs
index e4c184ec..3cce2cc9 100644
--- a/hasheous-lib/Classes/Metadata/IGDB/IMetadata_IGDB.cs
+++ b/hasheous-lib/Classes/Metadata/IGDB/IMetadata_IGDB.cs
@@ -1,5 +1,6 @@
+using Classes;
using hasheous_server.Classes.Metadata.IGDB;
namespace hasheous_server.Classes.MetadataLib
@@ -13,6 +14,15 @@ public class MetadataIGDB : IMetadata
///
public Metadata.Communications.MetadataSources MetadataSource => Metadata.Communications.MetadataSources.IGDB;
+ ///
+ public bool Enabled
+ {
+ get
+ {
+ return !String.IsNullOrEmpty(Config.IGDB.ClientId) && !String.IsNullOrEmpty(Config.IGDB.Secret);
+ }
+ }
+
///
public async Task FindMatchItemAsync(hasheous_server.Models.DataObjectItem item, List searchCandidates, Dictionary? options = null)
{
@@ -27,10 +37,32 @@ public class MetadataIGDB : IMetadata
switch (item.ObjectType)
{
case DataObjects.DataObjectType.Company:
- DataObjectSearchResults = await dataObjects.GetDataObject(Metadata.Communications.MetadataSources.IGDB, IGDB.IGDBClient.Endpoints.Companies, "fields *;", "where name ~ *\"" + item.Name + "\"");
+ foreach (string candidate in searchCandidates)
+ {
+ DataObjectSearchResults = await dataObjects.GetDataObject(Metadata.Communications.MetadataSources.IGDB, IGDB.IGDBClient.Endpoints.Companies, "fields *;", "where name ~ *\"" + candidate + "\"");
+ if (DataObjectSearchResults != null && DataObjectSearchResults.MatchMethod != BackgroundMetadataMatcher.BackgroundMetadataMatcher.MatchMethod.NoMatch)
+ {
+ break;
+ }
+ }
+ if (DataObjectSearchResults == null || DataObjectSearchResults.MatchMethod == BackgroundMetadataMatcher.BackgroundMetadataMatcher.MatchMethod.NoMatch)
+ {
+ DataObjectSearchResults = await dataObjects.GetDataObject(Metadata.Communications.MetadataSources.IGDB, IGDB.IGDBClient.Endpoints.Companies, "fields *;", "where name ~ *\"" + item.Name + "\"");
+ }
break;
case DataObjects.DataObjectType.Platform:
- DataObjectSearchResults = await dataObjects.GetDataObject(Metadata.Communications.MetadataSources.IGDB, IGDB.IGDBClient.Endpoints.Platforms, "fields *;", "where name ~ *\"" + item.Name + "\"");
+ foreach (string candidate in searchCandidates)
+ {
+ DataObjectSearchResults = await dataObjects.GetDataObject(Metadata.Communications.MetadataSources.IGDB, IGDB.IGDBClient.Endpoints.Platforms, "fields *;", "where name ~ *\"" + candidate + "\"");
+ if (DataObjectSearchResults != null && DataObjectSearchResults.MatchMethod != BackgroundMetadataMatcher.BackgroundMetadataMatcher.MatchMethod.NoMatch)
+ {
+ break;
+ }
+ }
+ if (DataObjectSearchResults == null || DataObjectSearchResults.MatchMethod == BackgroundMetadataMatcher.BackgroundMetadataMatcher.MatchMethod.NoMatch)
+ {
+ DataObjectSearchResults = await dataObjects.GetDataObject(Metadata.Communications.MetadataSources.IGDB, IGDB.IGDBClient.Endpoints.Platforms, "fields *;", "where name ~ *\"" + item.Name + "\"");
+ }
break;
case DataObjects.DataObjectType.Game:
bool searchComplete = false;
diff --git a/hasheous-lib/Classes/Metadata/IGDB/TableBuilder.cs b/hasheous-lib/Classes/Metadata/IGDB/TableBuilder.cs
deleted file mode 100644
index 3a5c6068..00000000
--- a/hasheous-lib/Classes/Metadata/IGDB/TableBuilder.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-using System.Reflection;
-using Classes;
-using Classes.Metadata;
-using IGDB.Models;
-
-namespace Classes.Metadata.Utility
-{
- public class TableBuilder
- {
- public static void BuildTables()
- {
- BuildTableFromType(typeof(AgeRating));
- BuildTableFromType(typeof(AgeRatingCategory));
- BuildTableFromType(typeof(AgeRatingContentDescriptionV2));
- BuildTableFromType(typeof(AgeRatingOrganization));
- BuildTableFromType(typeof(AlternativeName));
- BuildTableFromType(typeof(Artwork));
- BuildTableFromType(typeof(Character));
- BuildTableFromType(typeof(CharacterGender));
- BuildTableFromType(typeof(CharacterMugShot));
- BuildTableFromType(typeof(CharacterSpecies));
- BuildTableFromType(typeof(Collection));
- BuildTableFromType(typeof(CollectionMembership));
- BuildTableFromType(typeof(CollectionMembershipType));
- BuildTableFromType(typeof(CollectionRelation));
- BuildTableFromType(typeof(CollectionRelationType));
- BuildTableFromType(typeof(CollectionType));
- BuildTableFromType(typeof(Company));
- BuildTableFromType(typeof(CompanyLogo));
- BuildTableFromType(typeof(CompanyStatus));
- BuildTableFromType(typeof(CompanyWebsite));
- BuildTableFromType(typeof(Cover));
- BuildTableFromType(typeof(Event));
- BuildTableFromType(typeof(EventLogo));
- BuildTableFromType(typeof(EventNetwork));
- BuildTableFromType(typeof(ExternalGame));
- BuildTableFromType(typeof(ExternalGameSource));
- BuildTableFromType(typeof(Franchise));
- BuildTableFromType(typeof(Game));
- BuildTableFromType(typeof(GameEngine));
- BuildTableFromType(typeof(GameEngineLogo));
- BuildTableFromType(typeof(GameLocalization));
- BuildTableFromType(typeof(GameMode));
- BuildTableFromType(typeof(GameReleaseFormat));
- BuildTableFromType(typeof(GameStatus));
- BuildTableFromType(typeof(GameTimeToBeat));
- BuildTableFromType(typeof(GameType));
- BuildTableFromType(typeof(GameVersion));
- BuildTableFromType(typeof(GameVersionFeature));
- BuildTableFromType(typeof(GameVersionFeatureValue));
- BuildTableFromType(typeof(GameVideo));
- BuildTableFromType(typeof(Genre));
- BuildTableFromType(typeof(InvolvedCompany));
- BuildTableFromType(typeof(Keyword));
- BuildTableFromType(typeof(Language));
- BuildTableFromType(typeof(LanguageSupport));
- BuildTableFromType(typeof(LanguageSupportType));
- BuildTableFromType(typeof(MultiplayerMode));
- BuildTableFromType(typeof(NetworkType));
- BuildTableFromType(typeof(Platform));
- BuildTableFromType(typeof(PlatformFamily));
- BuildTableFromType(typeof(PlatformLogo));
- BuildTableFromType(typeof(PlatformVersion));
- BuildTableFromType(typeof(PlatformVersionCompany));
- BuildTableFromType(typeof(PlatformVersionReleaseDate));
- BuildTableFromType(typeof(PlatformWebsite));
- BuildTableFromType(typeof(PlayerPerspective));
- BuildTableFromType(typeof(PopularityPrimitive));
- BuildTableFromType(typeof(PopularityType));
- BuildTableFromType(typeof(Region));
- BuildTableFromType(typeof(ReleaseDate));
- BuildTableFromType(typeof(ReleaseDateRegion));
- BuildTableFromType(typeof(ReleaseDateStatus));
- BuildTableFromType(typeof(Screenshot));
- BuildTableFromType(typeof(Theme));
- BuildTableFromType(typeof(Website));
- BuildTableFromType(typeof(WebsiteType));
- }
-
- ///
- /// Builds a table from a type definition, or modifies an existing table.
- /// This is used to create or update tables in the database based on the properties of a class.
- /// Updates are limited to adding new columns, as the table structure should not change once created.
- /// If the table already exists, it will only add new columns that are not already present.
- /// This is useful for maintaining a consistent schema across different versions of the application.
- /// The method is generic and can be used with any type that has properties that can be mapped to database columns.
- /// The method does not return any value, but it will throw an exception if there is an error during the table creation or modification process.
- ///
- /// The type definition of the class for which the table should be built.
- public static void BuildTableFromType(Type type)
- {
- Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
-
- db.BuildTableFromType("hasheous", Storage.TablePrefix.IGDB.ToString(), type);
- }
- }
-}
\ No newline at end of file
diff --git a/hasheous-lib/Classes/Metadata/IMetadata.cs b/hasheous-lib/Classes/Metadata/IMetadata.cs
index 780faaff..a5ff5eed 100644
--- a/hasheous-lib/Classes/Metadata/IMetadata.cs
+++ b/hasheous-lib/Classes/Metadata/IMetadata.cs
@@ -1,5 +1,28 @@
namespace hasheous_server.Classes.MetadataLib
{
+ ///
+ /// Thrown by an implementation when the upstream API rate limit has been
+ /// reached or approached, and the caller should defer the search until .
+ ///
+ public class MetadataRateLimitException : Exception
+ {
+ ///
+ /// The earliest UTC time at which the caller should retry the operation.
+ ///
+ public DateTime RetryAfter { get; }
+
+ ///
+ /// Initialises a new instance with an optional retry-after hint.
+ ///
+ /// Human-readable description of the limit that was hit.
+ /// Earliest UTC time to retry; defaults to 1 hour from now if not supplied.
+ public MetadataRateLimitException(string message, DateTime? retryAfter = null)
+ : base(message)
+ {
+ RetryAfter = retryAfter ?? DateTime.UtcNow.AddHours(1);
+ }
+ }
+
///
/// Provides metadata lookup capabilities and identifies the metadata source.
/// Implementations can locate matching DataObjects.MatchItem instances for a given DataObjectType.
@@ -11,6 +34,11 @@ public interface IMetadata
///
public Metadata.Communications.MetadataSources MetadataSource { get; }
+ ///
+ /// Indicates whether this metadata provider is currently enabled and should be used for lookups.
+ ///
+ public bool Enabled { get; }
+
///
/// Finds a matching DataObjects.MatchItem for the specified data object type.
///
diff --git a/hasheous-lib/Classes/Metadata/RetroAchievements/IMetadata_RetroAchievements.cs b/hasheous-lib/Classes/Metadata/RetroAchievements/IMetadata_RetroAchievements.cs
index d4f4e436..4e178519 100644
--- a/hasheous-lib/Classes/Metadata/RetroAchievements/IMetadata_RetroAchievements.cs
+++ b/hasheous-lib/Classes/Metadata/RetroAchievements/IMetadata_RetroAchievements.cs
@@ -10,6 +10,15 @@ public class MetadataRetroAchievements : IMetadata
///
public Metadata.Communications.MetadataSources MetadataSource => Metadata.Communications.MetadataSources.RetroAchievements;
+ ///
+ public bool Enabled
+ {
+ get
+ {
+ return !String.IsNullOrEmpty(Config.RetroAchievements.APIKey);
+ }
+ }
+
///
public async Task FindMatchItemAsync(hasheous_server.Models.DataObjectItem item, List searchCandidates, Dictionary? options = null)
{
diff --git a/hasheous-lib/Classes/Metadata/ScreenScraper/IMetadata_ScreenScraper.cs b/hasheous-lib/Classes/Metadata/ScreenScraper/IMetadata_ScreenScraper.cs
new file mode 100644
index 00000000..61c09279
--- /dev/null
+++ b/hasheous-lib/Classes/Metadata/ScreenScraper/IMetadata_ScreenScraper.cs
@@ -0,0 +1,1153 @@
+using System.Data;
+using System.Net;
+using Classes;
+using Classes.Metadata;
+using hasheous_server.Classes.Metadata;
+using hasheous_server.Models;
+using HasheousClient.WebApp;
+
+namespace hasheous_server.Classes.MetadataLib
+{
+ ///
+ /// ScreenScraper metadata provider that implements to locate and return
+ /// metadata matches from the ScreenScraper source for data objects (companies, platforms, games).
+ ///
+ public class MetadataScreenScraper : IMetadata
+ {
+ ///
+ /// Tracks the last time user information was fetched from the ScreenScraper API to manage API rate limits.
+ ///
+ private static DateTime lastUserInfoFetchTime = DateTime.UtcNow.AddHours(-10);
+
+ ///
+ /// Defines the interval in minutes for fetching user information from the ScreenScraper API to check API rate limits.
+ ///
+ private static int userInfoFetchIntervalMinutes = 60;
+
+ ///
+ /// Tracks the number of API calls made to the ScreenScraper API to manage API rate limits. Resets to 0 when user information is fetched to check the remaining API calls and reset time. This counter is incremented with each API call made to the ScreenScraper API, and it helps ensure that the application does not exceed the daily limit of 10000 calls.
+ ///
+ private static int apiCallCount = 0;
+
+ ///
+ /// Tracks the number of failed match API calls made to the ScreenScraper API to manage API rate limits. Resets to 0 when user information is fetched to check the remaining API calls and reset time. This counter is incremented with each failed match API call (e.g., when a ROM hash lookup returns a 404 Not Found) made to the ScreenScraper API, and it helps ensure that the application does not exceed the daily limit of 2000 failed calls, which could lead to temporary blocking of the API access. By tracking both successful and failed API calls, the application can better manage its usage of the ScreenScraper API and avoid hitting rate limits.
+ ///
+ private static int apiFailedCallCount = 0;
+
+ ///
+ /// Defines the threshold percentage of API calls used to stop making API calls.
+ ///
+ private static int maxApiCallsThresholdPercentage = 90;
+
+ ///
+ /// Defines the delay in minutes to wait before retrying API calls after exceeding the API call limit. This delay is used when the application detects that it has approached or exceeded the API call limits based on the user information fetched from the ScreenScraper API. By implementing this delay, the application can avoid making further API calls that would be rejected due to rate limits and can instead wait until the limits are reset before resuming API interactions.
+ ///
+ private static int exceededAPICallDelayMinutes = 60;
+
+ ///
+ /// Stores the user information retrieved from the ScreenScraper API, including the number of API calls used and the time until the next reset. This information is used to manage API rate limits by tracking how many calls have been made and when the limits will reset. The user information is fetched at regular intervals defined by to ensure that the application has up-to-date information on API usage and can avoid exceeding the limits.
+ ///
+ private static ssUser? userItem;
+
+ ///
+ public Communications.MetadataSources MetadataSource => Communications.MetadataSources.ScreenScraper;
+
+ ///
+ public bool Enabled
+ {
+ get
+ {
+ return !String.IsNullOrEmpty(Config.ScreenScraperConfiguration.ClientId) && !String.IsNullOrEmpty(Config.ScreenScraperConfiguration.Secret) && !String.IsNullOrEmpty(Config.ScreenScraperConfiguration.DevClientId) && !String.IsNullOrEmpty(Config.ScreenScraperConfiguration.DevSecret);
+ }
+ }
+
+ ///
+ /// Connects to the ScreenScraper API and searches for matches based on the ROM hashes in the provided . The value is ignored for ScreenScraper game metadata matching since it relies on ROM hash matching rather than name-based searching. The parameter is also ignored for ScreenScraper metadata matching since no additional options are needed for ROM hash searching.
+ ///
+ /// The data object item containing ROM hashes to search for.
+ /// Ignored for ScreenScraper game metadata matching.
+ /// Ignored for ScreenScraper game metadata matching.
+ /// A task representing the asynchronous operation, with a result containing the metadata match.
+ ///
+ public async Task FindMatchItemAsync(DataObjectItem item, List searchCandidates, Dictionary? options = null)
+ {
+ hasheous_server.Classes.DataObjects.MatchItem? DataObjectSearchResults = new hasheous_server.Classes.DataObjects.MatchItem
+ {
+ MatchMethod = BackgroundMetadataMatcher.BackgroundMetadataMatcher.MatchMethod.NoMatch,
+ MetadataId = ""
+ };
+
+ // check if we have the user info
+ if (userItem == null || DateTime.UtcNow - lastUserInfoFetchTime > TimeSpan.FromMinutes(userInfoFetchIntervalMinutes))
+ {
+ // fetch user info from ScreenScraper API
+ try
+ {
+ UserItem fullUserItem = await HttpHelper.Get(UserItem.Endpoint());
+ userItem = fullUserItem.response.ssuser; // update the user information with the latest data from the API response
+ lastUserInfoFetchTime = DateTime.UtcNow;
+ apiCallCount = 0; // reset API call count after fetching user info
+ }
+ catch (Exception ex)
+ {
+ // Unable to determine remaining quota - treat as rate-limited to be safe.
+ Logging.Log(Logging.LogType.Critical, "ScreenScraper", $"Failed to fetch user info from ScreenScraper API: {ex.Message}");
+
+ throw new MetadataRateLimitException(
+ $"ScreenScraper: could not fetch user info to check API limits: {ex.Message}",
+ DateTime.UtcNow.AddMinutes(userInfoFetchIntervalMinutes));
+ }
+ }
+
+ // check if we are approaching the API call limit
+ if (userItem != null)
+ {
+ int maxRequestsPerDay = int.TryParse(userItem.maxrequestsperday, out int maxReq) ? maxReq : 10000; // default to 10000
+ int requestsToday = int.TryParse(userItem.requeststoday, out int reqToday) ? reqToday : 0;
+ int maxAllowedRequests = (int)(maxRequestsPerDay * (maxApiCallsThresholdPercentage / 100.0));
+ int requestsTotal = requestsToday + apiCallCount;
+ if (requestsTotal >= maxAllowedRequests)
+ {
+ Logging.Log(Logging.LogType.Warning, "ScreenScraper", $"Approaching API call limit: {requestsTotal}/{maxAllowedRequests} calls used. Stopping API calls to avoid exceeding the limit.");
+ throw new MetadataRateLimitException(
+ $"ScreenScraper: daily request limit approached ({requestsTotal}/{maxAllowedRequests}).", DateTime.UtcNow.AddMinutes(exceededAPICallDelayMinutes));
+ }
+
+ int maxFailedRequestsPerDay = int.TryParse(userItem.maxrequestskoperday, out int maxFailedReq) ? maxFailedReq : 2000; // default to 2000
+ int failedRequestsToday = int.TryParse(userItem.requestskotoday, out int failedReqToday) ? failedReqToday : 0;
+ int maxAllowedFailedRequests = (int)(maxFailedRequestsPerDay * (maxApiCallsThresholdPercentage / 100.0));
+ int failedRequestsTotal = failedRequestsToday + apiFailedCallCount;
+ if (failedRequestsTotal >= maxAllowedFailedRequests)
+ {
+ Logging.Log(Logging.LogType.Warning, "ScreenScraper", $"Approaching failed API call limit: {failedRequestsTotal}/{maxAllowedFailedRequests} failed calls used. Stopping API calls to avoid exceeding the limit.");
+ throw new MetadataRateLimitException(
+ $"ScreenScraper: daily failed-request limit approached ({failedRequestsTotal}/{maxAllowedFailedRequests}).", DateTime.UtcNow.AddMinutes(exceededAPICallDelayMinutes));
+ }
+ }
+
+ switch (item.ObjectType)
+ {
+ case DataObjects.DataObjectType.Game:
+ DataObjectSearchResults = await GetGameDataAsync(item);
+ break;
+ case DataObjects.DataObjectType.Platform:
+ DataObjectSearchResults = await GetPlatformAsync(item, searchCandidates);
+ break;
+ default:
+ // ScreenScraper metadata matching is only implemented for games and platforms, so return no match for other types
+ break;
+ }
+
+ return DataObjectSearchResults;
+ }
+
+ private async Task GetGameDataAsync(DataObjectItem item)
+ {
+ hasheous_server.Classes.DataObjects.MatchItem? DataObjectSearchResults = new hasheous_server.Classes.DataObjects.MatchItem
+ {
+ MatchMethod = BackgroundMetadataMatcher.BackgroundMetadataMatcher.MatchMethod.NoMatch,
+ MetadataId = ""
+ };
+
+ // get the first ROM hashs from the item to search for
+ if (item.Attributes?.Find(a => a.attributeName == AttributeItem.AttributeName.ROMs) != null)
+ {
+ List romItems = item.Attributes.Find(a => a.attributeName == AttributeItem.AttributeName.ROMs)?.Value as List ?? new List();
+
+ foreach (Signatures_Games_2.RomItem romItem in romItems)
+ {
+ // first check if we have the hash in our local cache mapping table to avoid unnecessary API calls
+ string sql = "SELECT GameId FROM Screenscraper_HashToGameMap WHERE Hash = @Hash AND HashType = @HashType LIMIT 1";
+ Dictionary parameters = new Dictionary();
+
+ string endpointUrl;
+ string hashUsed;
+ if (!string.IsNullOrEmpty(romItem.Md5))
+ {
+ endpointUrl = GameItem.Endpoint(sha1hash: romItem.Sha1);
+ hashUsed = romItem.Md5;
+ parameters["@Hash"] = romItem.Md5;
+ parameters["@HashType"] = "MD5";
+ }
+ else if (!string.IsNullOrEmpty(romItem.Sha1))
+ {
+ endpointUrl = GameItem.Endpoint(sha1hash: romItem.Sha1);
+ hashUsed = romItem.Sha1;
+ parameters["@Hash"] = romItem.Sha1;
+ parameters["@HashType"] = "SHA1";
+ }
+ else
+ {
+ // if we don't have a hash to search for, skip this ROM
+ continue;
+ }
+
+ // now check if the the hash is in our cache of failed hash lookups to avoid unnecessary API calls for hashes we know won't return results
+ string failedsql = "SELECT COUNT(1) FROM Screenscraper_FailedHashLookups WHERE Hash = @Hash AND HashType = @HashType";
+ DataTable failedLookupResult = await Config.database.ExecuteCMDAsync(failedsql, parameters);
+ if (failedLookupResult.Rows.Count > 0 && int.TryParse(failedLookupResult.Rows[0][0].ToString(), out int failedLookupCount) && failedLookupCount > 0)
+ {
+ // we have a failed lookup for this hash, skip the API call
+ continue;
+ }
+
+ try
+ {
+ // check the cache first to avoid unnecessary API calls
+ DataTable? cacheResult = await Config.database.ExecuteCMDAsync(sql, parameters);
+ if (cacheResult.Rows.Count > 0)
+ {
+ if (!String.IsNullOrEmpty(cacheResult.Rows[0]["GameId"].ToString()))
+ {
+ long gameId = long.Parse(cacheResult.Rows[0]["GameId"].ToString() ?? "0");
+ if (gameId > 0)
+ {
+ // we have a cached match, check if we have a copy of the metadata locally, and that it hasn't been updated in the last 30 days
+ string cacheFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Screenscraper, "games", gameId.ToString() + ".json");
+
+ if (File.Exists(cacheFilePath))
+ {
+ // check file info to ensure the file is less than 30 days old
+ FileInfo fileInfo = new FileInfo(cacheFilePath);
+ if (fileInfo.LastWriteTimeUtc > DateTime.UtcNow.AddDays(-30))
+ {
+ // we have a valid cached file, return the match
+ DataObjectSearchResults = new hasheous_server.Classes.DataObjects.MatchItem
+ {
+ MatchMethod = BackgroundMetadataMatcher.BackgroundMetadataMatcher.MatchMethod.Automatic,
+ MetadataId = gameId.ToString()
+ };
+
+ break; // exit the loop after the first successful match
+ }
+ else
+ {
+ // cached file is too old, delete it and continue to fetch fresh data
+ File.Delete(cacheFilePath);
+ }
+ }
+ }
+ }
+ }
+
+ // query the ScreenScraper API for the ROM hash
+ var response = await HttpHelper.Get(endpointUrl);
+ apiCallCount++; // increment API call count after making the call
+
+ if (response != null && response.response != null && response.response.jeu != null && response.response.jeu.id != null)
+ {
+ // capture the user information from the response to update our API usage tracking in case the user information has changed since we last fetched it
+ if (response.response.ssuser != null)
+ {
+ userItem = response.response.ssuser; // update the user information with the latest data from the API response
+ lastUserInfoFetchTime = DateTime.UtcNow; // update the last fetch time to now since we just got fresh user info from the API
+ apiFailedCallCount = 0; // reset failed API call count after getting a successful response which indicates we are not currently blocked by rate limits
+ }
+
+ // we have a match, return it
+ DataObjectSearchResults = new hasheous_server.Classes.DataObjects.MatchItem
+ {
+ MatchMethod = BackgroundMetadataMatcher.BackgroundMetadataMatcher.MatchMethod.Automatic,
+ MetadataId = response.response.jeu.id.ToString()
+ };
+
+ // now cache it
+ string cacheFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Screenscraper, "games", response.response.jeu.id.ToString() + ".json");
+ if (!File.Exists(cacheFilePath))
+ {
+ // ensure the directory exists
+ Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath) ?? string.Empty);
+
+ // save the response to the cache file
+ File.WriteAllText(cacheFilePath, Newtonsoft.Json.JsonConvert.SerializeObject(response.response.jeu));
+
+ // update mapping table
+ // delete existing maps
+ sql = "DELETE FROM Screenscraper_HashToGameMap WHERE GameId = @GameId";
+ _ = await Config.database.ExecuteCMDAsync(sql, new Dictionary { { "@GameId", response.response.jeu.id } });
+
+ // add new maps
+ foreach (var jeuRom in response.response.jeu.roms)
+ {
+ if (!String.IsNullOrEmpty(jeuRom.romcrc))
+ {
+ sql = "INSERT INTO Screenscraper_HashToGameMap (Hash, HashType, GameId) VALUES (@Hash, @HashType, @GameId)";
+ _ = await Config.database.ExecuteCMDAsync(sql, new Dictionary { { "@Hash", jeuRom.romcrc }, { "@HashType", "CRC32" }, { "@GameId", response.response.jeu.id } });
+ }
+
+ if (!String.IsNullOrEmpty(jeuRom.rommd5))
+ {
+ sql = "INSERT INTO Screenscraper_HashToGameMap (Hash, HashType, GameId) VALUES (@Hash, @HashType, @GameId)";
+ _ = await Config.database.ExecuteCMDAsync(sql, new Dictionary { { "@Hash", jeuRom.rommd5 }, { "@HashType", "MD5" }, { "@GameId", response.response.jeu.id } });
+ }
+
+ if (!String.IsNullOrEmpty(jeuRom.romsha1))
+ {
+ sql = "INSERT INTO Screenscraper_HashToGameMap (Hash, HashType, GameId) VALUES (@Hash, @HashType, @GameId)";
+ _ = await Config.database.ExecuteCMDAsync(sql, new Dictionary { { "@Hash", jeuRom.romsha1 }, { "@HashType", "SHA1" }, { "@GameId", response.response.jeu.id } });
+ }
+ }
+ }
+
+ break; // exit the loop after the first successful match
+ }
+ }
+ catch (HttpRequestException httpEx)
+ {
+ switch (httpEx.StatusCode)
+ {
+ case HttpStatusCode.NotFound:
+ // 404 Not Found means we don't have metadata for this ROM hash, so we can cache this result to avoid unnecessary API calls in the future
+ sql = "INSERT INTO Screenscraper_FailedHashLookups (Hash, HashType, LookupDate) VALUES (@Hash, @HashType, @LookupDate)";
+ await Config.database.ExecuteCMDAsync(sql, new Dictionary { { "@Hash", hashUsed }, { "@HashType", parameters["@HashType"] }, { "@LookupDate", DateTime.UtcNow } });
+ apiFailedCallCount++; // increment failed API call count since this is a failed lookup which counts against the failed call limit
+
+ Logging.Log(Logging.LogType.Information, "ScreenScraper", $"No metadata found for ROM hash {hashUsed}. Caching this result to avoid future API calls for this hash.");
+ break;
+ case HttpStatusCode.Unauthorized:
+ Logging.Log(Logging.LogType.Critical, "Screenscraper", "Unauthorized access to ScreenScraper API. Please check your API credentials and ensure they are valid.");
+ break;
+ default:
+ // log the error and continue with the next hash
+ Logging.Log(Logging.LogType.Critical, "ScreenScraper", $"HTTP error querying ScreenScraper API for ROM hash {hashUsed}: {httpEx.Message}");
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ // log the error and continue with the next hash
+ Logging.Log(Logging.LogType.Critical, "ScreenScraper", $"Error querying ScreenScraper API for ROM hash {hashUsed}: {ex.Message}");
+ }
+ }
+ }
+
+ return DataObjectSearchResults;
+ }
+
+ private async Task GetPlatformAsync(DataObjectItem item, List SearchCandidates)
+ {
+ List? response = null;
+
+ // check if the cache file exists and is valid - file must be less than 30 days old to be valid
+ string cacheFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Screenscraper, "platforms.json");
+ if (File.Exists(cacheFilePath))
+ {
+ var platformFileInfo = new FileInfo(cacheFilePath);
+ if (platformFileInfo.LastWriteTimeUtc > DateTime.UtcNow.AddDays(-30))
+ {
+ // cache file is valid, read from it
+ string cachedData = File.ReadAllText(cacheFilePath);
+ response = Newtonsoft.Json.JsonConvert.DeserializeObject>(cachedData);
+ }
+ else
+ {
+ // cache file is too old, delete it
+ File.Delete(cacheFilePath);
+ }
+ }
+
+ // if we don't have a valid cache, fetch from the API
+ if (response == null)
+ {
+ try
+ {
+ var apiResponse = await HttpHelper.Get(PlatformItem.Endpoint());
+ apiCallCount++; // increment API call count after making the call
+
+ if (apiResponse != null && apiResponse.response != null && apiResponse.response.systemes != null)
+ {
+ response = apiResponse.response.systemes;
+
+ // cache the response for future use
+ Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath) ?? string.Empty);
+ File.WriteAllText(cacheFilePath, Newtonsoft.Json.JsonConvert.SerializeObject(response));
+ }
+ }
+ catch (Exception ex)
+ {
+ // log the error and return no match
+ Logging.Log(Logging.LogType.Critical, "ScreenScraper", $"Failed to fetch platforms from ScreenScraper API: {ex.Message}");
+ return new hasheous_server.Classes.DataObjects.MatchItem
+ {
+ MatchMethod = BackgroundMetadataMatcher.BackgroundMetadataMatcher.MatchMethod.NoMatch,
+ MetadataId = ""
+ };
+ }
+ }
+
+ // search the platform response for the provided platform name candidates and return a match if found
+ foreach (var platform in response)
+ {
+ foreach (var candidate in SearchCandidates)
+ {
+ // check the regional names
+ if (!string.IsNullOrEmpty(candidate) && platform.noms != null && platform.noms.Any(n => n.Value != null && n.Value.Equals(candidate, StringComparison.OrdinalIgnoreCase)))
+ {
+ return new hasheous_server.Classes.DataObjects.MatchItem
+ {
+ MatchMethod = BackgroundMetadataMatcher.BackgroundMetadataMatcher.MatchMethod.Automatic,
+ MetadataId = platform.id.ToString()
+ };
+ }
+
+ // check noms_commun, which is a comma separated list of common names for the platform
+ if (platform.noms.ContainsKey("noms_commun") && platform.noms["noms_commun"] != null)
+ {
+ var commonNames = platform.noms["noms_commun"].Split(',').Select(n => n.Trim());
+ if (commonNames.Any(n => n.Equals(candidate, StringComparison.OrdinalIgnoreCase)))
+ {
+ return new hasheous_server.Classes.DataObjects.MatchItem
+ {
+ MatchMethod = BackgroundMetadataMatcher.BackgroundMetadataMatcher.MatchMethod.Automatic,
+ MetadataId = platform.id.ToString()
+ };
+ }
+ }
+ }
+ }
+
+ return new hasheous_server.Classes.DataObjects.MatchItem
+ {
+ MatchMethod = BackgroundMetadataMatcher.BackgroundMetadataMatcher.MatchMethod.NoMatch,
+ MetadataId = ""
+ };
+ }
+
+ ///
+ /// Standard header class for ScreenScraper API results
+ ///
+ public class ssHeader
+ {
+ public string? APIversion { get; set; }
+ public DateTime? dateTime { get; set; }
+ public string? commandRequested { get; set; }
+ public bool? success { get; set; }
+ public string? error { get; set; }
+ }
+
+ ///
+ /// Provides information about the ScreenScraper API servers
+ ///
+ public class ssServeurs
+ {
+ ///
+ /// CPU usage of server 1 (average of the last 5 minutes)
+ ///
+ public string? cpu1 { get; set; }
+ ///
+ /// CPU usage of server 2 (average of the last 5 minutes)
+ ///
+ public string? cpu2 { get; set; }
+ ///
+ /// CPU usage of server 3 (average of the last 5 minutes)
+ ///
+ public string? cpu3 { get; set; }
+ ///
+ /// CPU usage of server 4 (average of the last 5 minutes)
+ ///
+ public string? cpu4 { get; set; }
+ ///
+ /// Number of accesses to the API since the last minute
+ ///
+ public string? threadsmin { get; set; }
+ ///
+ /// Number of scrapers using the api since the last minute
+ ///
+ public string? nbscrapeurs { get; set; }
+ ///
+ /// Number of accesses to the API in the current day (GMT+1)
+ ///
+ public string? apiacces { get; set; }
+ ///
+ /// Closed API for anonymous (unregistered or unidentified) (0: open / 1: closed)
+ ///
+ public string? closefornomember { get; set; }
+ ///
+ /// Closed API for non-participating members (no validated proposal) (0: open / 1: closed)
+ ///
+ public string? closeforleecher { get; set; }
+ ///
+ /// Maximum number of threads opened for anonymous (unregistered or unidentified) at the same time by the api
+ ///
+ public string? maxthreadfornonmember { get; set; }
+ ///
+ /// Current number of threads opened by anonymous (unregistered or unidentified) at the same time by the api
+ ///
+ public string? threadfornonmember { get; set; }
+ ///
+ /// Maximum number of threads open for members at the same time by the api
+ ///
+ public string? maxthreadformember { get; set; }
+ ///
+ /// Current number of threads opened by members at the same time by the api
+ ///
+ public string? threadformember { get; set; }
+ }
+
+ ///
+ /// Provides information about the ScreenScraper API user, including API usage and rate limit information. This class is used to track how many API calls have been made and when the limits will reset to manage API rate limits effectively.
+ ///
+ public class ssUser
+ {
+ ///
+ /// username of the user on ScreenScraper
+ ///
+ public string? id { get; set; }
+ ///
+ /// user's digital identifier on ScreenScraper
+ ///
+ public string? numid { get; set; }
+ ///
+ /// user level on ScreenScraper
+ ///
+ public string? niveau { get; set; }
+ ///
+ /// level of financial contribution on ScreenScraper (2 = 1 Additional Thread / 3 and + = 5 Additional Threads)
+ ///
+ public string? contribution { get; set; }
+ ///
+ /// Counter of valid contributions (system media) proposed by the user
+ ///
+ public string? uploadsysteme { get; set; }
+ ///
+ /// Valid contribution counter (text info) proposed by the user
+ ///
+ public string? uploadinfos { get; set; }
+ ///
+ /// Valid contributions counter (association of roms) proposed by the user
+ ///
+ public string? romasso { get; set; }
+ ///
+ /// Counter of valid contributions (game media) proposed by the user
+ ///
+ public string? uploadmedia { get; set; }
+ ///
+ /// Number of user proposals validated by a moderator
+ ///
+ public string? propositionok { get; set; }
+ ///
+ /// Number of user proposals rejected by a moderator
+ ///
+ public string? propositionko { get; set; }
+ ///
+ /// Percentage of refusal of the user's proposal
+ ///
+ public string? quotarefu { get; set; }
+ ///
+ /// Number of threads allowed for the user (also indicated for non-registered)
+ ///
+ public string? maxthreads { get; set; }
+ ///
+ /// Download speed (in KB/s) allowed for the user (also indicated for non-registered)
+ ///
+ public string? maxdownloadspeed { get; set; }
+ ///
+ /// Total number of calls to the api during the day in short GMT+1 (resets at 0:00 GMT+1)
+ ///
+ public string? requeststoday { get; set; }
+ ///
+ /// Number of calls to the api with negative feedback (rom/game not found) during the day in short GMT+1 (resets at 0:00 GMT+1)
+ ///
+ public string? requestskotoday { get; set; }
+ ///
+ /// Maximum number of API calls allowed per minute for the user
+ ///
+ public string? maxrequestspermin { get; set; }
+ ///
+ /// Maximum number of calls to the API allowed per day for the user
+ ///
+ public string? maxrequestsperday { get; set; }
+ ///
+ /// Number of calls to the api with a negative feedback (rom/game not found) maximum allowed per day for the user
+ ///
+ public string? maxrequestskoperday { get; set; }
+ ///
+ /// number of user visits to ScreenScraper
+ ///
+ public string? visites { get; set; }
+ ///
+ /// date of the user's last visit to ScreenScraper (format: yyyy-mm-dd hh:mm:ss)
+ ///
+ public string? datedernierevisite { get; set; }
+ ///
+ /// favorite region of user visits on ScreenScraper (france,europe,usa,japon)
+ ///
+ public string? favregion { get; set; }
+ }
+
+ ///
+ /// Represents a regional text item for the ScreenScraper API, containing information about the region and the associated text. This class is used to deserialize regional text data from the ScreenScraper API responses, allowing for structured access to localized information based on different regions. The region property indicates the specific region (e.g., France, Europe, USA, Japan) associated with the text, while the text property contains the localized information relevant to that region.
+ ///
+ public class ssRegionalText
+ {
+ ///
+ /// Region associated with the text, such as France, Europe, USA, or Japan. This property is used to identify the specific region for which the text information is relevant, allowing for localized metadata retrieval based on regional preferences and differences in game releases or information.
+ ///
+ public string? region { get; set; }
+ ///
+ /// Text associated with the region, containing localized information relevant to that region.
+ ///
+ public string? text { get; set; }
+ }
+
+ ///
+ /// Represents a text item for the ScreenScraper API, containing an identifier and the associated text. This class is used to deserialize text data from the ScreenScraper API responses, allowing for structured access to various pieces of information based on their identifiers. The id property serves as a unique identifier for the specific piece of information, while the text property contains the actual information or description associated with that identifier. This structure allows for flexible handling of different types of text information returned by the ScreenScraper API,
+ ///
+ public class ssTextId
+ {
+ ///
+ /// Identifier for the text item, which can be used to categorize or reference specific pieces of information returned by the ScreenScraper API. This identifier allows for structured access to different types of text information, enabling the application to handle various metadata fields effectively based on their unique IDs.
+ ///
+ public string? id { get; set; }
+ ///
+ /// Text associated with the identifier, containing the actual information or description relevant to that identifier.
+ ///
+ public string? text { get; set; }
+ }
+
+ ///
+ /// Represents a language-specific text item for the ScreenScraper API, containing the language code and the associated text. This class is used to deserialize language-specific text data from the ScreenScraper API responses, allowing for structured access to localized information based on different languages. The langue property indicates the specific language (e.g., "en" for English, "fr" for French) associated with the text, while the text property contains the localized information relevant to that language. This structure enables the application to handle multilingual metadata effectively based on the language preferences of users or regional differences in game information.
+ ///
+ public class ssLanguageText
+ {
+ ///
+ /// Language code associated with the text, such as "en" for English or "fr" for French. This property is used to identify the specific language for which the text information is relevant, allowing for localized metadata retrieval based on language preferences and differences in game releases or information across different languages.
+ ///
+ public string? langue { get; set; }
+ ///
+ /// Text associated with the language code, containing the localized information relevant to that language.
+ ///
+ public string? text { get; set; }
+ }
+
+ ///
+ /// Represents a game classification item for the ScreenScraper API, containing the type of classification and the associated text. This class is used to deserialize game classification data from the ScreenScraper API responses, allowing for structured access to different classifications or categories associated with a game. The type property indicates the specific type of classification (e.g., genre, theme, etc.), while the text property contains the information or description relevant to that classification type. This structure enables the application to handle various classifications of games effectively based on the information returned by the ScreenScraper API.
+ ///
+ public class ssGameClassification
+ {
+ ///
+ /// Type of classification for the game, such as genre, theme, or other categories used by the ScreenScraper API to classify games. This property allows for structured access to different classifications associated with a game, enabling the application to organize and present metadata based on these classifications effectively.
+ ///
+ public string? type { get; set; }
+ ///
+ /// Text associated with the classification type, containing the information or description relevant to that classification.
+ ///
+ public string? text { get; set; }
+ }
+
+ ///
+ /// Represents a game item for the ScreenScraper API, containing various properties such as ID, ROM ID, names in different regions, and other metadata fields. This class is used to deserialize game data from the ScreenScraper API responses, allowing for structured access to detailed information about games based on their ROM hashes or IDs. The properties include identifiers, names in different regions, developer and publisher information, player counts, ratings, and classifications, providing a comprehensive representation of a game as returned by the ScreenScraper API.
+ ///
+ public class ssGameDate
+ {
+ ///
+ /// Region associated with the release date, such as France, Europe, USA, or Japan. This property is used to identify the specific region for which the release date information is relevant, allowing for localized metadata retrieval based on regional differences in game release dates.
+ ///
+ public string? region { get; set; }
+ ///
+ /// Release date of the game for the associated region, providing information about when the game was released in that specific region. This property allows for structured access to release date information based on regional differences, enabling the application to present accurate metadata about game releases across different regions as returned by the ScreenScraper API.
+ ///
+ public string? date { get; set; }
+ }
+
+ ///
+ /// Represents a game genre item for the ScreenScraper API, containing properties such as ID, name, and parent-child relationships between genres. This class is used to deserialize game genre data from the ScreenScraper API responses, allowing for structured access to genre information associated with games. The properties include identifiers, names in different languages, and relationships between genres, providing a comprehensive representation of game genres as returned by the ScreenScraper API.
+ ///
+ public class ssGameGenre
+ {
+ ///
+ /// ID of the genre, serving as a unique identifier for the genre in the ScreenScraper API. This property allows for structured access to genre information based on its unique ID, enabling the application to reference and organize genres effectively based on the data returned by the ScreenScraper API.
+ ///
+ public string? id { get; set; }
+ ///
+ /// Short name of the genre, providing a concise identifier for the genre. This property is used to access a brief name for the genre, which can be useful for display purposes or when referencing genres in a more compact form based on the data returned by the ScreenScraper API.
+ ///
+ public string? nomcourt { get; set; }
+ ///
+ /// Indicates whether this genre is the main genre for a game. This property can be used to identify the primary genre associated with a game, allowing for structured access to genre information based on its significance or relevance to the game as returned by the ScreenScraper API.
+ ///
+ public string? principale { get; set; }
+ ///
+ /// ID of the parent genre, if applicable, indicating a hierarchical relationship between genres. This property allows for structured access to genre information based on parent-child relationships, enabling the application to organize genres effectively based on their relationships as returned by the ScreenScraper API.
+ ///
+ public string? parentid { get; set; }
+ ///
+ /// List of names for the genre in different languages, providing localized information about the genre based on language preferences. This property allows for structured access to genre names in various languages, enabling the application to present genre information effectively based on the language preferences of users or regional differences in game information as returned by the ScreenScraper API.
+ ///
+ public List? noms { get; set; }
+ }
+
+ ///
+ /// Represents a game mode item for the ScreenScraper API, containing properties such as ID, name, and parent-child relationships between game modes. This class is used to deserialize game mode data from the ScreenScraper API responses, allowing for structured access to game mode information associated with games. The properties include identifiers, names in different languages, and relationships between game modes, providing a comprehensive representation of game modes as returned by the ScreenScraper API.
+ ///
+ public class ssGameMode
+ {
+ ///
+ /// ID of the game mode, serving as a unique identifier for the game mode in the ScreenScraper API. This property allows for structured access to game mode information based on its unique ID, enabling the application to reference and organize game modes effectively based on the data returned by the ScreenScraper API.
+ ///
+ public string? id { get; set; }
+ ///
+ /// Short name of the game mode, providing a concise identifier for the game mode. This property is used to access a brief name for the game mode, which can be useful for display purposes or when referencing game modes in a more compact form based on the data returned by the ScreenScraper API.
+ ///
+ public string? nomcourt { get; set; }
+ ///
+ /// Indicates whether this game mode is the main mode for a game. This property can be used to identify the primary game mode associated with a game, allowing for structured access to game mode information based on its significance or relevance to the game as returned by the ScreenScraper API.
+ ///
+ public string? principale { get; set; }
+ ///
+ /// ID of the parent game mode, if applicable, indicating a hierarchical relationship between game modes. This property allows for structured access to game mode information based on parent-child relationships, enabling the application to organize game modes effectively based on their relationships as returned by the ScreenScraper API.
+ ///
+ public string? parentid { get; set; }
+ ///
+ /// List of names for the game mode in different languages, providing localized information about the game mode based on language preferences. This property allows for structured access to game mode names in various languages, enabling the application to present game mode information effectively based on the language preferences of users or regional differences in game information as returned by the ScreenScraper API.
+ ///
+ public List? noms { get; set; }
+ }
+
+ ///
+ /// Represents a game franchise item for the ScreenScraper API, containing properties such as ID, name, and parent-child relationships between franchises. This class is used to deserialize game franchise data from the ScreenScraper API responses, allowing for structured access to game franchise information associated with games. The properties include identifiers, names in different languages, and relationships between franchises, providing a comprehensive representation of game franchises as returned by the ScreenScraper API.
+ ///
+ public class ssGameFranchise
+ {
+ ///
+ /// ID of the franchise, serving as a unique identifier for the franchise in the ScreenScraper API. This property allows for structured access to franchise information based on its unique ID, enabling the application to reference and organize franchises effectively based on the data returned by the ScreenScraper API.
+ ///
+ public string? id { get; set; }
+ ///
+ /// Short name of the franchise, providing a concise identifier for the franchise. This property is used to access a brief name for the franchise, which can be useful for display purposes or when referencing franchises in a more compact form based on the data returned by the ScreenScraper API.
+ ///
+ public string? nomcourt { get; set; }
+ ///
+ /// Indicates whether this franchise is the main franchise for a game. This property can be used to identify the primary franchise associated with a game, allowing for structured access to franchise information based on its significance or relevance to the game as returned by the ScreenScraper API.
+ ///
+ public string? principale { get; set; }
+ ///
+ /// ID of the parent franchise, if applicable, indicating a hierarchical relationship between franchises. This property allows for structured access to franchise information based on parent-child relationships, enabling the application to organize franchises effectively based on their relationships as returned by the ScreenScraper API.
+ ///
+ public string? parentid { get; set; }
+ ///
+ /// List of names for the franchise in different languages, providing localized representations of the franchise name. This property allows for structured access to franchise names based on language, enabling the application to display franchise names appropriately for different locales as returned by the ScreenScraper API.
+ ///
+ public List? noms { get; set; }
+ }
+
+ ///
+ /// Represents a media item for the ScreenScraper API, containing properties such as type, URL, region, and various hash values. This class is used to deserialize media data from the ScreenScraper API responses, allowing for structured access to media information. The properties include the type of media (e.g., screenshot, box art), the URL where the media can be accessed, the region associated with the media, and various hash values (CRC, MD5, SHA1) for verifying the integrity of the media file. This structure provides a comprehensive representation of media as returned by the ScreenScraper API.
+ ///
+ public class ssMedia
+ {
+ ///
+ /// Type of media, such as "screenshot", "boxart", "banner", etc., indicating the category or purpose of the media item. This property allows for structured access to media information based on its type, enabling the application to organize and present media effectively based on the type of media returned by the ScreenScraper API.
+ ///
+ public string? type { get; set; }
+ ///
+ /// URL where the media can be accessed, providing a direct link to the media file associated with the game. This property allows for structured access to media information based on its URL, enabling the application to retrieve and display media effectively based on the URL provided by the ScreenScraper API.
+ ///
+ public string? parent { get; set; }
+ ///
+ /// URL where the media can be accessed, providing a direct link to the media file associated with the game. This property allows for structured access to media information based on its URL, enabling the application to retrieve and display media effectively based on the URL provided by the ScreenScraper API.
+ ///
+ public string? url { get; set; }
+ ///
+ /// Region associated with the media, such as France, Europe, USA, or Japan. This property is used to identify the specific region for which the media information is relevant, allowing for localized metadata retrieval based on regional differences in game releases or information as returned by the ScreenScraper API.
+ ///
+ public string? region { get; set; }
+ ///
+ /// Indicates whether the media is the main media for the game (0 = no / 1 = yes). This property can be used to identify the primary media associated with a game, allowing for structured access to media information based on its significance or relevance to the game as returned by the ScreenScraper API.
+ ///
+ public string? support { get; set; }
+ ///
+ /// CRC hash value for the media file, used for verifying the integrity of the media file. This property allows for structured access to media information based on its CRC hash, enabling the application to validate the media file effectively based on the hash value provided by the ScreenScraper API.
+ ///
+ public string? crc { get; set; }
+ ///
+ /// MD5 hash value for the media file, used for verifying the integrity of the media file. This property allows for structured access to media information based on its MD5 hash, enabling the application to validate the media file effectively based on the hash value provided by the ScreenScraper API.
+ ///
+ public string? md5 { get; set; }
+ ///
+ /// SHA1 hash value for the media file, used for verifying the integrity of the media file. This property allows for structured access to media information based on its SHA1 hash, enabling the application to validate the media file effectively based on the hash value provided by the ScreenScraper API.
+ ///
+ public string? sha1 { get; set; }
+ ///
+ /// Size of the media file, providing information about the file's storage requirements. This property allows for structured access to media information based on its size, enabling the application to manage storage and display media effectively based on the size information provided by the ScreenScraper API.
+ ///
+ public string? size { get; set; }
+ ///
+ /// Format of the media file, indicating the file type or encoding used. This property allows for structured access to media information based on its format, enabling the application to handle and display media effectively based on the format information provided by the ScreenScraper API.
+ ///
+ public string? format { get; set; }
+ }
+
+ public class ssRom
+ {
+ ///
+ /// numeric identifier of the rom
+ ///
+ public long? Id { get; set; }
+ ///
+ /// support number (ex: 1 = floppy disk 01 or CD 01)
+ ///
+ public int? Romnumsupport { get; set; }
+ ///
+ /// total number of supports (ex: 2 = 2 floppy disks or 2 CDs)
+ ///
+ public int? romtotalsupport { get; set; }
+ ///
+ /// name of the rom file or folder
+ ///
+ public string? romfilename { get; set; }
+ ///
+ /// octect size of the rom file or size of the contents of the folder
+ ///
+ public int? romsize { get; set; }
+ ///
+ /// result of the CRC32 calculation of the rom file or the largest file of the "rom" folder
+ ///
+ public string? romcrc { get; set; }
+ ///
+ /// result of the MD5 calculation of the rom file or the largest file of the "rom" folder
+ ///
+ public string? rommd5 { get; set; }
+ ///
+ /// result of the SHA1 calculation of the rom file or the largest file of the "rom" folder
+ ///
+ public string? romsha1 { get; set; }
+ ///
+ /// digital identifier of the parent rom if the rom is a clone (Arcade Systems)
+ ///
+ public long? romcloneof { get; set; }
+ ///
+ /// Beta version of the game (0 = no / 1 = yes)
+ ///
+ public int? Beta { get; set; }
+ ///
+ /// Demo version of the game (0 = no / 1 = yes)
+ ///
+ public int? Demo { get; set; }
+ ///
+ /// Translated version of the game (0 = no / 1 = yes)
+ ///
+ public int? trad { get; set; }
+ ///
+ /// Modified version of the game (0 = no / 1 = yes)
+ ///
+ public int? hack { get; set; }
+ ///
+ /// Game not "Official" (0 = no / 1 = yes)
+ ///
+ public int? Unl { get; set; }
+ ///
+ /// Alternative version of the game (0 = no / 1 = yes)
+ ///
+ public int? alt { get; set; } = 0;
+ ///
+ /// Best version of the game (0 = no / 1 = yes)
+ ///
+ public int? best { get; set; }
+ ///
+ /// Compatible Retro Achievement (0 = no / 1 = yes)
+ ///
+ public int? Retroachievement { get; set; }
+ ///
+ /// Gamelink compatible (0 = no / 1 = yes)
+ ///
+ public int? Gamelink { get; set; }
+ ///
+ /// Total number of times scraped
+ ///
+ public int? nbscrap { get; set; }
+ ///
+ /// List of supported languages
+ ///
+ public Dictionary>? langues { get; set; }
+ ///
+ /// List of supported regions
+ ///
+ public Dictionary>? regions { get; set; }
+ }
+
+ ///
+ /// Represents a game item for the ScreenScraper API, containing various properties such as ID, ROM ID, names in different regions, and other metadata fields. This class is used to deserialize game data from the ScreenScraper API responses, allowing for structured access to detailed information about games based on their ROM hashes or IDs. The properties include identifiers, names in different regions, developer and publisher information, player counts, ratings, classifications, release dates, genres, modes, franchises, and associated media, providing a comprehensive representation of a game as returned by the ScreenScraper API.
+ ///
+ public class ssGame
+ {
+ ///
+ /// ID of the game, serving as a unique identifier for the game in the ScreenScraper API. This property allows for structured access to game information based on its unique ID, enabling the application to reference and organize games effectively based on the data returned by the ScreenScraper API.
+ ///
+ public long? id { get; set; }
+ ///
+ /// ROM ID associated with the game, providing a reference to the specific ROM for which the metadata is relevant. This property allows for structured access to game information based on its ROM ID, enabling the application to manage and present metadata effectively based on the ROM information provided by the ScreenScraper API.
+ ///
+ public long? romid { get; set; }
+ ///
+ /// Indicates whether the item is not a game, which can be used to filter out non-game items from the metadata results. This property allows for structured access to game information based on its classification as a game or non-game item, enabling the application to manage and present metadata effectively based on the type of item returned by the ScreenScraper API.
+ ///
+ public bool? notgame { get; set; }
+ ///
+ /// List of names for the game in different regions, providing localized information about the game's title based on regional preferences. This property allows for structured access to game names in various regions, enabling the application to present game information effectively based on regional differences in game titles as returned by the ScreenScraper API.
+ ///
+ public List? noms { get; set; }
+ ///
+ /// Indicates if the game is a clone of another game, which can be used to identify and manage metadata for games that are variations or derivatives of other games. This property allows for structured access to game information based on its classification as a clone, enabling the application to handle and present metadata effectively based on the relationships between games as returned by the ScreenScraper API.
+ ///
+ public string? cloneof { get; set; }
+ ///
+ /// System associated with the game, providing information about the platform or console for which the game was released. This property allows for structured access to game information based on its associated system, enabling the application to manage and present metadata effectively based on the platform information provided by the ScreenScraper API.
+ ///
+ public ssTextId? systeme { get; set; }
+ ///
+ /// Publisher of the game, providing information about the company or entity responsible for publishing the game. This property allows for structured access to game information based on its publisher, enabling the application to manage and present metadata effectively based on the publisher information provided by the ScreenScraper API.
+ ///
+ public ssTextId? editeur { get; set; }
+ ///
+ /// Developer of the game, providing information about the company or entity responsible for developing the game. This property allows for structured access to game information based on its developer, enabling the application to manage and present metadata effectively based on the developer information provided by the ScreenScraper API.
+ ///
+ public ssTextId? developpeur { get; set; }
+ ///
+ /// Number of players supported by the game, providing information about the multiplayer capabilities of the game. This property allows for structured access to game information based on its player count, enabling the application to manage and present metadata effectively based on the multiplayer information provided by the ScreenScraper API.
+ ///
+ public KeyValuePair? joueurs { get; set; }
+ ///
+ /// Rating of the game, providing information about the game's quality or popularity based on user ratings or reviews. This property allows for structured access to game information based on its rating, enabling the application to manage and present metadata effectively based on the rating information provided by the ScreenScraper API.
+ ///
+ public KeyValuePair? note { get; set; }
+ ///
+ /// Top staff associated with the game, providing information about key personnel involved in the game's development or production. This property allows for structured access to game information based on its top staff, enabling the application to manage and present metadata effectively based on the personnel information provided by the ScreenScraper API.
+ ///
+ public string? topstaff { get; set; }
+ ///
+ /// Rotation of the game, providing information about the orientation or display settings for the game. This property allows for structured access to game information based on its rotation, enabling the application to manage and present metadata effectively based on the display information provided by the ScreenScraper API.
+ ///
+ public string? rotation { get; set; }
+ ///
+ /// Synopsis of the game, providing a brief description or summary of the game's plot, gameplay, or features. This property allows for structured access to game information based on its synopsis, enabling the application to manage and present metadata effectively based on the descriptive information provided by the ScreenScraper API.
+ ///
+ public List? synopsis { get; set; }
+ ///
+ /// List of classifications associated with the game, providing information about the various categories or classifications that apply to the game. This property allows for structured access to game information based on its classifications, enabling the application to manage and present metadata effectively based on the classification information provided by the ScreenScraper API.
+ ///
+ public List? classifications { get; set; }
+ ///
+ /// List of release dates for the game in different regions, providing information about when the game was released in various regions. This property allows for structured access to game information based on its release dates, enabling the application to manage and present metadata effectively based on regional differences in game release dates as returned by the ScreenScraper API.
+ ///
+ public List? dates { get; set; }
+ ///
+ /// List of genres associated with the game, providing information about the various genres that apply to the game. This property allows for structured access to game information based on its genres, enabling the application to manage and present metadata effectively based on the genre information provided by the ScreenScraper API.
+ ///
+ public List? genres { get; set; }
+ ///
+ /// List of game modes associated with the game, providing information about the various modes of play that apply to the game. This property allows for structured access to game information based on its game modes, enabling the application to manage and present metadata effectively based on the game mode information provided by the ScreenScraper API.
+ ///
+ public List? modes { get; set; }
+ ///
+ /// List of franchises associated with the game, providing information about the various franchises that apply to the game. This property allows for structured access to game information based on its franchises, enabling the application to manage and present metadata effectively based on the franchise information provided by the ScreenScraper API.
+ ///
+ public List? familles { get; set; }
+ ///
+ /// List of media associated with the game, providing information about the various media types that apply to the game. This property allows for structured access to game information based on its media, enabling the application to manage and present metadata effectively based on the media information provided by the ScreenScraper API.
+ ///
+ public List? medias { get; set; }
+ ///
+ /// List of ROMs associated with the game, providing information about the various ROM files that apply to the game. This property allows for structured access to game information based on its ROMs, enabling the application to manage and present metadata effectively based on the ROM information provided by the ScreenScraper API.
+ ///
+ public List? roms { get; set; }
+ }
+
+ ///
+ /// Represents a platform item for the ScreenScraper API, containing properties such as ID, parent ID, and names in different languages. This class is used to deserialize platform data from the ScreenScraper API responses, allowing for structured access to platform information associated with games. The properties include identifiers, parent-child relationships between platforms, and names in different languages, providing a comprehensive representation of platforms as returned by the ScreenScraper API.
+ ///
+ public class ssPlatform
+ {
+ ///
+ /// digital identifier of the system (to be provided again in other API requests)
+ ///
+ public long? id { get; set; }
+ ///
+ /// digital identifier of the parent system
+ ///
+ public long? parentid { get; set; }
+ /// List of names for the platform in different languages, providing localized information about the platform based on language preferences. This property allows for structured access to platform names in various languages, enabling the application to present platform information effectively based on the language preferences of users or regional differences in platform information as returned by the ScreenScraper API.
+ ///
+ public Dictionary noms { get; set; }
+ ///
+ /// extensions of usable rom files (all emulators combined)
+ ///
+ public string? extensions { get; set; }
+ ///
+ /// Name of the system production company
+ ///
+ public string? compagnie { get; set; }
+ ///
+ /// System type (Arcade,Console,Portable Console,Arcade Emulation,Fipper,Online,Computer,Smartphone)
+ ///
+ public string? type { get; set; }
+ ///
+ /// Year of production start
+ ///
+ public string? datedebut { get; set; }
+ ///
+ /// Year of end of production
+ ///
+ public string? datefin { get; set; }
+ ///
+ /// Type(s) of roms
+ ///
+ public string? romtype { get; set; }
+ ///
+ /// Type of the original system media
+ ///
+ public string? romTypesListe { get; set; }
+ ///
+ /// List of media associated with the platform
+ ///
+ public List? medias { get; set; }
+ }
+
+ ///
+ /// Maps to the ssuser ScreenScraper API object, used to guage how many API calls are being made and to manage the API rate limits by tracking the number of calls made and the time until the next reset.
+ ///
+ public class UserItem
+ {
+ ///
+ /// Gets the ScreenScraper API endpoint URL for retrieving user information, including the client ID and secret from the configuration settings. This endpoint is used to check the API rate limits and usage for the ScreenScraper API.
+ ///
+ public static string Endpoint()
+ {
+ return $"https://api.screenscraper.fr/api2/ssuserInfos.php?devid={Config.ScreenScraperConfiguration.DevClientId}&devpassword={Config.ScreenScraperConfiguration.DevSecret}&softname=Hasheous&output=json&ssid={Config.ScreenScraperConfiguration.ClientId}&sspassword={Config.ScreenScraperConfiguration.Secret}";
+ }
+
+ ///
+ /// Standard header for ScreenScraper API responses, containing information about the API version, request time, command requested, success status, and any error messages. This header is included in the user information response to provide context about the API request and response.
+ ///
+ public ssHeader? header { get; set; }
+
+ ///
+ /// Contains information about the ScreenScraper API servers and the user, including API usage and rate limit information. This information is used to manage API rate limits effectively by tracking how many calls have been made and when the limits will reset. The server information provides insights into the current load on the API servers, while the user information tracks the API usage for the specific user account, allowing the application to avoid exceeding the limits and ensure smooth operation.
+ ///
+ public UserInfoResponse? response { get; set; }
+
+ ///
+ /// Represents the response from the ScreenScraper API when fetching user information, including server status and user API usage details. This class is used to deserialize the JSON response from the API and provides structured access to the server and user information needed to manage API rate limits effectively.
+ ///
+ public class UserInfoResponse
+ {
+ ///
+ /// Information about the ScreenScraper API servers, including CPU usage, thread counts, and API access details. This information helps gauge the current load on the API servers and can be used to make informed decisions about when to make API calls to avoid overloading the servers.
+ ///
+ public ssServeurs? serveurs { get; set; }
+ ///
+ /// Information about the ScreenScraper API user, including API usage and rate limit details. This information is crucial for managing API rate limits effectively by tracking how many calls have been made and when the limits will reset, allowing the application to avoid exceeding the limits and ensure smooth operation.
+ ///
+ public ssUser? ssuser { get; set; }
+ }
+ }
+
+ ///
+ /// Represents a game item for the ScreenScraper API, providing a method to construct the API endpoint URL for retrieving game information based on either the game ID or ROM hashes (MD5 and SHA1). This class is used to generate the correct endpoint for fetching game metadata from the ScreenScraper API, allowing for flexible searching by either ID or hash values. The method ensures that the necessary parameters are provided and constructs the appropriate URL for the API request.
+ ///
+ public class GameItem
+ {
+ ///
+ /// Constructs the ScreenScraper API endpoint URL for retrieving game information based on the provided ID or ROM hashes (MD5 and SHA1). If an ID is provided, it constructs the endpoint using the ID. If no ID is provided, it requires both MD5 and SHA1 hashes to construct the endpoint for searching by hash. This method ensures that the correct endpoint is generated based on the available information for retrieving game metadata from the ScreenScraper API.
+ ///
+ /// The ID of the game to retrieve information for.
+ /// The MD5 hash of the game's ROM.
+ /// The SHA1 hash of the game's ROM.
+ /// The constructed ScreenScraper API endpoint URL.
+ /// Thrown when neither ID nor valid hashes are provided.
+ public static string Endpoint(long? id = null, string? md5hash = null, string? sha1hash = null)
+ {
+ // if we have an ID, we can construct the endpoint directly
+ if (id.HasValue)
+ {
+ return $"https://api.screenscraper.fr/api2/jeuInfos.php?devid={Config.ScreenScraperConfiguration.DevClientId}&devpassword={Config.ScreenScraperConfiguration.DevSecret}&softname=Hasheous&output=json&ssid={Config.ScreenScraperConfiguration.ClientId}&sspassword={Config.ScreenScraperConfiguration.Secret}&id={id.Value}";
+ }
+
+ // if we don't have an ID, we need to search by hash, either MD5 or SHA1 (or both) must be provided
+ if (String.IsNullOrEmpty(md5hash) && String.IsNullOrEmpty(sha1hash))
+ {
+ throw new ArgumentException("Both MD5 and SHA1 hashes must be provided to construct the ScreenScraper game endpoint.");
+ }
+ else if (!String.IsNullOrEmpty(md5hash))
+ {
+ return $"https://api.screenscraper.fr/api2/jeuInfos.php?devid={Config.ScreenScraperConfiguration.DevClientId}&devpassword={Config.ScreenScraperConfiguration.DevSecret}&softname=Hasheous&output=json&ssid={Config.ScreenScraperConfiguration.ClientId}&sspassword={Config.ScreenScraperConfiguration.Secret}&md5={md5hash}";
+ }
+ else if (!String.IsNullOrEmpty(sha1hash))
+ {
+ return $"https://api.screenscraper.fr/api2/jeuInfos.php?devid={Config.ScreenScraperConfiguration.DevClientId}&devpassword={Config.ScreenScraperConfiguration.DevSecret}&softname=Hasheous&output=json&ssid={Config.ScreenScraperConfiguration.ClientId}&sspassword={Config.ScreenScraperConfiguration.Secret}&sha1={sha1hash}";
+ }
+ else
+ {
+ throw new ArgumentException("At least one of MD5 or SHA1 hash must be provided to construct the ScreenScraper game endpoint.");
+ }
+ }
+
+ ///
+ /// Standard header for ScreenScraper API responses, containing information about the API version, request time, command requested, success status, and any error messages. This header is included in the user information response to provide context about the API request and response.
+ ///
+ public ssHeader? header { get; set; }
+
+ ///
+ /// Contains information about the ScreenScraper API servers and the user, including API usage and rate limit information, as well as detailed information about the game retrieved from the API. This information is used to manage API rate limits effectively by tracking how many calls have been made and when the limits will reset, while also providing structured access to comprehensive game metadata based on the data returned by the ScreenScraper API. The server information provides insights into the current load on the API servers, while the user information tracks the API usage for the specific user account, allowing the application to avoid exceeding the limits and ensure smooth operation. The game information includes various metadata fields such as names, developer, publisher, player counts, ratings, classifications, release dates, genres, modes, franchises, and associated media, enabling the application to manage and present metadata effectively based on the detailed information provided for each game.
+ ///
+ public GameInfoResponse? response { get; set; }
+
+ ///
+ /// Represents the response from the ScreenScraper API when fetching game information, including server status, user API usage details, and comprehensive game metadata. This class is used to deserialize the JSON response from the API and provides structured access to the server, user, and game information needed to manage API rate limits effectively while also providing detailed metadata about the game as returned by the ScreenScraper API.
+ ///
+ public class GameInfoResponse
+ {
+ ///
+ /// Information about the ScreenScraper API servers, including CPU usage, thread counts, and API access details. This information helps gauge the current load on the API servers and can be used to make informed decisions about when to make API calls to avoid overloading the servers.
+ ///
+ public ssServeurs? serveurs { get; set; }
+ ///
+ /// Information about the ScreenScraper API user, including API usage and rate limit details. This information is crucial for managing API rate limits effectively by tracking how many calls have been made and when the limits will reset, allowing the application to avoid exceeding the limits and ensure smooth operation.
+ ///
+ public ssUser? ssuser { get; set; }
+ ///
+ /// Detailed information about the game retrieved from the ScreenScraper API, including various metadata fields such as names, developer, publisher, player counts, ratings, classifications, release dates, genres, modes, franchises, and associated media. This property allows for structured access to comprehensive game information based on the data returned by the ScreenScraper API, enabling the application to manage and present metadata effectively based on the detailed information provided for each game.
+ ///
+ public ssGame? jeu { get; set; }
+ }
+ }
+
+ public class PlatformItem
+ {
+ ///
+ /// Gets the ScreenScraper API endpoint URL for retrieving platform information, which returns a list of all platforms available in the ScreenScraper database. Since the ScreenScraper API does not support server-side filtering for platforms, this endpoint retrieves all platforms, and any necessary filtering must be done client-side based on the data returned by the API. This method constructs the endpoint URL using the client ID and secret from the configuration settings, allowing for authenticated access to the ScreenScraper API to fetch platform metadata.
+ ///
+ public static string Endpoint()
+ {
+ // ScreenScraper's endpoint for platform metadata returns ALL platforms with no server side filtering
+ return $"https://api.screenscraper.fr/api2/systemesListe.php?devid=devid={Config.ScreenScraperConfiguration.DevClientId}&devpassword={Config.ScreenScraperConfiguration.DevSecret}&softname=Hasheous&output=json&ssid={Config.ScreenScraperConfiguration.ClientId}&sspassword={Config.ScreenScraperConfiguration.Secret}";
+ }
+
+ ///
+ /// Standard header for ScreenScraper API responses, containing information about the API version, request time, command requested, success status, and any error messages. This header is included in the user information response to provide context about the API request and response.
+ ///
+ public ssHeader? header { get; set; }
+
+ ///
+ /// Contains information about the ScreenScraper API servers and the user, including API usage and rate limit information, as well as detailed information about the platforms retrieved from the API. This information is used to manage API rate limits effectively by tracking how many calls have been made and when the limits will reset, while also providing structured access to comprehensive platform metadata based on the data returned by the ScreenScraper API. The server information provides insights into the current load on the API servers, while the user information tracks the API usage for the specific user account, allowing the application to avoid exceeding the limits and ensure smooth operation. The platform information includes various metadata fields such as platform names, release dates, manufacturers, and other relevant details, enabling the application to manage and present metadata effectively based on the detailed information provided for each platform.
+ ///
+ public PlatformInfoResponse? response { get; set; }
+
+ ///
+ /// Represents the response from the ScreenScraper API when fetching platform information, including server status, user API usage details, and comprehensive platform metadata. This class is used to deserialize the JSON response from the API and provides structured access to the server, user, and platform information needed to manage API rate limits effectively while also providing detailed metadata about the platforms as returned by the ScreenScraper API.
+ ///
+ public class PlatformInfoResponse
+ {
+ ///
+ /// Information about the ScreenScraper API servers, including CPU usage, thread counts, and API access details. This information helps gauge the current load on the API servers and can be used to make informed decisions about when to make API calls to avoid overloading the servers.
+ ///
+ public ssServeurs? serveurs { get; set; }
+
+ ///
+ /// Information about the ScreenScraper API user, including API usage and rate limit details. This information is crucial for managing API rate limits effectively by tracking how many calls have been made and when the limits will reset, allowing the application to avoid exceeding the limits and ensure smooth operation.
+ ///
+ public List? systemes { get; set; }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/hasheous-lib/Classes/Metadata/Storage.cs b/hasheous-lib/Classes/Metadata/Storage.cs
index 4a3d6e13..386fe515 100644
--- a/hasheous-lib/Classes/Metadata/Storage.cs
+++ b/hasheous-lib/Classes/Metadata/Storage.cs
@@ -18,7 +18,8 @@ public enum CacheStatus
public enum TablePrefix
{
- IGDB
+ IGDB,
+ ScreenScraper
}
private static string GetTableName(TablePrefix prefix, string Endpoint)
@@ -129,6 +130,14 @@ public static async Task NewCacheValueAsync(TablePrefix prefix, object ObjectToC
if (objectProperty != null)
{
string compareName = objectProperty.PropertyType.Name.ToLower().Split("`")[0];
+ if (compareName == "nullable")
+ {
+ Type? underlyingType = Nullable.GetUnderlyingType(objectProperty.PropertyType);
+ if (underlyingType != null)
+ {
+ compareName = underlyingType.Name.ToLower().Split("`")[0];
+ }
+ }
var objectValue = objectProperty.GetValue(ObjectToCache);
if (objectValue != null)
{
@@ -152,6 +161,10 @@ public static async Task NewCacheValueAsync(TablePrefix prefix, object ObjectToC
newObjectValue = Newtonsoft.Json.JsonConvert.SerializeObject(objectValue);
objectDict[key.Key] = newObjectValue;
break;
+ case "list":
+ newObjectValue = Newtonsoft.Json.JsonConvert.SerializeObject(objectValue);
+ objectDict[key.Key] = newObjectValue;
+ break;
}
}
}
@@ -208,11 +221,16 @@ public static T BuildCacheObject(T EndpointType, DataRow dataRow)
if (dataRow[property.Name] != DBNull.Value)
{
string objectTypeName = property.PropertyType.Name.ToLower().Split("`")[0];
+ Type? nullableUnderlyingType = null;
string subObjectTypeName = "";
object? objectToStore = null;
if (objectTypeName == "nullable")
{
- objectTypeName = property.PropertyType.UnderlyingSystemType.ToString().Split("`1")[1].Replace("[System.", "").Replace("]", "").ToLower();
+ nullableUnderlyingType = Nullable.GetUnderlyingType(property.PropertyType);
+ if (nullableUnderlyingType != null)
+ {
+ objectTypeName = nullableUnderlyingType.Name.ToLower().Split("`")[0];
+ }
}
try
{
@@ -226,6 +244,14 @@ public static T BuildCacheObject(T EndpointType, DataRow dataRow)
DateTimeOffset? storedDate = (DateTime?)dataRow[property.Name];
property.SetValue(EndpointType, storedDate);
break;
+ case "list":
+ string listJson = dataRow[property.Name]?.ToString() ?? "[]";
+ object? listObject = Newtonsoft.Json.JsonConvert.DeserializeObject(listJson, property.PropertyType);
+ if (listObject != null)
+ {
+ property.SetValue(EndpointType, listObject);
+ }
+ break;
case "identityorvalue":
subObjectTypeName = property.PropertyType.UnderlyingSystemType.ToString().Split("`1")[1].Replace("[IGDB.Models.", "").Replace("]", "");
diff --git a/hasheous-lib/Classes/Metadata/TableBuilder.cs b/hasheous-lib/Classes/Metadata/TableBuilder.cs
new file mode 100644
index 00000000..44e4a013
--- /dev/null
+++ b/hasheous-lib/Classes/Metadata/TableBuilder.cs
@@ -0,0 +1,100 @@
+using System.Reflection;
+using Classes;
+using Classes.Metadata;
+using hasheous_server.Classes.MetadataLib;
+using IGDB.Models;
+
+namespace Classes.Metadata.Utility
+{
+ public class TableBuilder
+ {
+ public static void BuildTables()
+ {
+ // build IGDB tables
+ BuildTableFromType(typeof(AgeRating), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(AgeRatingCategory), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(AgeRatingContentDescriptionV2), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(AgeRatingOrganization), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(AlternativeName), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Artwork), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Character), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(CharacterGender), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(CharacterMugShot), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(CharacterSpecies), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Collection), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(CollectionMembership), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(CollectionMembershipType), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(CollectionRelation), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(CollectionRelationType), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(CollectionType), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Company), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(CompanyLogo), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(CompanyStatus), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(CompanyWebsite), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Cover), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Event), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(EventLogo), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(EventNetwork), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(ExternalGame), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(ExternalGameSource), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Franchise), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Game), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(GameEngine), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(GameEngineLogo), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(GameLocalization), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(GameMode), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(GameReleaseFormat), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(GameStatus), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(GameTimeToBeat), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(GameType), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(GameVersion), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(GameVersionFeature), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(GameVersionFeatureValue), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(GameVideo), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Genre), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(InvolvedCompany), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Keyword), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Language), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(LanguageSupport), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(LanguageSupportType), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(MultiplayerMode), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(NetworkType), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Platform), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(PlatformFamily), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(PlatformLogo), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(PlatformVersion), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(PlatformVersionCompany), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(PlatformVersionReleaseDate), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(PlatformWebsite), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(PlayerPerspective), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(PopularityPrimitive), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(PopularityType), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Region), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(ReleaseDate), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(ReleaseDateRegion), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(ReleaseDateStatus), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Screenshot), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Theme), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(Website), Storage.TablePrefix.IGDB.ToString());
+ BuildTableFromType(typeof(WebsiteType), Storage.TablePrefix.IGDB.ToString());
+ }
+
+ ///
+ /// Builds a table from a type definition, or modifies an existing table.
+ /// This is used to create or update tables in the database based on the properties of a class.
+ /// Updates are limited to adding new columns, as the table structure should not change once created.
+ /// If the table already exists, it will only add new columns that are not already present.
+ /// This is useful for maintaining a consistent schema across different versions of the application.
+ /// The method is generic and can be used with any type that has properties that can be mapped to database columns.
+ /// The method does not return any value, but it will throw an exception if there is an error during the table creation or modification process.
+ ///
+ /// The type definition of the class for which the table should be built.
+ /// The prefix to use for the table name.
+ public static void BuildTableFromType(Type type, string prefix)
+ {
+ Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
+
+ db.BuildTableFromType("hasheous", prefix, type);
+ }
+ }
+}
\ No newline at end of file
diff --git a/hasheous-lib/Classes/Metadata/TheGamesDB/IMetadata_TheGamesDB.cs b/hasheous-lib/Classes/Metadata/TheGamesDB/IMetadata_TheGamesDB.cs
index 12d6b428..da650ca5 100644
--- a/hasheous-lib/Classes/Metadata/TheGamesDB/IMetadata_TheGamesDB.cs
+++ b/hasheous-lib/Classes/Metadata/TheGamesDB/IMetadata_TheGamesDB.cs
@@ -12,6 +12,15 @@ public class MetadataTheGamesDB : IMetadata
///
public Metadata.Communications.MetadataSources MetadataSource => Metadata.Communications.MetadataSources.TheGamesDb;
+ ///
+ public bool Enabled
+ {
+ get
+ {
+ return true;
+ }
+ }
+
///
public async Task FindMatchItemAsync(hasheous_server.Models.DataObjectItem item, List searchCandidates, Dictionary? options = null)
{
diff --git a/hasheous-lib/Classes/Metadata/Wikipedia/IMetadata_Wikipedia.cs b/hasheous-lib/Classes/Metadata/Wikipedia/IMetadata_Wikipedia.cs
index f9707f55..66328ba0 100644
--- a/hasheous-lib/Classes/Metadata/Wikipedia/IMetadata_Wikipedia.cs
+++ b/hasheous-lib/Classes/Metadata/Wikipedia/IMetadata_Wikipedia.cs
@@ -9,6 +9,15 @@ public class MetadataWikipedia : IMetadata
///
public Metadata.Communications.MetadataSources MetadataSource => Metadata.Communications.MetadataSources.Wikipedia;
+ ///
+ public bool Enabled
+ {
+ get
+ {
+ return true;
+ }
+ }
+
///
public async Task FindMatchItemAsync(hasheous_server.Models.DataObjectItem item, List searchCandidates, Dictionary? options = null)
{
diff --git a/hasheous-lib/Models/DataObjectItem.cs b/hasheous-lib/Models/DataObjectItem.cs
index ded57988..60d1ee95 100644
--- a/hasheous-lib/Models/DataObjectItem.cs
+++ b/hasheous-lib/Models/DataObjectItem.cs
@@ -240,6 +240,30 @@ public uint WinningVotePercent
Template = "https://www.steamgriddb.com/game/{0}"
}
}
+ },
+ {
+ Communications.MetadataSources.ScreenScraper,
+ new List
+ {
+ new LinkTemplateItem
+ {
+ Source = Communications.MetadataSources.ScreenScraper,
+ ObjectType = DataObjects.DataObjectType.Company,
+ Template = "https://www.screenscraper.fr/companieinfos.php?companyid={0}"
+ },
+ new LinkTemplateItem
+ {
+ Source = Communications.MetadataSources.ScreenScraper,
+ ObjectType = DataObjects.DataObjectType.Platform,
+ Template = "https://www.screenscraper.fr/systemeinfos.php?plateforme={0}"
+ },
+ new LinkTemplateItem
+ {
+ Source = Communications.MetadataSources.ScreenScraper,
+ ObjectType = DataObjects.DataObjectType.Game,
+ Template = "https://www.screenscraper.fr/gameinfos.php?gameid={0}"
+ }
+ }
}
};
diff --git a/hasheous-lib/Schema/hasheous-1033.sql b/hasheous-lib/Schema/hasheous-1033.sql
new file mode 100644
index 00000000..3578bd83
--- /dev/null
+++ b/hasheous-lib/Schema/hasheous-1033.sql
@@ -0,0 +1,17 @@
+CREATE TABLE `Screenscraper_HashToGameMap` (
+ `Hash` varchar(255) NOT NULL,
+ `HashType` varchar(50) NOT NULL,
+ `GameId` BIGINT NOT NULL,
+ PRIMARY KEY (`Hash`, `HashType`, `GameId`),
+ INDEX `IX_Screenscraper_HashToGameMap_Hash` (`Hash`, `HashType`),
+ INDEX `IX_Screenscraper_HashToGameMap_GameId` (`GameId`)
+);
+
+CREATE TABLE `Screenscraper_FailedHashLookups` (
+ `Hash` varchar(255) NOT NULL,
+ `HashType` varchar(50) NOT NULL,
+ `LookupDate` datetime NOT NULL,
+ PRIMARY KEY (`Hash`, `HashType`),
+ INDEX `IX_Screenscraper_FailedHashLookups_Hash` (`Hash`, `HashType`),
+ INDEX `IX_Screenscraper_FailedHashLookups_LookupDate` (`LookupDate`)
+)
\ No newline at end of file
diff --git a/hasheous/wwwroot/localisation/en.json b/hasheous/wwwroot/localisation/en.json
index 30a30c05..3046f4c3 100644
--- a/hasheous/wwwroot/localisation/en.json
+++ b/hasheous/wwwroot/localisation/en.json
@@ -105,6 +105,7 @@
"epicgamestore": "Epic Games",
"gog": "GOG.com",
"steamgriddb": "SteamGridDB",
+ "screenscraper": "Screen Scraper",
"nomatch": "Unmatched",
"manual": "Manual",
"automatic": "Automatic Match",
diff --git a/hasheous/wwwroot/scripts/language.js b/hasheous/wwwroot/scripts/language.js
index 97433850..e107178e 100644
--- a/hasheous/wwwroot/scripts/language.js
+++ b/hasheous/wwwroot/scripts/language.js
@@ -82,6 +82,16 @@ class language {
// }
getLang(token, substituteArray) {
+ // check if token is null or undefined
+ if (token === null || token === undefined) {
+ return '';
+ }
+
+ // check if token is not a string
+ if (typeof token !== 'string') {
+ return token;
+ }
+
let page = getQueryString('page', 'string');
switch (page) {
case "dataobjects":