gaseous-server/gaseous-lib/Classes/Plugins/FileSignatures/FileSignaturePlugins/Hasheous.cs
Michael Green 04d427cbeb
Refactor file handling to improve performance (#701)
Refactor image file handling to use PhysicalFile for better performance and add ETag for caching. Optimize metadata fetching, query performance, and memory usage in various controllers.

Many background tasks are now moved to a discrete process so as to not interfere with the main process.
2026-03-26 16:14:50 +11:00

289 lines
No EOL
17 KiB
C#

using gaseous_server.Models;
using gaseous_server.Classes.Plugins.MetadataProviders.MetadataTypes;
using gaseous_server.Classes.Metadata;
using static gaseous_server.Classes.FileSignature;
using Newtonsoft.Json;
namespace gaseous_server.Classes.Plugins.FileSignatures
{
/// <summary>
/// File signature plugin for Hasheous integration.
/// </summary>
public class Hasheous : IFileSignaturePlugin
{
private static readonly JsonSerializerSettings HasheousJsonSerializerSettings = CreateHasheousJsonSerializerSettings();
/// <inheritdoc/>
public string Name { get; } = "Hasheous";
/// <inheritdoc/>
public Dictionary<string, object>? Settings { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
/// <inheritdoc/>
public bool UsesInternet { get; } = true;
private static JsonSerializerSettings CreateHasheousJsonSerializerSettings()
{
JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
serializerSettings.Converters.Add(new UnknownEnumFallbackConverter());
return serializerSettings;
}
private static HasheousClient.Models.LookupItemModel? DeserializeLookupItemModel(string? json)
{
if (string.IsNullOrWhiteSpace(json))
{
return null;
}
return JsonConvert.DeserializeObject<HasheousClient.Models.LookupItemModel>(json, HasheousJsonSerializerSettings);
}
/// <inheritdoc/>
public async Task<Signatures_Games?> GetSignature(HashObject hash, string ImageName, string ImageExtension, long ImageSize, string GameFileImportPath)
{
// check if hasheous is enabled, and if so use it's signature database
if (Config.MetadataConfiguration.SignatureSource == HasheousClient.Models.MetadataModel.SignatureSources.Hasheous)
{
HTTPComms comms = new HTTPComms();
Dictionary<string, string> headers = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(Config.MetadataConfiguration.HasheousAPIKey))
{
headers.Add("X-API-Key", Config.MetadataConfiguration.HasheousAPIKey);
}
headers.Add("X-Client-API-Key", Config.MetadataConfiguration.HasheousClientAPIKey);
// headers.Add("CacheControl", "no-cache");
// headers.Add("Pragma", "no-cache");
HasheousClient.Models.LookupItemModel? HasheousResult = null;
try
{
// check the cache first
if (!Directory.Exists(Config.LibraryConfiguration.LibraryMetadataDirectory_Hasheous()))
{
Directory.CreateDirectory(Config.LibraryConfiguration.LibraryMetadataDirectory_Hasheous());
}
// create file name from hash object
string cacheFileName = hash.md5hash + "_" + hash.sha1hash + "_" + hash.crc32hash + ".json";
string cacheFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Hasheous(), cacheFileName);
// use cache file if it exists and is less than 30 days old, otherwise fetch from hasheous. if the fetch from hasheous is successful, save it to the cache, if it fails, use the cache if it exists even if it's old
if (File.Exists(cacheFilePath))
{
FileInfo cacheFile = new FileInfo(cacheFilePath);
if (cacheFile.LastWriteTimeUtc > DateTime.UtcNow.AddDays(-30))
{
Logging.LogKey(Logging.LogType.Information, "process.get_signature", "getsignature.using_cached_signature_from_hasheous");
HasheousResult = DeserializeLookupItemModel(await File.ReadAllTextAsync(cacheFilePath));
}
}
try
{
if (HasheousResult == null)
{
// fetch from hasheous
var body = new HasheousClient.Models.HashLookupModel
{
MD5 = hash.md5hash,
SHA1 = hash.sha1hash,
SHA256 = hash.sha256hash,
CRC = hash.crc32hash
};
string bodyJson = Newtonsoft.Json.JsonConvert.SerializeObject(body);
string sourceList = "";
// var sb = new System.Text.StringBuilder("returnSources=");
// foreach (string source in Enum.GetNames(typeof(HasheousClient.Models.SignatureModel.RomItem.SignatureSourceType)))
// {
// if (source != "None" && source != "Unknown")
// {
// sb.Append(source).Append(",");
// }
// }
// sourceList = sb.ToString();
// sourceList = "?" + sourceList.TrimEnd(',');
var response = await comms.SendRequestAsync<string>(HTTPComms.HttpMethod.POST, new Uri("https://hasheous.org/api/v1/Lookup/ByHash" + sourceList), headers, body, contentType: "application/json", returnRawResponse: true);
if (response != null && response.StatusCode == 200)
{
if (!string.IsNullOrWhiteSpace(response.Body) && response.Body != "The provided hash was not found in the signature database.")
{
HasheousResult = DeserializeLookupItemModel(response.Body);
if (HasheousResult != null)
{
// save to cache
await File.WriteAllTextAsync(cacheFilePath, Newtonsoft.Json.JsonConvert.SerializeObject(HasheousResult));
}
}
}
}
}
catch (Exception ex)
{
if (ex.Message.Contains("404"))
{
Logging.LogKey(Logging.LogType.Information, "process.get_signature", "getsignature.no_signature_found_in_hasheous");
}
else if (ex.Message.Contains("403"))
{
Logging.LogKey(Logging.LogType.Warning, "process.get_signature", "getsignature.hasheous_api_key_invalid_or_expired_using_cached_signature");
}
else
{
if (File.Exists(cacheFilePath))
{
Logging.LogKey(Logging.LogType.Warning, "process.get_signature", "getsignature.error_retrieving_signature_from_hasheous_using_cached_signature", null, null, ex);
HasheousResult = DeserializeLookupItemModel(await File.ReadAllTextAsync(cacheFilePath));
}
else
{
Logging.LogKey(Logging.LogType.Warning, "process.get_signature", "getsignature.error_retrieving_signature_from_hasheous", null, null, ex);
}
}
}
if (HasheousResult != null)
{
if (HasheousResult.Signature != null)
{
gaseous_server.Models.Signatures_Games signature = new Models.Signatures_Games();
string gameJson = Newtonsoft.Json.JsonConvert.SerializeObject(HasheousResult.Signature.Game);
string romJson = Newtonsoft.Json.JsonConvert.SerializeObject(HasheousResult.Signature.Rom);
signature.Game = Newtonsoft.Json.JsonConvert.DeserializeObject<Models.Signatures_Games.GameItem>(gameJson);
signature.Rom = Newtonsoft.Json.JsonConvert.DeserializeObject<Models.Signatures_Games.RomItem>(romJson);
// get platform metadata
if (HasheousResult.Platform != null)
{
if (HasheousResult.Platform.metadata.Count > 0)
{
foreach (HasheousClient.Models.MetadataItem metadataResult in HasheousResult.Platform.metadata)
{
if (Enum.TryParse<MetadataSources>(metadataResult.Source, out MetadataSources metadataSource))
{
// only IGDB metadata is supported
if (metadataSource == MetadataSources.IGDB)
{
// check if the immutable id is a long
if (metadataResult.ImmutableId.Length > 0 && long.TryParse(metadataResult.ImmutableId, out long immutableId) == true)
{
// use immutable id
Platform hasheousPlatform = await Platforms.GetPlatform(immutableId);
signature.MetadataSources.AddPlatform((long)hasheousPlatform.Id, hasheousPlatform.Name, metadataSource);
}
else
{
// unresolvable immutableid - use unknown platform
signature.MetadataSources.AddPlatform(0, "Unknown Platform", MetadataSources.None);
}
}
}
}
}
}
// get game metadata
if (HasheousResult.Metadata != null)
{
if (HasheousResult.Metadata.Count > 0)
{
foreach (HasheousClient.Models.MetadataItem metadataResult in HasheousResult.Metadata)
{
if (Enum.TryParse<MetadataSources>(metadataResult.Source, out MetadataSources metadataSource))
{
if (metadataResult.ImmutableId.Length > 0)
{
switch (metadataSource)
{
case FileSignature.MetadataSources.IGDB:
// check if the immutable id is a long
if (metadataResult.ImmutableId.Length > 0 && long.TryParse(metadataResult.ImmutableId, out long immutableId) == true)
{
// use immutable id
gaseous_server.Classes.Plugins.MetadataProviders.MetadataTypes.Game hasheousGame = await Games.GetGame(FileSignature.MetadataSources.IGDB, immutableId);
signature.MetadataSources.AddGame((long)hasheousGame.Id, hasheousGame.Name, metadataSource);
}
else
{
// unresolvable immutable id - use unknown game
signature.MetadataSources.AddGame(0, "Unknown Game", FileSignature.MetadataSources.None);
}
break;
default:
if (long.TryParse(metadataResult.ImmutableId, out long id) == true)
{
signature.MetadataSources.AddGame(id, HasheousResult.Name, metadataSource);
}
else
{
signature.MetadataSources.AddGame(0, "Unknown Game", FileSignature.MetadataSources.None);
}
break;
}
}
else
{
// unresolvable immutable id - use unknown game
signature.MetadataSources.AddGame(0, "Unknown Game", FileSignature.MetadataSources.None);
}
}
}
}
}
// check attributes for a user manual link
if (HasheousResult.Attributes != null)
{
if (HasheousResult.Attributes.Count > 0)
{
foreach (HasheousClient.Models.AttributeItem attribute in HasheousResult.Attributes)
{
if (attribute.attributeName == HasheousClient.Models.AttributeItem.AttributeName.VIMMManualId)
{
signature.Game.UserManual = attribute.GetType().GetProperty("Link").GetValue(attribute).ToString();
}
}
}
}
return signature;
}
}
}
catch (AggregateException aggEx)
{
foreach (Exception ex in aggEx.InnerExceptions)
{
// get exception type
if (ex is HttpRequestException)
{
if (ex.Message.Contains("404 (Not Found)"))
{
Logging.LogKey(Logging.LogType.Information, "process.get_signature", "getsignature.no_signature_found_in_hasheous");
}
else
{
Logging.LogKey(Logging.LogType.Warning, "process.get_signature", "getsignature.error_retrieving_signature_from_hasheous", null, null, ex);
throw;
}
}
else
{
Logging.LogKey(Logging.LogType.Warning, "process.get_signature", "getsignature.error_retrieving_signature_from_hasheous", null, null, ex);
throw;
}
}
}
catch (Exception ex)
{
Logging.LogKey(Logging.LogType.Warning, "process.get_signature", "getsignature.error_retrieving_signature_from_hasheous", null, null, ex);
}
}
return null;
}
}
}