From 13dc90883d74127fec4ac59519a75bda2865a653 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sat, 11 Mar 2023 01:04:03 +1100 Subject: [PATCH 01/71] fix: database upgrade and added basic timer --- Gaseous.sln | 2 - .../Controllers/SignaturesController.cs | 2 +- gaseous-server/Program.cs | 11 ++- gaseous-server/Timer.cs | 50 +++++++++++++ gaseous-signature-ingestor/Program.cs | 17 +++-- gaseous-tools/Config.cs | 74 +++++++++++++++++++ gaseous-tools/Database.cs | 2 + gaseous-tools/Database/MySQL/gaseous-1000.sql | 10 +-- gaseous-tools/Database/MySQL/gaseous-1001.sql | 6 +- gaseous-tools/Database/MySQL/gaseous-1002.sql | 6 ++ gaseous-tools/gaseous-tools.csproj | 2 + 11 files changed, 157 insertions(+), 25 deletions(-) create mode 100644 gaseous-server/Timer.cs create mode 100644 gaseous-tools/Database/MySQL/gaseous-1002.sql diff --git a/Gaseous.sln b/Gaseous.sln index f7adb01..9d1c2ba 100644 --- a/Gaseous.sln +++ b/Gaseous.sln @@ -15,8 +15,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaseous-tools", "gaseous-to EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gaseous-server", "gaseous-server\gaseous-server.csproj", "{A01D2EFF-C82E-473B-84D7-7C25E736F5D2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{B07A4655-A003-416B-A790-ADAA5B548E1A}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/gaseous-server/Controllers/SignaturesController.cs b/gaseous-server/Controllers/SignaturesController.cs index 7f69893..e1f3e58 100644 --- a/gaseous-server/Controllers/SignaturesController.cs +++ b/gaseous-server/Controllers/SignaturesController.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc; namespace gaseous_server.Controllers { [ApiController] - [Route("api/[controller]/[action]")] + [Route("api/v1/[controller]/[action]")] public class SignaturesController : ControllerBase { /// diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index 6fd856e..f0ae836 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -1,6 +1,12 @@ using System.Text.Json.Serialization; +using gaseous_server; using gaseous_tools; +// set up db +Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); +db.InitDB(); + +// set up server var builder = WebApplication.CreateBuilder(args); // Add services to the container. @@ -13,6 +19,7 @@ builder.Services.AddControllers().AddJsonOptions(x => // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +builder.Services.AddHostedService(); var app = builder.Build(); @@ -29,10 +36,6 @@ app.UseAuthorization(); app.MapControllers(); -// set up db -Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); -db.InitDB(); - // start the app app.Run(); diff --git a/gaseous-server/Timer.cs b/gaseous-server/Timer.cs new file mode 100644 index 0000000..6d3a977 --- /dev/null +++ b/gaseous-server/Timer.cs @@ -0,0 +1,50 @@ +using System; + +namespace gaseous_server +{ + // see: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0&tabs=visual-studio-mac#timed-background-tasks-1 + public class TimedHostedService : IHostedService, IDisposable + { + private int executionCount = 0; + private readonly ILogger _logger; + private Timer _timer; + + public TimedHostedService(ILogger logger) + { + _logger = logger; + } + + public Task StartAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Timed Hosted Service running."); + + _timer = new Timer(DoWork, null, TimeSpan.Zero, + TimeSpan.FromSeconds(5)); + + return Task.CompletedTask; + } + + private void DoWork(object state) + { + var count = Interlocked.Increment(ref executionCount); + + _logger.LogInformation( + "Timed Hosted Service is working. Count: {Count}", count); + } + + public Task StopAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Timed Hosted Service is stopping."); + + _timer?.Change(Timeout.Infinite, 0); + + return Task.CompletedTask; + } + + public void Dispose() + { + _timer?.Dispose(); + } + } +} + diff --git a/gaseous-signature-ingestor/Program.cs b/gaseous-signature-ingestor/Program.cs index 6746f5f..5108736 100644 --- a/gaseous-signature-ingestor/Program.cs +++ b/gaseous-signature-ingestor/Program.cs @@ -66,6 +66,7 @@ if (Directory.Exists(tosecXML)) tosecXML = Path.GetFullPath(tosecXML); string[] tosecPathContents = Directory.GetFiles(tosecXML); + Array.Sort(tosecPathContents); int lineFileNameLength = 0; int lineGameNameLength = 0; @@ -172,10 +173,10 @@ if (Directory.Exists(tosecXML)) if (sigDB.Rows.Count == 0) { // entry not present, insert it - sql = "INSERT INTO signatures_platforms (platform) VALUES (@platform); SELECT LAST_INSERT_ID()"; + sql = "INSERT INTO signatures_platforms (platform) VALUES (@platform); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; sigDB = db.ExecuteCMD(sql, dbDict); - gameSystem = (int)sigDB.Rows[0][0]; + gameSystem = Convert.ToInt32(sigDB.Rows[0][0]); } else { @@ -194,9 +195,9 @@ if (Directory.Exists(tosecXML)) if (sigDB.Rows.Count == 0) { // entry not present, insert it - sql = "INSERT INTO signatures_publishers (publisher) VALUES (@publisher); SELECT LAST_INSERT_ID()"; + sql = "INSERT INTO signatures_publishers (publisher) VALUES (@publisher); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; sigDB = db.ExecuteCMD(sql, dbDict); - gamePublisher = (int)sigDB.Rows[0][0]; + gamePublisher = Convert.ToInt32(sigDB.Rows[0][0]); } else { @@ -215,10 +216,10 @@ if (Directory.Exists(tosecXML)) // entry not present, insert it sql = "INSERT INTO signatures_games " + "(name, description, year, publisherid, demo, systemid, systemvariant, video, country, language, copyright) VALUES " + - "(@name, @description, @year, @publisherid, @demo, @systemid, @systemvariant, @video, @country, @language, @copyright); SELECT LAST_INSERT_ID()"; + "(@name, @description, @year, @publisherid, @demo, @systemid, @systemvariant, @video, @country, @language, @copyright); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; sigDB = db.ExecuteCMD(sql, dbDict); - gameId = (int)sigDB.Rows[0][0]; + gameId = Convert.ToInt32(sigDB.Rows[0][0]); } else { @@ -264,11 +265,11 @@ if (Directory.Exists(tosecXML)) if (sigDB.Rows.Count == 0) { // entry not present, insert it - sql = "INSERT INTO signatures_roms (gameid, name, size, crc, md5, sha1, developmentstatus, flags, romtype, romtypemedia, medialabel) VALUES (@gameid, @name, @size, @crc, @md5, @sha1, @developmentstatus, @flags, @romtype, @romtypemedia, @medialabel); SELECT LAST_INSERT_ID()"; + sql = "INSERT INTO signatures_roms (gameid, name, size, crc, md5, sha1, developmentstatus, flags, romtype, romtypemedia, medialabel) VALUES (@gameid, @name, @size, @crc, @md5, @sha1, @developmentstatus, @flags, @romtype, @romtypemedia, @medialabel); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; sigDB = db.ExecuteCMD(sql, dbDict); - romId = (int)sigDB.Rows[0][0]; + romId = Convert.ToInt32(sigDB.Rows[0][0]); } else { diff --git a/gaseous-tools/Config.cs b/gaseous-tools/Config.cs index c9e8e4e..948b2ac 100644 --- a/gaseous-tools/Config.cs +++ b/gaseous-tools/Config.cs @@ -1,4 +1,5 @@ using System; +using System.Data; using Newtonsoft.Json; namespace gaseous_tools @@ -84,10 +85,44 @@ namespace gaseous_tools File.WriteAllText(ConfigurationFilePath, configRaw); } + private static string ReadSetting(string SettingName, string DefaultValue) + { + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "SELECT * FROM settings WHERE setting = @settingname"; + Dictionary dbDict = new Dictionary(); + dbDict.Add("settingname", SettingName); + dbDict.Add("value", DefaultValue); + + DataTable dbResponse = db.ExecuteCMD(sql, dbDict); + if (dbResponse.Rows.Count == 0) + { + // no value with that name stored - respond with the default value + return DefaultValue; + } + else + { + return (string)dbResponse.Rows[0][0]; + } + } + + private static void SetSetting(string SettingName, string Value) + { + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "REPLACE INTO settings (setting, value) VALUES (@settingname, @value)"; + Dictionary dbDict = new Dictionary(); + dbDict.Add("settingname", SettingName); + dbDict.Add("value", Value); + + db.ExecuteCMD(sql, dbDict); + } + public class ConfigFile { public Database DatabaseConfiguration = new Database(); + [JsonIgnore] + public Library LibraryConfiguration = new Library(); + public class Database { public string HostName = "localhost"; @@ -106,6 +141,45 @@ namespace gaseous_tools } } } + + public class Library + { + public string LibraryRootDirectory + { + get + { + return ReadSetting("LibraryRootDirectory", Path.Combine(Config.ConfigurationPath, "Data")); + } + set + { + SetSetting("LibraryRootDirectory", value); + } + } + + public string LibraryUploadDirectory + { + get + { + return Path.Combine(LibraryRootDirectory, "Upload"); + } + } + + public string LibraryImportDirectory + { + get + { + return Path.Combine(LibraryRootDirectory, "Import"); + } + } + + public string LibraryDataDirectory + { + get + { + return Path.Combine(LibraryRootDirectory, "Library"); + } + } + } } } } diff --git a/gaseous-tools/Database.cs b/gaseous-tools/Database.cs index 81bc5f7..e11afc0 100644 --- a/gaseous-tools/Database.cs +++ b/gaseous-tools/Database.cs @@ -91,6 +91,7 @@ namespace gaseous_tools // apply script sql = "SELECT schema_version FROM schema_version;"; + dbDict = new Dictionary(); DataTable SchemaVersion = ExecuteCMD(sql, dbDict); if (SchemaVersion.Rows.Count == 0) { @@ -106,6 +107,7 @@ namespace gaseous_tools ExecuteCMD(dbScript, dbDict); sql = "UPDATE schema_version SET schema_version=@schemaver"; + dbDict = new Dictionary(); dbDict.Add("schemaver", i); ExecuteCMD(sql, dbDict); } diff --git a/gaseous-tools/Database/MySQL/gaseous-1000.sql b/gaseous-tools/Database/MySQL/gaseous-1000.sql index 83b73cb..ba3f120 100644 --- a/gaseous-tools/Database/MySQL/gaseous-1000.sql +++ b/gaseous-tools/Database/MySQL/gaseous-1000.sql @@ -42,7 +42,7 @@ CREATE TABLE `signatures_games` ( KEY `ingest_idx` (`name`,`year`,`publisherid`,`systemid`,`country`,`language`) USING BTREE, CONSTRAINT `publisher` FOREIGN KEY (`publisherid`) REFERENCES `signatures_publishers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `system` FOREIGN KEY (`systemid`) REFERENCES `signatures_platforms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=1466355 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -58,7 +58,7 @@ CREATE TABLE `signatures_platforms` ( PRIMARY KEY (`id`), UNIQUE KEY `idsignatures_platforms_UNIQUE` (`id`), KEY `platforms_idx` (`platform`,`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=1231 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -74,7 +74,7 @@ CREATE TABLE `signatures_publishers` ( PRIMARY KEY (`id`), UNIQUE KEY `id_UNIQUE` (`id`), KEY `publisher_idx` (`publisher`,`id`) -) ENGINE=InnoDB AUTO_INCREMENT=97693 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -104,7 +104,7 @@ CREATE TABLE `signatures_roms` ( KEY `sha1_idx` (`sha1`) USING BTREE, KEY `flags_idx` ((cast(`flags` as char(255) array))), CONSTRAINT `gameid` FOREIGN KEY (`gameid`) REFERENCES `signatures_games` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=3350963 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -131,7 +131,7 @@ CREATE TABLE `signatures_sources` ( UNIQUE KEY `id_UNIQUE` (`id`), KEY `sourcemd5_idx` (`sourcemd5`,`id`) USING BTREE, KEY `sourcesha1_idx` (`sourcesha1`,`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=7573 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; diff --git a/gaseous-tools/Database/MySQL/gaseous-1001.sql b/gaseous-tools/Database/MySQL/gaseous-1001.sql index 1b39072..98eb6e0 100644 --- a/gaseous-tools/Database/MySQL/gaseous-1001.sql +++ b/gaseous-tools/Database/MySQL/gaseous-1001.sql @@ -1,8 +1,4 @@ -CREATE - ALGORITHM = UNDEFINED - DEFINER = `root`@`localhost` - SQL SECURITY DEFINER -VIEW `view_signatures_games` AS +CREATE VIEW `view_signatures_games` AS SELECT `signatures_games`.`id` AS `id`, `signatures_games`.`name` AS `name`, diff --git a/gaseous-tools/Database/MySQL/gaseous-1002.sql b/gaseous-tools/Database/MySQL/gaseous-1002.sql new file mode 100644 index 0000000..29ed498 --- /dev/null +++ b/gaseous-tools/Database/MySQL/gaseous-1002.sql @@ -0,0 +1,6 @@ +CREATE TABLE `gaseous`.`settings` ( + `setting` VARCHAR(45) NOT NULL, + `value` LONGTEXT NULL, + UNIQUE INDEX `setting_UNIQUE` (`setting` ASC) VISIBLE, + PRIMARY KEY (`setting`)); + diff --git a/gaseous-tools/gaseous-tools.csproj b/gaseous-tools/gaseous-tools.csproj index 33e2fc7..b63788c 100644 --- a/gaseous-tools/gaseous-tools.csproj +++ b/gaseous-tools/gaseous-tools.csproj @@ -16,6 +16,7 @@ + @@ -24,5 +25,6 @@ + From 55735599f8be054c3ac45a867b900cd61ae09a5d Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sat, 11 Mar 2023 01:10:41 +1100 Subject: [PATCH 02/71] chore(deps): upgrade MySQL Connector and Newtonsoft.Json packages --- gaseous-identifier/gaseous-identifier-testapp.csproj | 2 +- gaseous-signature-ingestor/gaseous-signature-ingestor.csproj | 4 ++-- gaseous-tools/gaseous-tools.csproj | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gaseous-identifier/gaseous-identifier-testapp.csproj b/gaseous-identifier/gaseous-identifier-testapp.csproj index 02cc813..d5aea35 100644 --- a/gaseous-identifier/gaseous-identifier-testapp.csproj +++ b/gaseous-identifier/gaseous-identifier-testapp.csproj @@ -12,7 +12,7 @@ - + diff --git a/gaseous-signature-ingestor/gaseous-signature-ingestor.csproj b/gaseous-signature-ingestor/gaseous-signature-ingestor.csproj index 473788c..119e791 100644 --- a/gaseous-signature-ingestor/gaseous-signature-ingestor.csproj +++ b/gaseous-signature-ingestor/gaseous-signature-ingestor.csproj @@ -14,7 +14,7 @@ - - + + diff --git a/gaseous-tools/gaseous-tools.csproj b/gaseous-tools/gaseous-tools.csproj index b63788c..5f6b960 100644 --- a/gaseous-tools/gaseous-tools.csproj +++ b/gaseous-tools/gaseous-tools.csproj @@ -8,8 +8,8 @@ - - + + From 5260738a5be62087c9204f6b394ded7ebf089a82 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Fri, 17 Mar 2023 23:08:46 +1100 Subject: [PATCH 03/71] feat: scaffolding for background tasks complete --- .../Controllers/BackgroundTasksController.cs | 41 +++++++ .../Controllers/SignaturesController.cs | 3 +- gaseous-server/ProcessQueue.cs | 107 ++++++++++++++++++ gaseous-server/Program.cs | 18 ++- gaseous-server/Timer.cs | 27 +++-- gaseous-server/gaseous-server.csproj | 6 +- gaseous-tools/Config.cs | 93 +++++++++++++-- gaseous-tools/Database.cs | 35 ++++-- gaseous-tools/Logging.cs | 102 +++++++++++++++++ 9 files changed, 397 insertions(+), 35 deletions(-) create mode 100644 gaseous-server/Controllers/BackgroundTasksController.cs create mode 100644 gaseous-server/ProcessQueue.cs create mode 100644 gaseous-tools/Logging.cs diff --git a/gaseous-server/Controllers/BackgroundTasksController.cs b/gaseous-server/Controllers/BackgroundTasksController.cs new file mode 100644 index 0000000..062ffad --- /dev/null +++ b/gaseous-server/Controllers/BackgroundTasksController.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace gaseous_server.Controllers +{ + [ApiController] + [Route("api/v1/[controller]")] + public class BackgroundTasksController : Controller + { + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public List GetQueue() + { + return ProcessQueue.QueueItems; + } + + [HttpGet] + [Route("{TaskType}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult ForceRun(ProcessQueue.QueueItemType TaskType, Boolean ForceRun) + { + foreach (ProcessQueue.QueueItem qi in ProcessQueue.QueueItems) + { + if (TaskType == qi.ItemType) + { + if (ForceRun == true) + { + qi.ForceExecute(); + } + return qi; + } + } + + return NotFound(); + } + } +} \ No newline at end of file diff --git a/gaseous-server/Controllers/SignaturesController.cs b/gaseous-server/Controllers/SignaturesController.cs index e1f3e58..fd14de2 100644 --- a/gaseous-server/Controllers/SignaturesController.cs +++ b/gaseous-server/Controllers/SignaturesController.cs @@ -20,13 +20,14 @@ namespace gaseous_server.Controllers /// /// Number of sources, publishers, games, and rom signatures in the database [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] public Models.Signatures_Status Status() { return new Models.Signatures_Status(); } [HttpGet] - [Route("api/[controller]/[action]")] + [ProducesResponseType(StatusCodes.Status200OK)] public List GetSignature(string md5 = "", string sha1 = "") { if (md5.Length > 0) diff --git a/gaseous-server/ProcessQueue.cs b/gaseous-server/ProcessQueue.cs new file mode 100644 index 0000000..e3e1c04 --- /dev/null +++ b/gaseous-server/ProcessQueue.cs @@ -0,0 +1,107 @@ +using System; +using gaseous_tools; + +namespace gaseous_server +{ + public static class ProcessQueue + { + public static List QueueItems = new List(); + + public class QueueItem + { + public QueueItem(QueueItemType ItemType, int ExecutionInterval) + { + _ItemType = ItemType; + _ItemState = QueueItemState.NeverStarted; + _LastRunTime = DateTime.UtcNow.AddMinutes(ExecutionInterval); + _Interval = ExecutionInterval; + } + + private QueueItemType _ItemType = QueueItemType.NotConfigured; + private QueueItemState _ItemState = QueueItemState.NeverStarted; + private DateTime _LastRunTime = DateTime.UtcNow; + private DateTime _LastFinishTime = DateTime.UtcNow; + private int _Interval = 0; + private string _LastResult = ""; + private Exception? _LastError = null; + private bool _ForceExecute = false; + + public QueueItemType ItemType => _ItemType; + public QueueItemState ItemState => _ItemState; + public DateTime LastRunTime => _LastRunTime; + public DateTime LastFinishTime => _LastFinishTime; + public DateTime NextRunTime { + get + { + return LastRunTime.AddMinutes(Interval); + } + } + public int Interval => _Interval; + public string LastResult => _LastResult; + public Exception? LastError => _LastError; + public bool Force => _ForceExecute; + + public void Execute() + { + if (_ItemState != QueueItemState.Disabled) + { + if ((DateTime.UtcNow > NextRunTime || _ForceExecute == true) && _ItemState != QueueItemState.Running) + { + // we can run - do some setup before we start processing + _LastRunTime = DateTime.UtcNow; + _ItemState = QueueItemState.Running; + _LastResult = ""; + _LastError = null; + _ForceExecute = false; + + Logging.Log(Logging.LogType.Information, "Timered Event", "Executing " + _ItemType); + + try + { + switch (_ItemType) + { + case QueueItemType.SignatureIngestor: + Logging.Log(Logging.LogType.Information, "Timered Event", "Starting Signature Ingestor"); + break; + + case QueueItemType.TitleIngestor: + Logging.Log(Logging.LogType.Information, "Timered Event", "Starting Title Ingestor"); + break; + } + } + catch (Exception ex) + { + Logging.Log(Logging.LogType.Warning, "Timered Event", "An error occurred", ex); + _LastResult = ""; + _LastError = ex; + } + + _ItemState = QueueItemState.Stopped; + _LastFinishTime = DateTime.UtcNow; + } + } + } + + public void ForceExecute() + { + _ForceExecute = true; + } + } + + public enum QueueItemType + { + NotConfigured, + SignatureIngestor, + TitleIngestor + } + + public enum QueueItemState + { + NeverStarted, + Running, + Stopped, + Disabled + } + } +} + diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index f0ae836..374a2de 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -2,20 +2,31 @@ using gaseous_server; using gaseous_tools; +Logging.Log(Logging.LogType.Information, "Startup", "Starting Gaseous Server"); + // set up db Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); db.InitDB(); +// set initial values +Guid APIKey = Guid.NewGuid(); +if (Config.ReadSetting("API Key", "Test API Key") == "Test API Key") +{ + // it's a new api key save it + Logging.Log(Logging.LogType.Information, "Startup", "Setting initial API key"); + Config.SetSetting("API Key", APIKey.ToString()); +} + // set up server var builder = WebApplication.CreateBuilder(args); // Add services to the container. - builder.Services.AddControllers().AddJsonOptions(x => { // serialize enums as strings in api responses (e.g. Role) x.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -36,6 +47,9 @@ app.UseAuthorization(); app.MapControllers(); +// add background tasks +ProcessQueue.QueueItems.Add(new ProcessQueue.QueueItem(ProcessQueue.QueueItemType.SignatureIngestor, 60)); +ProcessQueue.QueueItems.Add(new ProcessQueue.QueueItem(ProcessQueue.QueueItemType.TitleIngestor, 1)); + // start the app app.Run(); - diff --git a/gaseous-server/Timer.cs b/gaseous-server/Timer.cs index 6d3a977..147898d 100644 --- a/gaseous-server/Timer.cs +++ b/gaseous-server/Timer.cs @@ -1,4 +1,5 @@ using System; +using gaseous_tools; namespace gaseous_server { @@ -6,17 +7,18 @@ namespace gaseous_server public class TimedHostedService : IHostedService, IDisposable { private int executionCount = 0; - private readonly ILogger _logger; + //private readonly ILogger _logger; private Timer _timer; - public TimedHostedService(ILogger logger) - { - _logger = logger; - } + //public TimedHostedService(ILogger logger) + //{ + // _logger = logger; + //} public Task StartAsync(CancellationToken stoppingToken) { - _logger.LogInformation("Timed Hosted Service running."); + //_logger.LogInformation("Timed Hosted Service running."); + Logging.Log(Logging.LogType.Debug, "Background", "Starting background task monitor"); _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5)); @@ -28,13 +30,20 @@ namespace gaseous_server { var count = Interlocked.Increment(ref executionCount); - _logger.LogInformation( - "Timed Hosted Service is working. Count: {Count}", count); + //_logger.LogInformation( + // "Timed Hosted Service is working. Count: {Count}", count); + + foreach (ProcessQueue.QueueItem qi in ProcessQueue.QueueItems) { + if (DateTime.UtcNow > qi.NextRunTime || qi.Force == true) { + qi.Execute(); + } + } } public Task StopAsync(CancellationToken stoppingToken) { - _logger.LogInformation("Timed Hosted Service is stopping."); + //_logger.LogInformation("Timed Hosted Service is stopping."); + Logging.Log(Logging.LogType.Debug, "Background", "Stopping background task monitor"); _timer?.Change(Timeout.Infinite, 0); diff --git a/gaseous-server/gaseous-server.csproj b/gaseous-server/gaseous-server.csproj index 35f78b0..319ca1a 100644 --- a/gaseous-server/gaseous-server.csproj +++ b/gaseous-server/gaseous-server.csproj @@ -10,8 +10,9 @@ - + + @@ -21,6 +22,9 @@ + + + diff --git a/gaseous-tools/Config.cs b/gaseous-tools/Config.cs index 948b2ac..516b747 100644 --- a/gaseous-tools/Config.cs +++ b/gaseous-tools/Config.cs @@ -1,5 +1,6 @@ using System; using System.Data; +using Google.Protobuf.WellKnownTypes; using Newtonsoft.Json; namespace gaseous_tools @@ -40,6 +41,35 @@ namespace gaseous_tools } } + public static string LogPath + { + get + { + string logPath = Path.Combine(ConfigurationPath, "Logs"); + if (!Directory.Exists(logPath)) { + Directory.CreateDirectory(logPath); + } + return logPath; + } + } + + public static string LogFilePath + { + get + { + string logPathName = Path.Combine(LogPath, "Log " + DateTime.Now.ToUniversalTime().ToString("yyyyMMdd") + ".txt"); + return logPathName; + } + } + + public static ConfigFile.Logging LoggingConfiguration + { + get + { + return _config.LoggingConfiguration; + } + } + static Config() { if (_config == null) @@ -61,8 +91,7 @@ namespace gaseous_tools // no config file! // use defaults and save _config = new ConfigFile(); - string configRaw = Newtonsoft.Json.JsonConvert.SerializeObject(_config, Newtonsoft.Json.Formatting.Indented); - File.WriteAllText(ConfigurationFilePath, configRaw); + UpdateConfig(); } } @@ -73,7 +102,14 @@ namespace gaseous_tools public static void UpdateConfig() { // save any updates to the configuration - string configRaw = Newtonsoft.Json.JsonConvert.SerializeObject(_config, Newtonsoft.Json.Formatting.Indented); + Newtonsoft.Json.JsonSerializerSettings serializerSettings = new Newtonsoft.Json.JsonSerializerSettings + { + NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore, + Formatting = Newtonsoft.Json.Formatting.Indented + }; + serializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter()); + string configRaw = Newtonsoft.Json.JsonConvert.SerializeObject(_config, serializerSettings); + if (File.Exists(ConfigurationFilePath_Backup)) { File.Delete(ConfigurationFilePath_Backup); @@ -85,7 +121,7 @@ namespace gaseous_tools File.WriteAllText(ConfigurationFilePath, configRaw); } - private static string ReadSetting(string SettingName, string DefaultValue) + public static string ReadSetting(string SettingName, string DefaultValue) { Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); string sql = "SELECT * FROM settings WHERE setting = @settingname"; @@ -93,19 +129,28 @@ namespace gaseous_tools dbDict.Add("settingname", SettingName); dbDict.Add("value", DefaultValue); - DataTable dbResponse = db.ExecuteCMD(sql, dbDict); - if (dbResponse.Rows.Count == 0) + try { - // no value with that name stored - respond with the default value - return DefaultValue; + Logging.Log(Logging.LogType.Debug, "Database", "Reading setting '" + SettingName + "'"); + DataTable dbResponse = db.ExecuteCMD(sql, dbDict); + if (dbResponse.Rows.Count == 0) + { + // no value with that name stored - respond with the default value + return DefaultValue; + } + else + { + return (string)dbResponse.Rows[0][0]; + } } - else + catch (Exception ex) { - return (string)dbResponse.Rows[0][0]; + Logging.Log(Logging.LogType.Critical, "Database", "Failed reading setting " + SettingName, ex); + throw; } } - private static void SetSetting(string SettingName, string Value) + public static void SetSetting(string SettingName, string Value) { Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); string sql = "REPLACE INTO settings (setting, value) VALUES (@settingname, @value)"; @@ -113,7 +158,16 @@ namespace gaseous_tools dbDict.Add("settingname", SettingName); dbDict.Add("value", Value); - db.ExecuteCMD(sql, dbDict); + Logging.Log(Logging.LogType.Debug, "Database", "Storing setting '" + SettingName + "' to value: '" + Value + "'"); + try + { + db.ExecuteCMD(sql, dbDict); + } + catch (Exception ex) + { + Logging.Log(Logging.LogType.Critical, "Database", "Failed storing setting" + SettingName, ex); + throw; + } } public class ConfigFile @@ -123,6 +177,8 @@ namespace gaseous_tools [JsonIgnore] public Library LibraryConfiguration = new Library(); + public Logging LoggingConfiguration = new Logging(); + public class Database { public string HostName = "localhost"; @@ -180,6 +236,19 @@ namespace gaseous_tools } } } + + public class Logging + { + public bool DebugLogging = false; + + public LoggingFormat LogFormat = Logging.LoggingFormat.Json; + + public enum LoggingFormat + { + Json, + Text + } + } } } } diff --git a/gaseous-tools/Database.cs b/gaseous-tools/Database.cs index e11afc0..608c426 100644 --- a/gaseous-tools/Database.cs +++ b/gaseous-tools/Database.cs @@ -64,6 +64,7 @@ namespace gaseous_tools // check if the database exists first - first run must have permissions to create a database string sql = "CREATE DATABASE IF NOT EXISTS `" + Config.DatabaseConfiguration.DatabaseName + "`;"; Dictionary dbDict = new Dictionary(); + Logging.Log(Logging.LogType.Information, "Database", "Creating database if it doesn't exist"); ExecuteCMD(sql, dbDict, 30, "server=" + Config.DatabaseConfiguration.HostName + ";port=" + Config.DatabaseConfiguration.Port + ";userid=" + Config.DatabaseConfiguration.UserName + ";password=" + Config.DatabaseConfiguration.Password); // check if schema version table is in place - if not, create the schema version table @@ -71,8 +72,9 @@ namespace gaseous_tools DataTable SchemaVersionPresent = ExecuteCMD(sql, dbDict); if (SchemaVersionPresent.Rows.Count == 0) { - // no schema table present - create it - sql = "CREATE TABLE `schema_version` (`schema_version` INT NOT NULL, PRIMARY KEY (`schema_version`)); INSERT INTO `schema_version` (`schema_version`) VALUES (0);"; + // no schema table present - create it + Logging.Log(Logging.LogType.Information, "Database", "Schema version table doesn't exist. Creating it."); + sql = "CREATE TABLE `schema_version` (`schema_version` INT NOT NULL, PRIMARY KEY (`schema_version`)); INSERT INTO `schema_version` (`schema_version`) VALUES (0);"; ExecuteCMD(sql, dbDict); } @@ -87,7 +89,7 @@ namespace gaseous_tools using (Stream stream = assembly.GetManifestResourceStream(resourceName)) using (StreamReader reader = new StreamReader(stream)) { - dbScript = reader.ReadToEnd(); + dbScript = reader.ReadToEnd(); // apply script sql = "SELECT schema_version FROM schema_version;"; @@ -95,16 +97,19 @@ namespace gaseous_tools DataTable SchemaVersion = ExecuteCMD(sql, dbDict); if (SchemaVersion.Rows.Count == 0) { - // something is broken here... where's the table? - throw new Exception("schema_version table is missing!"); + // something is broken here... where's the table? + Logging.Log(Logging.LogType.Critical, "Database", "Schema table missing! This shouldn't happen!"); + throw new Exception("schema_version table is missing!"); } else { int SchemaVer = (int)SchemaVersion.Rows[0][0]; - if (SchemaVer < i) + Logging.Log(Logging.LogType.Information, "Database", "Schema version is " + SchemaVer); + if (SchemaVer < i) { - // apply schema! - ExecuteCMD(dbScript, dbDict); + // apply schema! + Logging.Log(Logging.LogType.Information, "Database", "Schema update available - applying"); + ExecuteCMD(dbScript, dbDict); sql = "UPDATE schema_version SET schema_version=@schemaver"; dbDict = new Dictionary(); @@ -115,7 +120,8 @@ namespace gaseous_tools } } } - break; + Logging.Log(Logging.LogType.Information, "Database", "Database setup complete"); + break; } } @@ -161,6 +167,7 @@ namespace gaseous_tools { DataTable RetTable = new DataTable(); + Logging.Log(Logging.LogType.Debug, "Database", "Connecting to database"); MySqlConnection conn = new MySqlConnection(DBConn); conn.Open(); @@ -178,12 +185,20 @@ namespace gaseous_tools try { - RetTable.Load(cmd.ExecuteReader()); + Logging.Log(Logging.LogType.Debug, "Database", "Executing sql: '" + SQL + "'"); + if (Parameters.Count > 0) + { + string dictValues = string.Join(";", Parameters.Select(x => string.Join("=", x.Key, x.Value))); + Logging.Log(Logging.LogType.Debug, "Database", "Parameters: " + dictValues); + } + RetTable.Load(cmd.ExecuteReader()); } catch (Exception ex) { + Logging.Log(Logging.LogType.Critical, "Database", "Error while executing '" + SQL + "'", ex); Trace.WriteLine("Error executing " + SQL); Trace.WriteLine("Full exception: " + ex.ToString()); } + Logging.Log(Logging.LogType.Debug, "Database", "Closing database connection"); conn.Close(); return RetTable; diff --git a/gaseous-tools/Logging.cs b/gaseous-tools/Logging.cs new file mode 100644 index 0000000..0243cbf --- /dev/null +++ b/gaseous-tools/Logging.cs @@ -0,0 +1,102 @@ +using System; +namespace gaseous_tools +{ + public class Logging + { + static public void Log(LogType EventType, string Section, string Message, Exception? ExceptionValue = null) + { + LogItem logItem = new LogItem + { + EventTime = DateTime.UtcNow, + EventType = EventType, + Section = Section, + Message = Message, + ExceptionValue = ExceptionValue + }; + + bool AllowWrite = false; + if (EventType == LogType.Debug) + { + if (Config.LoggingConfiguration.DebugLogging == true) + { + AllowWrite = true; + } + } + else + { + AllowWrite = true; + } + + if (AllowWrite == true) + { + // console output + string TraceOutput = logItem.EventTime.ToString("yyyyMMdd HHmmss") + ": " + logItem.EventType.ToString() + ": " + logItem.Section + ": " + logItem.Message; + if (logItem.ExceptionValue != null) + { + TraceOutput += Environment.NewLine + logItem.ExceptionValue.ToString(); + } + Console.WriteLine(TraceOutput); + + StreamWriter LogFile = File.AppendText(Config.LogFilePath); + switch (Config.LoggingConfiguration.LogFormat) + { + case Config.ConfigFile.Logging.LoggingFormat.Text: + LogFile.WriteLine(TraceOutput); + break; + + case Config.ConfigFile.Logging.LoggingFormat.Json: + Newtonsoft.Json.JsonSerializerSettings serializerSettings = new Newtonsoft.Json.JsonSerializerSettings + { + NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore, + Formatting = Newtonsoft.Json.Formatting.Indented + }; + serializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter()); + string JsonOutput = Newtonsoft.Json.JsonConvert.SerializeObject(logItem, serializerSettings); + LogFile.WriteLine(JsonOutput); + break; + } + LogFile.Close(); + } + } + + public enum LogType + { + Information = 0, + Debug = 1, + Warning = 2, + Critical = 3 + } + + public class LogItem + { + public DateTime EventTime { get; set; } + public LogType EventType { get; set; } + private string _Section = ""; + public string Section + { + get + { + return _Section; + } + set + { + _Section = value; + } + } + private string _Message = ""; + public string Message + { + get + { + return _Message; + } + set + { + _Message = value; + } + } + public Exception? ExceptionValue { get; set; } + } + } +} + From 1ad840750e700907e5dd0f90fe75beed7d36481d Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sat, 18 Mar 2023 07:38:46 +1100 Subject: [PATCH 04/71] feat: can now run hourly (or by force) the TOSEC ingestor --- .../Classes/SignatureIngestors/TOSEC.cs | 221 ++++++++++++++++++ gaseous-server/ProcessQueue.cs | 4 +- gaseous-server/Program.cs | 3 + gaseous-server/gaseous-server.csproj | 24 +- gaseous-tools/Config.cs | 35 ++- 5 files changed, 275 insertions(+), 12 deletions(-) create mode 100644 gaseous-server/Classes/SignatureIngestors/TOSEC.cs diff --git a/gaseous-server/Classes/SignatureIngestors/TOSEC.cs b/gaseous-server/Classes/SignatureIngestors/TOSEC.cs new file mode 100644 index 0000000..f6b1a2a --- /dev/null +++ b/gaseous-server/Classes/SignatureIngestors/TOSEC.cs @@ -0,0 +1,221 @@ +using System; +using System.IO; +using MySql.Data.MySqlClient; +using gaseous_romsignatureobject; +using gaseous_signature_parser.parsers; +using gaseous_tools; +using MySqlX.XDevAPI; + +namespace gaseous_server.SignatureIngestors.TOSEC +{ + public class TOSECIngestor + { + public void Import(string SearchPath) + { + // connect to database + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + + // process provided files + Logging.Log(Logging.LogType.Information, "Signature Ingestor - TOSEC", "Importing from " + SearchPath); + if (Directory.Exists(Config.LibraryConfiguration.LibrarySignatureImportDirectory_TOSEC)) + { + string[] tosecPathContents = Directory.GetFiles(SearchPath); + Array.Sort(tosecPathContents); + + string sql = ""; + Dictionary dbDict = new Dictionary(); + System.Data.DataTable sigDB; + + for (UInt16 i = 0; i < tosecPathContents.Length; ++i) + { + string tosecXMLFile = tosecPathContents[i]; + + // check tosec file md5 + Logging.Log(Logging.LogType.Information, "Signature Ingestor - TOSEC", "Checking file: " + tosecXMLFile); + Common.hashObject hashObject = new Common.hashObject(tosecXMLFile); + sql = "SELECT * FROM signatures_sources WHERE sourcemd5=@sourcemd5"; + dbDict = new Dictionary(); + dbDict.Add("sourcemd5", hashObject.md5hash); + sigDB = db.ExecuteCMD(sql, dbDict); + + if (sigDB.Rows.Count == 0) + { + // start parsing file + TosecParser tosecParser = new TosecParser(); + RomSignatureObject tosecObject = tosecParser.Parse(tosecXMLFile); + + // store in database + + // store source object + bool processGames = false; + if (tosecObject.SourceMd5 != null) + { + sql = "SELECT * FROM signatures_sources WHERE sourcemd5=@sourcemd5"; + dbDict = new Dictionary(); + dbDict.Add("name", Common.ReturnValueIfNull(tosecObject.Name, "")); + dbDict.Add("description", Common.ReturnValueIfNull(tosecObject.Description, "")); + dbDict.Add("category", Common.ReturnValueIfNull(tosecObject.Category, "")); + dbDict.Add("version", Common.ReturnValueIfNull(tosecObject.Version, "")); + dbDict.Add("author", Common.ReturnValueIfNull(tosecObject.Author, "")); + dbDict.Add("email", Common.ReturnValueIfNull(tosecObject.Email, "")); + dbDict.Add("homepage", Common.ReturnValueIfNull(tosecObject.Homepage, "")); + dbDict.Add("uri", Common.ReturnValueIfNull(tosecObject.Url, "")); + dbDict.Add("sourcetype", Common.ReturnValueIfNull(tosecObject.SourceType, "")); + dbDict.Add("sourcemd5", tosecObject.SourceMd5); + dbDict.Add("sourcesha1", tosecObject.SourceSHA1); + + sigDB = db.ExecuteCMD(sql, dbDict); + if (sigDB.Rows.Count == 0) + { + // entry not present, insert it + sql = "INSERT INTO signatures_sources (name, description, category, version, author, email, homepage, url, sourcetype, sourcemd5, sourcesha1) VALUES (@name, @description, @category, @version, @author, @email, @homepage, @uri, @sourcetype, @sourcemd5, @sourcesha1)"; + + db.ExecuteCMD(sql, dbDict); + + processGames = true; + } + + if (processGames == true) + { + for (int x = 0; x < tosecObject.Games.Count; ++x) + { + RomSignatureObject.Game gameObject = tosecObject.Games[x]; + + // set up game dictionary + dbDict = new Dictionary(); + dbDict.Add("name", Common.ReturnValueIfNull(gameObject.Name, "")); + dbDict.Add("description", Common.ReturnValueIfNull(gameObject.Description, "")); + dbDict.Add("year", Common.ReturnValueIfNull(gameObject.Year, "")); + dbDict.Add("publisher", Common.ReturnValueIfNull(gameObject.Publisher, "")); + dbDict.Add("demo", (int)gameObject.Demo); + dbDict.Add("system", Common.ReturnValueIfNull(gameObject.System, "")); + dbDict.Add("platform", Common.ReturnValueIfNull(gameObject.System, "")); + dbDict.Add("systemvariant", Common.ReturnValueIfNull(gameObject.SystemVariant, "")); + dbDict.Add("video", Common.ReturnValueIfNull(gameObject.Video, "")); + dbDict.Add("country", Common.ReturnValueIfNull(gameObject.Country, "")); + dbDict.Add("language", Common.ReturnValueIfNull(gameObject.Language, "")); + dbDict.Add("copyright", Common.ReturnValueIfNull(gameObject.Copyright, "")); + + // store platform + int gameSystem = 0; + if (gameObject.System != null) + { + sql = "SELECT id FROM signatures_platforms WHERE platform=@platform"; + + sigDB = db.ExecuteCMD(sql, dbDict); + if (sigDB.Rows.Count == 0) + { + // entry not present, insert it + sql = "INSERT INTO signatures_platforms (platform) VALUES (@platform); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; + sigDB = db.ExecuteCMD(sql, dbDict); + + gameSystem = Convert.ToInt32(sigDB.Rows[0][0]); + } + else + { + gameSystem = (int)sigDB.Rows[0][0]; + } + } + dbDict.Add("systemid", gameSystem); + + // store publisher + int gamePublisher = 0; + if (gameObject.Publisher != null) + { + sql = "SELECT * FROM signatures_publishers WHERE publisher=@publisher"; + + sigDB = db.ExecuteCMD(sql, dbDict); + if (sigDB.Rows.Count == 0) + { + // entry not present, insert it + sql = "INSERT INTO signatures_publishers (publisher) VALUES (@publisher); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; + sigDB = db.ExecuteCMD(sql, dbDict); + gamePublisher = Convert.ToInt32(sigDB.Rows[0][0]); + } + else + { + gamePublisher = (int)sigDB.Rows[0][0]; + } + } + dbDict.Add("publisherid", gamePublisher); + + // store game + int gameId = 0; + sql = "SELECT * FROM signatures_games WHERE name=@name AND year=@year AND publisherid=@publisher AND systemid=@systemid AND country=@country AND language=@language"; + + sigDB = db.ExecuteCMD(sql, dbDict); + if (sigDB.Rows.Count == 0) + { + // entry not present, insert it + sql = "INSERT INTO signatures_games " + + "(name, description, year, publisherid, demo, systemid, systemvariant, video, country, language, copyright) VALUES " + + "(@name, @description, @year, @publisherid, @demo, @systemid, @systemvariant, @video, @country, @language, @copyright); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; + sigDB = db.ExecuteCMD(sql, dbDict); + + gameId = Convert.ToInt32(sigDB.Rows[0][0]); + } + else + { + gameId = (int)sigDB.Rows[0][0]; + } + + // store rom + foreach (RomSignatureObject.Game.Rom romObject in gameObject.Roms) + { + if (romObject.Md5 != null) + { + int romId = 0; + sql = "SELECT * FROM signatures_roms WHERE gameid=@gameid AND md5=@md5"; + dbDict = new Dictionary(); + dbDict.Add("gameid", gameId); + dbDict.Add("name", Common.ReturnValueIfNull(romObject.Name, "")); + dbDict.Add("size", Common.ReturnValueIfNull(romObject.Size, "")); + dbDict.Add("crc", Common.ReturnValueIfNull(romObject.Crc, "")); + dbDict.Add("md5", romObject.Md5); + dbDict.Add("sha1", Common.ReturnValueIfNull(romObject.Sha1, "")); + dbDict.Add("developmentstatus", Common.ReturnValueIfNull(romObject.DevelopmentStatus, "")); + + if (romObject.flags != null) + { + if (romObject.flags.Count > 0) + { + dbDict.Add("flags", Newtonsoft.Json.JsonConvert.SerializeObject(romObject.flags)); + } + else + { + dbDict.Add("flags", "[ ]"); + } + } + else + { + dbDict.Add("flags", "[ ]"); + } + dbDict.Add("romtype", (int)romObject.RomType); + dbDict.Add("romtypemedia", Common.ReturnValueIfNull(romObject.RomTypeMedia, "")); + dbDict.Add("medialabel", Common.ReturnValueIfNull(romObject.MediaLabel, "")); + + sigDB = db.ExecuteCMD(sql, dbDict); + if (sigDB.Rows.Count == 0) + { + // entry not present, insert it + sql = "INSERT INTO signatures_roms (gameid, name, size, crc, md5, sha1, developmentstatus, flags, romtype, romtypemedia, medialabel) VALUES (@gameid, @name, @size, @crc, @md5, @sha1, @developmentstatus, @flags, @romtype, @romtypemedia, @medialabel); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; + sigDB = db.ExecuteCMD(sql, dbDict); + + + romId = Convert.ToInt32(sigDB.Rows[0][0]); + } + else + { + romId = (int)sigDB.Rows[0][0]; + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/gaseous-server/ProcessQueue.cs b/gaseous-server/ProcessQueue.cs index e3e1c04..1dc60d0 100644 --- a/gaseous-server/ProcessQueue.cs +++ b/gaseous-server/ProcessQueue.cs @@ -52,7 +52,6 @@ namespace gaseous_server _ItemState = QueueItemState.Running; _LastResult = ""; _LastError = null; - _ForceExecute = false; Logging.Log(Logging.LogType.Information, "Timered Event", "Executing " + _ItemType); @@ -62,6 +61,8 @@ namespace gaseous_server { case QueueItemType.SignatureIngestor: Logging.Log(Logging.LogType.Information, "Timered Event", "Starting Signature Ingestor"); + SignatureIngestors.TOSEC.TOSECIngestor tIngest = new SignatureIngestors.TOSEC.TOSECIngestor(); + tIngest.Import(Config.LibraryConfiguration.LibrarySignatureImportDirectory_TOSEC); break; case QueueItemType.TitleIngestor: @@ -76,6 +77,7 @@ namespace gaseous_server _LastError = ex; } + _ForceExecute = false; _ItemState = QueueItemState.Stopped; _LastFinishTime = DateTime.UtcNow; } diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index 374a2de..cf33cca 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -47,6 +47,9 @@ app.UseAuthorization(); app.MapControllers(); +// setup library directories +Config.LibraryConfiguration.InitLibrary(); + // add background tasks ProcessQueue.QueueItems.Add(new ProcessQueue.QueueItem(ProcessQueue.QueueItemType.SignatureIngestor, 60)); ProcessQueue.QueueItems.Add(new ProcessQueue.QueueItem(ProcessQueue.QueueItemType.TitleIngestor, 1)); diff --git a/gaseous-server/gaseous-server.csproj b/gaseous-server/gaseous-server.csproj index 319ca1a..0383705 100644 --- a/gaseous-server/gaseous-server.csproj +++ b/gaseous-server/gaseous-server.csproj @@ -15,6 +15,19 @@ + + + + + + + + + + + + + @@ -22,17 +35,8 @@ - + - - - - - - - - - diff --git a/gaseous-tools/Config.cs b/gaseous-tools/Config.cs index 516b747..95553bd 100644 --- a/gaseous-tools/Config.cs +++ b/gaseous-tools/Config.cs @@ -41,6 +41,14 @@ namespace gaseous_tools } } + public static ConfigFile.Library LibraryConfiguration + { + get + { + return _config.LibraryConfiguration; + } + } + public static string LogPath { get @@ -235,6 +243,32 @@ namespace gaseous_tools return Path.Combine(LibraryRootDirectory, "Library"); } } + + public string LibrarySignatureImportDirectory + { + get + { + return Path.Combine(LibraryRootDirectory, "Signatures"); + } + } + + public string LibrarySignatureImportDirectory_TOSEC + { + get + { + return Path.Combine(LibrarySignatureImportDirectory, "TOSEC"); + } + } + + public void InitLibrary() + { + if (!Directory.Exists(LibraryRootDirectory)) { Directory.CreateDirectory(LibraryRootDirectory); } + if (!Directory.Exists(LibraryUploadDirectory)) { Directory.CreateDirectory(LibraryUploadDirectory); } + if (!Directory.Exists(LibraryImportDirectory)) { Directory.CreateDirectory(LibraryImportDirectory); } + if (!Directory.Exists(LibraryDataDirectory)) { Directory.CreateDirectory(LibraryDataDirectory); } + if (!Directory.Exists(LibrarySignatureImportDirectory)) { Directory.CreateDirectory(LibrarySignatureImportDirectory); } + if (!Directory.Exists(LibrarySignatureImportDirectory_TOSEC)) { Directory.CreateDirectory(LibrarySignatureImportDirectory_TOSEC); } + } } public class Logging @@ -252,4 +286,3 @@ namespace gaseous_tools } } } - From b36b3a8f57efb20bc9ecca5133ee62f22ff7e963 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sun, 9 Apr 2023 01:19:50 +1000 Subject: [PATCH 05/71] feat: now pulls platform information and stores it in the database --- gaseous-server/Classes/ImportGames.cs | 102 ++++++++ gaseous-server/Classes/Platforms.cs | 229 ++++++++++++++++++ .../Classes/SignatureIngestors/TOSEC.cs | 7 +- gaseous-server/Models/PlatformMapping.cs | 60 +++++ gaseous-server/Models/Signatures_Games.cs | 137 +++++++++++ gaseous-server/ProcessQueue.cs | 7 +- gaseous-server/Program.cs | 3 + gaseous-server/Support/PlatformMap.json | 66 +++++ gaseous-server/gaseous-server.csproj | 18 ++ gaseous-server/wwwroot/index.html | 10 + gaseous-tools/Common.cs | 11 +- gaseous-tools/Config.cs | 25 +- gaseous-tools/Database/MySQL/gaseous-1003.sql | 24 ++ gaseous-tools/gaseous-tools.csproj | 2 + 14 files changed, 686 insertions(+), 15 deletions(-) create mode 100644 gaseous-server/Classes/ImportGames.cs create mode 100644 gaseous-server/Classes/Platforms.cs create mode 100644 gaseous-server/Models/PlatformMapping.cs create mode 100644 gaseous-server/Support/PlatformMap.json create mode 100644 gaseous-server/wwwroot/index.html create mode 100644 gaseous-tools/Database/MySQL/gaseous-1003.sql diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs new file mode 100644 index 0000000..2cddbe9 --- /dev/null +++ b/gaseous-server/Classes/ImportGames.cs @@ -0,0 +1,102 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using gaseous_tools; + +namespace gaseous_server.Classes +{ + public class ImportGames + { + public ImportGames(string ImportPath) + { + if (Directory.Exists(ImportPath)) + { + string[] importContents_Files = Directory.GetFiles(ImportPath); + string[] importContents_Directories = Directory.GetDirectories(ImportPath); + + // import files first + foreach (string importContent in importContents_Files) { + ImportGame importGame = new ImportGame(); + importGame.ImportGameFile(importContent); + } + } + else + { + Logging.Log(Logging.LogType.Critical, "Import Games", "The import directory " + ImportPath + " does not exist."); + throw new DirectoryNotFoundException("Invalid path: " + ImportPath); + } + } + + + } + + public class ImportGame + { + private Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + + public void ImportGameFile(string GameFileImportPath, bool IsDirectory = false) + { + Logging.Log(Logging.LogType.Information, "Import Game", "Processing item " + GameFileImportPath); + if (IsDirectory == false) + { + FileInfo fi = new FileInfo(GameFileImportPath); + + // process as a single file + // check 1: do we have a signature for it? + Common.hashObject hash = new Common.hashObject(GameFileImportPath); + gaseous_server.Controllers.SignaturesController sc = new Controllers.SignaturesController(); + List signatures = sc.GetSignature(hash.md5hash); + if (signatures.Count == 0) + { + // no md5 signature found - try sha1 + signatures = sc.GetSignature("", hash.sha1hash); + } + + Models.Signatures_Games discoveredSignature = new Models.Signatures_Games(); + if (signatures.Count == 1) + { + // only 1 signature found! + discoveredSignature = signatures.ElementAt(0); + gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, false); + } + else if (signatures.Count > 1) + { + // more than one signature found - find one with highest score + foreach(Models.Signatures_Games Sig in signatures) + { + if (Sig.Score > discoveredSignature.Score) + { + discoveredSignature = Sig; + gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, false); + } + } + } + else + { + // no signature match found - try alternate methods + Models.Signatures_Games.GameItem gi = new Models.Signatures_Games.GameItem(); + Models.Signatures_Games.RomItem ri = new Models.Signatures_Games.RomItem(); + + // game title is the file name without the extension or path + gi.Name = Path.GetFileNameWithoutExtension(GameFileImportPath); + + // guess platform + gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, true); + + // get rom data + ri.Name = Path.GetFileName(GameFileImportPath); + ri.Md5 = hash.md5hash; + ri.Sha1 = hash.sha1hash; + + discoveredSignature.Game = gi; + discoveredSignature.Rom = ri; + } + + //Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(discoveredSignature)); + + IGDB.Models.Platform determinedPlatform = Platforms.GetPlatform(discoveredSignature.Flags.IGDBPlatformId); + } + } + } +} + diff --git a/gaseous-server/Classes/Platforms.cs b/gaseous-server/Classes/Platforms.cs new file mode 100644 index 0000000..eae183d --- /dev/null +++ b/gaseous-server/Classes/Platforms.cs @@ -0,0 +1,229 @@ +using System; +using System.Data; +using gaseous_tools; +using IGDB; +using IGDB.Models; + +namespace gaseous_server.Classes +{ + public class Platforms + { + public Platforms() + { + } + + public static Platform UnknownPlatform + { + get + { + Platform unkownPlatform = new Platform + { + Id = 0, + Abbreviation = "", + AlternativeName = "", + Category = PlatformCategory.Computer, + Checksum = "", + CreatedAt = DateTime.UtcNow, + Generation = 1, + Name = "Unknown", + PlatformFamily = new IdentityOrValue(0), + PlatformLogo = new IdentityOrValue(0), + Slug = "Unknown", + Summary = "", + UpdatedAt = DateTime.UtcNow, + Url = "", + Versions = new IdentitiesOrValues(), + Websites = new IdentitiesOrValues() + }; + + return unkownPlatform; + } + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static Platform GetPlatform(int Id) + { + if (Id == 0) + { + return UnknownPlatform; + } + else + { + Task RetVal = _GetPlatform(SearchUsing.id, Id); + return RetVal.Result; + } + } + + public static Platform GetPlatform(string Slug) + { + Task RetVal = _GetPlatform(SearchUsing.slug, Slug); + return RetVal.Result; + } + + private static async Task _GetPlatform(SearchUsing searchUsing, object searchValue) + { + // check database first + Platform? platform = DBGetPlatform(searchUsing, searchValue); + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + if (platform == null) + { + var results = await igdb.QueryAsync(IGDBClient.Endpoints.Platforms, query: "fields abbreviation,alternative_name,category,checksum,created_at,generation,name,platform_family,platform_logo,slug,summary,updated_at,url,versions,websites; " + WhereClause + ";"); + var result = results.First(); + + DBInsertPlatform(result, true); + + return result; + } + else + { + return platform; + } + } + + private static Platform? DBGetPlatform(SearchUsing searchUsing, object searchValue) + { + Dictionary dbDict = new Dictionary(); + switch (searchUsing) + { + case SearchUsing.id: + dbDict.Add("id", searchValue); + + return _DBGetPlatform("SELECT * FROM platforms WHERE id = @id", dbDict); + + case SearchUsing.slug: + dbDict.Add("slug", searchValue); + + return _DBGetPlatform("SELECT * FROM platforms WHERE slug = @slug", dbDict); + + default: + throw new Exception("Invalid Search Type"); + } + } + + private enum SearchUsing + { + id, + slug + } + + private static Platform? _DBGetPlatform(string sql, Dictionary searchParams) + { + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + + DataTable dbResponse = db.ExecuteCMD(sql, searchParams); + + if (dbResponse.Rows.Count > 0) + { + return ConvertDataRowToPlatform(dbResponse.Rows[0]); + } + else + { + return null; + } + } + + private static Platform ConvertDataRowToPlatform(DataRow PlatformDR) + { + Platform returnPlatform = new Platform + { + Id = (long)(UInt64)PlatformDR["id"], + Abbreviation = (string?)PlatformDR["abbreviation"], + AlternativeName = (string?)PlatformDR["alternative_name"], + Category = (PlatformCategory)PlatformDR["category"], + Checksum = (string?)PlatformDR["checksum"], + CreatedAt = (DateTime?)PlatformDR["created_at"], + Generation = (int?)PlatformDR["generation"], + Name = (string?)PlatformDR["name"], + PlatformFamily = new IdentityOrValue((int?)PlatformDR["platform_family"]), + PlatformLogo = new IdentityOrValue((int?)PlatformDR["platform_logo"]), + Slug = (string?)PlatformDR["slug"], + Summary = (string?)PlatformDR["summary"], + UpdatedAt = (DateTime?)PlatformDR["updated_at"], + Url = (string?)PlatformDR["url"], + Versions = Newtonsoft.Json.JsonConvert.DeserializeObject>((string?)PlatformDR["versions"]), + Websites = Newtonsoft.Json.JsonConvert.DeserializeObject>((string?)PlatformDR["websites"]) + }; + + return returnPlatform; + } + + private static void DBInsertPlatform(Platform PlatformItem, bool Insert) + { + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "INSERT INTO platforms (id, abbreviation, alternative_name, category, checksum, created_at, generation, name, platform_family, platform_logo, slug, summary, updated_at, url, versions, websites, dateAdded, lastUpdated) VALUES (@id, @abbreviation, @alternative_name, @category, @checksum, @created_at, @generation, @name, @platform_family, @platform_logo, @slug, @summary, @updated_at, @url, @versions, @websites, @lastUpdated, @lastUpdated)"; + if (Insert == false) + { + sql = "UPDATE platforms SET abbreviation=@abbreviation, alternative_name=@alternative_name, category=@category, checksum=@checksum, created_at=@created_at, generation=@generation, name=@name, platform_family=@platform_family, platform_logo=@platform_logo, slug=@slug, summary=@summary, updated_at=@updated_at, url=@url, versions=@versions, websites=@websites, lastUpdated=@lastUpdated WHERE id=@id"; + } + Dictionary dbDict = new Dictionary(); + dbDict.Add("id", PlatformItem.Id); + dbDict.Add("abbreviation", Common.ReturnValueIfNull(PlatformItem.Abbreviation, "")); + dbDict.Add("alternative_name", Common.ReturnValueIfNull(PlatformItem.AlternativeName, "")); + dbDict.Add("category", Common.ReturnValueIfNull(PlatformItem.Category, PlatformCategory.Computer)); + dbDict.Add("checksum", Common.ReturnValueIfNull(PlatformItem.Checksum, "")); + dbDict.Add("created_at", Common.ReturnValueIfNull(PlatformItem.CreatedAt, DateTime.UtcNow)); + dbDict.Add("generation", Common.ReturnValueIfNull(PlatformItem.Generation, 1)); + dbDict.Add("name", Common.ReturnValueIfNull(PlatformItem.Name, "")); + if (PlatformItem.PlatformFamily == null) + { + dbDict.Add("platform_family", 0); + } + else + { + dbDict.Add("platform_family", Common.ReturnValueIfNull(PlatformItem.PlatformFamily.Id, 0)); + } + if (PlatformItem.PlatformLogo == null) + { + dbDict.Add("platform_logo", 0); + } + else + { + dbDict.Add("platform_logo", Common.ReturnValueIfNull(PlatformItem.PlatformLogo.Id, 0)); + } + dbDict.Add("slug", Common.ReturnValueIfNull(PlatformItem.Slug, "")); + dbDict.Add("summary", Common.ReturnValueIfNull(PlatformItem.Summary, "")); + dbDict.Add("updated_at", Common.ReturnValueIfNull(PlatformItem.UpdatedAt, DateTime.UtcNow)); + dbDict.Add("url", Common.ReturnValueIfNull(PlatformItem.Url, "")); + dbDict.Add("lastUpdated", DateTime.UtcNow); + string EmptyJson = "{\"Ids\": [], \"Values\": null}"; + if (PlatformItem.Versions == null) + { + dbDict.Add("versions", EmptyJson); + } + else + { + dbDict.Add("versions", Newtonsoft.Json.JsonConvert.SerializeObject(PlatformItem.Versions)); + } + if (PlatformItem.Websites == null) + { + dbDict.Add("websites", EmptyJson); + } + else + { + dbDict.Add("websites", Newtonsoft.Json.JsonConvert.SerializeObject(PlatformItem.Websites)); + } + + db.ExecuteCMD(sql, dbDict); + } + } +} + diff --git a/gaseous-server/Classes/SignatureIngestors/TOSEC.cs b/gaseous-server/Classes/SignatureIngestors/TOSEC.cs index f6b1a2a..c1c46b4 100644 --- a/gaseous-server/Classes/SignatureIngestors/TOSEC.cs +++ b/gaseous-server/Classes/SignatureIngestors/TOSEC.cs @@ -31,7 +31,6 @@ namespace gaseous_server.SignatureIngestors.TOSEC string tosecXMLFile = tosecPathContents[i]; // check tosec file md5 - Logging.Log(Logging.LogType.Information, "Signature Ingestor - TOSEC", "Checking file: " + tosecXMLFile); Common.hashObject hashObject = new Common.hashObject(tosecXMLFile); sql = "SELECT * FROM signatures_sources WHERE sourcemd5=@sourcemd5"; dbDict = new Dictionary(); @@ -40,6 +39,8 @@ namespace gaseous_server.SignatureIngestors.TOSEC if (sigDB.Rows.Count == 0) { + Logging.Log(Logging.LogType.Information, "Signature Ingestor - TOSEC", "Importing file: " + tosecXMLFile); + // start parsing file TosecParser tosecParser = new TosecParser(); RomSignatureObject tosecObject = tosecParser.Parse(tosecXMLFile); @@ -214,6 +215,10 @@ namespace gaseous_server.SignatureIngestors.TOSEC } } } + else + { + Logging.Log(Logging.LogType.Debug, "Signature Ingestor - TOSEC", "Rejecting already imported file: " + tosecXMLFile); + } } } } diff --git a/gaseous-server/Models/PlatformMapping.cs b/gaseous-server/Models/PlatformMapping.cs new file mode 100644 index 0000000..86a1614 --- /dev/null +++ b/gaseous-server/Models/PlatformMapping.cs @@ -0,0 +1,60 @@ +using System; +using System.Reflection; + +namespace gaseous_server.Models +{ + public class PlatformMapping + { + public PlatformMapping() + { + + } + + private static List _PlatformMaps = new List(); + public static List PlatformMap + { + get + { + if (_PlatformMaps.Count == 0) + { + // load platform maps from: gaseous_server.Support.PlatformMap.json + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = "gaseous_server.Support.PlatformMap.json"; + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) + using (StreamReader reader = new StreamReader(stream)) + { + string rawJson = reader.ReadToEnd(); + _PlatformMaps.Clear(); + _PlatformMaps = Newtonsoft.Json.JsonConvert.DeserializeObject>(rawJson); + } + } + + return _PlatformMaps; + } + } + + public static void GetIGDBPlatformMapping(ref Models.Signatures_Games Signature, FileInfo RomFileInfo, bool SetSystemName) + { + foreach (Models.PlatformMapping.PlatformMapItem PlatformMapping in Models.PlatformMapping.PlatformMap) + { + if (PlatformMapping.KnownFileExtensions.Contains(RomFileInfo.Extension, StringComparer.OrdinalIgnoreCase)) + { + if (SetSystemName == true) + { + if (Signature.Game != null) { Signature.Game.System = PlatformMapping.IGDBName; } + } + Signature.Flags.IGDBPlatformId = PlatformMapping.IGDBId; + } + } + } + + public class PlatformMapItem + { + public int IGDBId { get; set; } + public string IGDBName { get; set; } + public List AlternateNames { get; set; } = new List(); + public List KnownFileExtensions { get; set; } = new List(); + } + } +} + diff --git a/gaseous-server/Models/Signatures_Games.cs b/gaseous-server/Models/Signatures_Games.cs index 9765714..8aec7a4 100644 --- a/gaseous-server/Models/Signatures_Games.cs +++ b/gaseous-server/Models/Signatures_Games.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json.Serialization; using static gaseous_romsignatureobject.RomSignatureObject.Game; namespace gaseous_server.Models @@ -12,6 +13,29 @@ namespace gaseous_server.Models public GameItem? Game { get; set; } public RomItem? Rom { get; set; } + //[JsonIgnore] + public int Score + { + get + { + int _score = 0; + + if (Game != null) + { + _score = _score + Game.Score; + } + + if (Rom != null) + { + _score = _score + Rom.Score; + } + + return _score; + } + } + + public SignatureFlags Flags = new SignatureFlags(); + public class GameItem { public Int32? Id { get; set; } @@ -36,6 +60,60 @@ namespace gaseous_server.Models demo_rolling = 4, demo_slideshow = 5 } + + [JsonIgnore] + public int Score + { + get + { + // calculate a score based on the availablility of data + int _score = 0; + var properties = this.GetType().GetProperties(); + foreach (var prop in properties) + { + if (prop.GetGetMethod() != null) + { + switch (prop.Name.ToLower()) + { + case "id": + case "score": + break; + case "name": + case "year": + case "publisher": + case "system": + if (prop.PropertyType == typeof(string)) + { + if (prop.GetValue(this) != null) + { + string propVal = prop.GetValue(this).ToString(); + if (propVal.Length > 0) + { + _score = _score + 10; + } + } + } + break; + default: + if (prop.PropertyType == typeof(string)) + { + if (prop.GetValue(this) != null) + { + string propVal = prop.GetValue(this).ToString(); + if (propVal.Length > 0) + { + _score = _score + 1; + } + } + } + break; + } + } + } + + return _score; + } + } } public class RomItem @@ -92,6 +170,65 @@ namespace gaseous_server.Models /// Side = 6 } + + [JsonIgnore] + public int Score + { + get + { + // calculate a score based on the availablility of data + int _score = 0; + var properties = this.GetType().GetProperties(); + foreach (var prop in properties) + { + if (prop.GetGetMethod() != null) + { + switch (prop.Name.ToLower()) + { + case "name": + case "size": + case "crc": + case "developmentstatus": + case "flags": + case "romtypemedia": + case "medialabel": + if (prop.PropertyType == typeof(string) || prop.PropertyType == typeof(Int64) || prop.PropertyType == typeof(List)) + { + if (prop.GetValue(this) != null) + { + string propVal = prop.GetValue(this).ToString(); + if (propVal.Length > 0) + { + _score = _score + 10; + } + } + } + break; + default: + if (prop.PropertyType == typeof(string)) + { + if (prop.GetValue(this) != null) + { + string propVal = prop.GetValue(this).ToString(); + if (propVal.Length > 0) + { + _score = _score + 1; + } + } + } + break; + } + } + } + + return _score; + } + } + } + + public class SignatureFlags + { + public int IGDBPlatformId { get; set; } } } } diff --git a/gaseous-server/ProcessQueue.cs b/gaseous-server/ProcessQueue.cs index 1dc60d0..07b01ad 100644 --- a/gaseous-server/ProcessQueue.cs +++ b/gaseous-server/ProcessQueue.cs @@ -23,7 +23,7 @@ namespace gaseous_server private DateTime _LastFinishTime = DateTime.UtcNow; private int _Interval = 0; private string _LastResult = ""; - private Exception? _LastError = null; + private string? _LastError = null; private bool _ForceExecute = false; public QueueItemType ItemType => _ItemType; @@ -38,7 +38,7 @@ namespace gaseous_server } public int Interval => _Interval; public string LastResult => _LastResult; - public Exception? LastError => _LastError; + public string? LastError => _LastError; public bool Force => _ForceExecute; public void Execute() @@ -67,6 +67,7 @@ namespace gaseous_server case QueueItemType.TitleIngestor: Logging.Log(Logging.LogType.Information, "Timered Event", "Starting Title Ingestor"); + Classes.ImportGames importGames = new Classes.ImportGames(Config.LibraryConfiguration.LibraryImportDirectory); break; } } @@ -74,7 +75,7 @@ namespace gaseous_server { Logging.Log(Logging.LogType.Warning, "Timered Event", "An error occurred", ex); _LastResult = ""; - _LastError = ex; + _LastError = ex.ToString(); } _ForceExecute = false; diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index cf33cca..dd471da 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -45,6 +45,9 @@ app.UseHttpsRedirection(); app.UseAuthorization(); +app.UseDefaultFiles(); +app.UseStaticFiles(); + app.MapControllers(); // setup library directories diff --git a/gaseous-server/Support/PlatformMap.json b/gaseous-server/Support/PlatformMap.json new file mode 100644 index 0000000..e403b63 --- /dev/null +++ b/gaseous-server/Support/PlatformMap.json @@ -0,0 +1,66 @@ +[ + { + "IGDBId": 15, + "IGDBName": "Commodore C64/128/MAX", + "AlternateNames": [ + "C64", + "Commodore C64", + "Commodore C128", + "Commodore C64DTV", + "Commodore C64/128/MAX" + ], + "KnownFileExtensions": [ + ".C64", + ".CRT", + ".D64", + ".D71", + ".D81", + ".G64", + ".PRG", + ".T64", + ".TAP", + ".Z64" + ] + }, + { + "IGDBId": 16, + "IGDBName": "Amiga", + "AlternateNames": [ + "Amiga", + "Commodore Amiga" + ], + "KnownFileExtensions": [ + ".ADF", + ".ADZ", + ".DMS" + ] + }, + { + "IGDBId": 64, + "IGDBName": "Sega Master System/Mark III", + "AlternateNames": [ + "Sega Master System/Mark III", + "Sega Master System", + "Sega Mark III & Master System" + ], + "KnownFileExtensions": [ + ".SMS" + ] + }, + { + "IGDBId": 29, + "IGDBName": "Sega Mega Drive/Genesis", + "AlternateNames": [ + "Sega Mega Drive/Genesis", + "Sega Mega Drive", + "Sega Genesis", + "Sega Mega Drive & Genesis" + ], + "KnownFileExtensions": [ + ".GEN", + ".MD", + ".SG", + ".SMD" + ] + } +] diff --git a/gaseous-server/gaseous-server.csproj b/gaseous-server/gaseous-server.csproj index 0383705..c66b20b 100644 --- a/gaseous-server/gaseous-server.csproj +++ b/gaseous-server/gaseous-server.csproj @@ -13,6 +13,7 @@ + @@ -21,12 +22,15 @@ + + + @@ -39,4 +43,18 @@ + + + + + + + + true + PreserveNewest + + + + + diff --git a/gaseous-server/wwwroot/index.html b/gaseous-server/wwwroot/index.html new file mode 100644 index 0000000..045404b --- /dev/null +++ b/gaseous-server/wwwroot/index.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/gaseous-tools/Common.cs b/gaseous-tools/Common.cs index 00bc713..ef95b17 100644 --- a/gaseous-tools/Common.cs +++ b/gaseous-tools/Common.cs @@ -22,6 +22,13 @@ namespace gaseous_tools } } + static public DateTime ConvertUnixToDateTime(double UnixTimeStamp) + { + DateTime dateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + dateTime = dateTime.AddSeconds(UnixTimeStamp).ToLocalTime(); + return dateTime; + } + public class hashObject { public hashObject(string FileName) @@ -31,12 +38,12 @@ namespace gaseous_tools var md5 = MD5.Create(); byte[] md5HashByte = md5.ComputeHash(xmlStream); string md5Hash = BitConverter.ToString(md5HashByte).Replace("-", "").ToLowerInvariant(); - _md5hash = md5hash; + _md5hash = md5Hash; var sha1 = SHA1.Create(); byte[] sha1HashByte = sha1.ComputeHash(xmlStream); string sha1Hash = BitConverter.ToString(sha1HashByte).Replace("-", "").ToLowerInvariant(); - _sha1hash = sha1hash; + _sha1hash = sha1Hash; } string _md5hash = ""; diff --git a/gaseous-tools/Config.cs b/gaseous-tools/Config.cs index 95553bd..941ee47 100644 --- a/gaseous-tools/Config.cs +++ b/gaseous-tools/Config.cs @@ -49,6 +49,14 @@ namespace gaseous_tools } } + public static ConfigFile.IGDB IGDB + { + get + { + return _config.IGDBConfiguration; + } + } + public static string LogPath { get @@ -185,6 +193,8 @@ namespace gaseous_tools [JsonIgnore] public Library LibraryConfiguration = new Library(); + public IGDB IGDBConfiguration = new IGDB(); + public Logging LoggingConfiguration = new Logging(); public class Database @@ -220,14 +230,6 @@ namespace gaseous_tools } } - public string LibraryUploadDirectory - { - get - { - return Path.Combine(LibraryRootDirectory, "Upload"); - } - } - public string LibraryImportDirectory { get @@ -263,7 +265,6 @@ namespace gaseous_tools public void InitLibrary() { if (!Directory.Exists(LibraryRootDirectory)) { Directory.CreateDirectory(LibraryRootDirectory); } - if (!Directory.Exists(LibraryUploadDirectory)) { Directory.CreateDirectory(LibraryUploadDirectory); } if (!Directory.Exists(LibraryImportDirectory)) { Directory.CreateDirectory(LibraryImportDirectory); } if (!Directory.Exists(LibraryDataDirectory)) { Directory.CreateDirectory(LibraryDataDirectory); } if (!Directory.Exists(LibrarySignatureImportDirectory)) { Directory.CreateDirectory(LibrarySignatureImportDirectory); } @@ -271,6 +272,12 @@ namespace gaseous_tools } } + public class IGDB + { + public string ClientId = ""; + public string Secret = ""; + } + public class Logging { public bool DebugLogging = false; diff --git a/gaseous-tools/Database/MySQL/gaseous-1003.sql b/gaseous-tools/Database/MySQL/gaseous-1003.sql new file mode 100644 index 0000000..38f81ee --- /dev/null +++ b/gaseous-tools/Database/MySQL/gaseous-1003.sql @@ -0,0 +1,24 @@ + +CREATE TABLE `platforms` ( + `id` bigint unsigned NOT NULL, + `abbreviation` varchar(45) DEFAULT NULL, + `alternative_name` varchar(45) DEFAULT NULL, + `category` int DEFAULT NULL, + `checksum` varchar(45) DEFAULT NULL, + `created_at` datetime DEFAULT NULL, + `generation` int DEFAULT NULL, + `name` varchar(45) DEFAULT NULL, + `platform_family` int DEFAULT NULL, + `platform_logo` int DEFAULT NULL, + `slug` varchar(45) DEFAULT NULL, + `summary` varchar(255) DEFAULT NULL, + `updated_at` datetime DEFAULT NULL, + `url` varchar(255) DEFAULT NULL, + `versions` json DEFAULT NULL, + `websites` json DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +SELECT * FROM gaseous.platforms; \ No newline at end of file diff --git a/gaseous-tools/gaseous-tools.csproj b/gaseous-tools/gaseous-tools.csproj index 5f6b960..351f32e 100644 --- a/gaseous-tools/gaseous-tools.csproj +++ b/gaseous-tools/gaseous-tools.csproj @@ -17,6 +17,7 @@ + @@ -26,5 +27,6 @@ + From c1e963c88663c8e57d4c622088b8b1dbc7f2500b Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sun, 9 Apr 2023 01:29:24 +1000 Subject: [PATCH 06/71] doc: added a README.MD file --- README.MD | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 README.MD diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..c303d5c --- /dev/null +++ b/README.MD @@ -0,0 +1,13 @@ +# Gaseous Server + +This is the server for the Gaseous system. All your games and metadata are stored within. + +## Requirements +* MySQL Server 8+ + +## Third Party Projects +The following projects are used by Gaseous +* https://dotnet.microsoft.com/en-us/apps/aspnet +* https://github.com/JamesNK/Newtonsoft.Json +* https://www.nuget.org/packages/MySql.Data/8.0.32.1 +* https://github.com/kamranayub/igdb-dotnet From 913a7ad1e345208100f056e3f6ced40165d69138 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sun, 9 Apr 2023 23:45:25 +1000 Subject: [PATCH 07/71] feat: added platform logos class --- .../Classes/{ => Metadata}/Platforms.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) rename gaseous-server/Classes/{ => Metadata}/Platforms.cs (89%) diff --git a/gaseous-server/Classes/Platforms.cs b/gaseous-server/Classes/Metadata/Platforms.cs similarity index 89% rename from gaseous-server/Classes/Platforms.cs rename to gaseous-server/Classes/Metadata/Platforms.cs index eae183d..4d3d76f 100644 --- a/gaseous-server/Classes/Platforms.cs +++ b/gaseous-server/Classes/Metadata/Platforms.cs @@ -1,10 +1,11 @@ using System; using System.Data; +using System.Net; using gaseous_tools; using IGDB; using IGDB.Models; -namespace gaseous_server.Classes +namespace gaseous_server.Classes.Metadata { public class Platforms { @@ -86,11 +87,30 @@ namespace gaseous_server.Classes if (platform == null) { + // get platform metadata var results = await igdb.QueryAsync(IGDBClient.Endpoints.Platforms, query: "fields abbreviation,alternative_name,category,checksum,created_at,generation,name,platform_family,platform_logo,slug,summary,updated_at,url,versions,websites; " + WhereClause + ";"); var result = results.First(); DBInsertPlatform(result, true); + // get platform logo + if (result.PlatformLogo != null) + { + var logo_results = await igdb.QueryAsync(IGDBClient.Endpoints.PlatformLogos, query: "fields alpha_channel,animated,checksum,height,image_id,url,width; where id = " + result.PlatformLogo.Id + ";"); + var logo_result = logo_results.First(); + + using (var client = new HttpClient()) + { + using (var s = client.GetStreamAsync("https:" + logo_result.Url)) + { + using (var fs = new FileStream(Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(result), "platform_logo.jpg"), FileMode.OpenOrCreate)) + { + s.Result.CopyTo(fs); + } + } + } + } + return result; } else From 36616caf7bcf9a3cffaf695fe8daaa0f47bcb58d Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sun, 9 Apr 2023 23:45:48 +1000 Subject: [PATCH 08/71] feat: added support for platform logos --- gaseous-server/Classes/ImportGames.cs | 2 +- .../Classes/Metadata/PlatformLogos.cs | 40 +++++++++++++++++++ gaseous-server/Classes/Metadata/Platforms.cs | 14 +------ gaseous-server/gaseous-server.csproj | 2 + gaseous-tools/Config.cs | 17 ++++++++ gaseous-tools/gaseous-tools.csproj | 1 + 6 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 gaseous-server/Classes/Metadata/PlatformLogos.cs diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs index 2cddbe9..fd2c474 100644 --- a/gaseous-server/Classes/ImportGames.cs +++ b/gaseous-server/Classes/ImportGames.cs @@ -94,7 +94,7 @@ namespace gaseous_server.Classes //Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(discoveredSignature)); - IGDB.Models.Platform determinedPlatform = Platforms.GetPlatform(discoveredSignature.Flags.IGDBPlatformId); + IGDB.Models.Platform determinedPlatform = Metadata.Platforms.GetPlatform(discoveredSignature.Flags.IGDBPlatformId); } } } diff --git a/gaseous-server/Classes/Metadata/PlatformLogos.cs b/gaseous-server/Classes/Metadata/PlatformLogos.cs new file mode 100644 index 0000000..1f0785a --- /dev/null +++ b/gaseous-server/Classes/Metadata/PlatformLogos.cs @@ -0,0 +1,40 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; +using MySqlX.XDevAPI.Common; +using static gaseous_tools.Config.ConfigFile; + +namespace gaseous_server.Classes.Metadata +{ + public class PlatformLogos + { + public PlatformLogos() + { + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public async static void GetPlatformLogo(long Id, string LogoPath) + { + var logo_results = await igdb.QueryAsync(IGDBClient.Endpoints.PlatformLogos, query: "fields alpha_channel,animated,checksum,height,image_id,url,width; where id = " + Id + ";"); + var logo_result = logo_results.First(); + + using (var client = new HttpClient()) + { + using (var s = client.GetStreamAsync("https:" + logo_result.Url)) + { + using (var fs = new FileStream(LogoPath, FileMode.OpenOrCreate)) + { + s.Result.CopyTo(fs); + } + } + } + } + } +} + diff --git a/gaseous-server/Classes/Metadata/Platforms.cs b/gaseous-server/Classes/Metadata/Platforms.cs index 4d3d76f..17ef5d7 100644 --- a/gaseous-server/Classes/Metadata/Platforms.cs +++ b/gaseous-server/Classes/Metadata/Platforms.cs @@ -96,19 +96,7 @@ namespace gaseous_server.Classes.Metadata // get platform logo if (result.PlatformLogo != null) { - var logo_results = await igdb.QueryAsync(IGDBClient.Endpoints.PlatformLogos, query: "fields alpha_channel,animated,checksum,height,image_id,url,width; where id = " + result.PlatformLogo.Id + ";"); - var logo_result = logo_results.First(); - - using (var client = new HttpClient()) - { - using (var s = client.GetStreamAsync("https:" + logo_result.Url)) - { - using (var fs = new FileStream(Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(result), "platform_logo.jpg"), FileMode.OpenOrCreate)) - { - s.Result.CopyTo(fs); - } - } - } + PlatformLogos.GetPlatformLogo((long)result.PlatformLogo.Id, Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(result), "platform_logo.jpg")); } return result; diff --git a/gaseous-server/gaseous-server.csproj b/gaseous-server/gaseous-server.csproj index c66b20b..8c81cda 100644 --- a/gaseous-server/gaseous-server.csproj +++ b/gaseous-server/gaseous-server.csproj @@ -23,6 +23,7 @@ + @@ -31,6 +32,7 @@ + diff --git a/gaseous-tools/Config.cs b/gaseous-tools/Config.cs index 941ee47..5ca8588 100644 --- a/gaseous-tools/Config.cs +++ b/gaseous-tools/Config.cs @@ -2,6 +2,7 @@ using System.Data; using Google.Protobuf.WellKnownTypes; using Newtonsoft.Json; +using IGDB.Models; namespace gaseous_tools { @@ -246,6 +247,21 @@ namespace gaseous_tools } } + public string LibraryMetadataDirectory + { + get + { + return Path.Combine(LibraryRootDirectory, "Metadata"); + } + } + + public string LibraryMetadataDirectory_Platform(Platform platform) + { + string MetadataPath = Path.Combine(LibraryMetadataDirectory, platform.Slug); + if (!Directory.Exists(MetadataPath)) { Directory.CreateDirectory(MetadataPath); } + return MetadataPath; + } + public string LibrarySignatureImportDirectory { get @@ -267,6 +283,7 @@ namespace gaseous_tools if (!Directory.Exists(LibraryRootDirectory)) { Directory.CreateDirectory(LibraryRootDirectory); } if (!Directory.Exists(LibraryImportDirectory)) { Directory.CreateDirectory(LibraryImportDirectory); } if (!Directory.Exists(LibraryDataDirectory)) { Directory.CreateDirectory(LibraryDataDirectory); } + if (!Directory.Exists(LibraryMetadataDirectory)) { Directory.CreateDirectory(LibraryMetadataDirectory); } if (!Directory.Exists(LibrarySignatureImportDirectory)) { Directory.CreateDirectory(LibrarySignatureImportDirectory); } if (!Directory.Exists(LibrarySignatureImportDirectory_TOSEC)) { Directory.CreateDirectory(LibrarySignatureImportDirectory_TOSEC); } } diff --git a/gaseous-tools/gaseous-tools.csproj b/gaseous-tools/gaseous-tools.csproj index 351f32e..025a8d2 100644 --- a/gaseous-tools/gaseous-tools.csproj +++ b/gaseous-tools/gaseous-tools.csproj @@ -10,6 +10,7 @@ + From fb7b0a7eb2a514941b8de8f6231f7aa078740c4a Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Mon, 10 Apr 2023 01:01:23 +1000 Subject: [PATCH 09/71] feat: collects metadata for platform versions now --- .../Classes/Metadata/PlatformLogos.cs | 1 + .../Classes/Metadata/PlatformVersions.cs | 240 ++++++++++++++++++ gaseous-server/Classes/Metadata/Platforms.cs | 6 + 3 files changed, 247 insertions(+) create mode 100644 gaseous-server/Classes/Metadata/PlatformVersions.cs diff --git a/gaseous-server/Classes/Metadata/PlatformLogos.cs b/gaseous-server/Classes/Metadata/PlatformLogos.cs index 1f0785a..aeefb33 100644 --- a/gaseous-server/Classes/Metadata/PlatformLogos.cs +++ b/gaseous-server/Classes/Metadata/PlatformLogos.cs @@ -28,6 +28,7 @@ namespace gaseous_server.Classes.Metadata { using (var s = client.GetStreamAsync("https:" + logo_result.Url)) { + if (!Directory.Exists(Path.GetDirectoryName(LogoPath))) { Directory.CreateDirectory(Path.GetDirectoryName(LogoPath)); } using (var fs = new FileStream(LogoPath, FileMode.OpenOrCreate)) { s.Result.CopyTo(fs); diff --git a/gaseous-server/Classes/Metadata/PlatformVersions.cs b/gaseous-server/Classes/Metadata/PlatformVersions.cs new file mode 100644 index 0000000..cfb3ee7 --- /dev/null +++ b/gaseous-server/Classes/Metadata/PlatformVersions.cs @@ -0,0 +1,240 @@ +using System; +using System.Data; +using gaseous_tools; +using IGDB; +using IGDB.Models; + +namespace gaseous_server.Classes.Metadata +{ + public class PlatformVersions + { + public PlatformVersions() + { + } + + public static PlatformVersion UnknownPlatformVersion + { + get + { + PlatformVersion unkownPlatformVersion = new PlatformVersion + { + Id = 0, + Checksum = "", + Companies = new IdentitiesOrValues(), + Connectivity = "", + CPU = "", + Graphics = "", + MainManufacturer = new IdentityOrValue(0), + Media = "", + Memory = "", + Name = "Unknown", + OS = "", + Output = "", + PlatformLogo = new IdentityOrValue(0), + PlatformVersionReleaseDates = new IdentitiesOrValues(), + Resolutions = "", + Slug = "Unknown", + Sound = "", + Storage = "", + Summary = "", + Url = "" + }; + + return unkownPlatformVersion; + } + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static PlatformVersion GetPlatformVersion(long Id, Platform ParentPlatform) + { + if (Id == 0) + { + return UnknownPlatformVersion; + } + else + { + Task RetVal = _GetPlatformVersion(SearchUsing.id, Id, ParentPlatform); + return RetVal.Result; + } + } + + public static PlatformVersion GetPlatformVersion(string Slug, Platform ParentPlatform) + { + Task RetVal = _GetPlatformVersion(SearchUsing.slug, Slug, ParentPlatform); + return RetVal.Result; + } + + private static async Task _GetPlatformVersion(SearchUsing searchUsing, object searchValue, Platform ParentPlatform) + { + // check database first + PlatformVersion? platformVersion = DBGetPlatformVersion(searchUsing, searchValue); + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + if (platformVersion == null) + { + // get platform version metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.PlatformVersions, query: "fields checksum,companies,connectivity,cpu,graphics,main_manufacturer,media,memory,name,online,os,output,platform_logo,platform_version_release_dates,resolutions,slug,sound,storage,summary,url; " + WhereClause + ";"); + var result = results.First(); + + DBInsertPlatformVersion(result, true); + + // get platform logo + if (result.PlatformLogo != null) + { + PlatformLogos.GetPlatformLogo((long)result.PlatformLogo.Id, Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(ParentPlatform), result.Slug, "platform_logo.jpg")); + } + + return result; + } + else + { + return platformVersion; + } + } + + private static PlatformVersion? DBGetPlatformVersion(SearchUsing searchUsing, object searchValue) + { + Dictionary dbDict = new Dictionary(); + switch (searchUsing) + { + case SearchUsing.id: + dbDict.Add("id", searchValue); + + return _DBGetPlatformVersion("SELECT * FROM platforms_versions WHERE id = @id", dbDict); + + case SearchUsing.slug: + dbDict.Add("slug", searchValue); + + return _DBGetPlatformVersion("SELECT * FROM platforms_versions WHERE slug = @slug", dbDict); + + default: + throw new Exception("Invalid Search Type"); + } + } + + private enum SearchUsing + { + id, + slug + } + + private static PlatformVersion? _DBGetPlatformVersion(string sql, Dictionary searchParams) + { + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + + DataTable dbResponse = db.ExecuteCMD(sql, searchParams); + + if (dbResponse.Rows.Count > 0) + { + return ConvertDataRowToPlatformVersion(dbResponse.Rows[0]); + } + else + { + return null; + } + } + + private static PlatformVersion ConvertDataRowToPlatformVersion(DataRow PlatformDR) + { + PlatformVersion returnPlatformVersion = new PlatformVersion + { + Id = (long)(UInt64)PlatformDR["id"], + Checksum = (string?)PlatformDR["checksum"], + Companies = Newtonsoft.Json.JsonConvert.DeserializeObject>((string?)PlatformDR["companies"]), + Connectivity = (string?)PlatformDR["connectivity"], + CPU = (string?)PlatformDR["cpu"], + Graphics = (string?)PlatformDR["graphics"], + MainManufacturer = new IdentityOrValue((int?)PlatformDR["main_manufacturer"]), + Media = (string?)PlatformDR["media"], + Memory = (string?)PlatformDR["memory"], + Name = (string?)PlatformDR["name"], + OS = (string?)PlatformDR["os"], + Output = (string?)PlatformDR["output"], + PlatformLogo = new IdentityOrValue((int?)PlatformDR["platform_logo"]), + PlatformVersionReleaseDates = Newtonsoft.Json.JsonConvert.DeserializeObject>((string?)PlatformDR["platform_version_release_dates"]), + Resolutions = (string?)PlatformDR["resolutions"], + Slug = (string?)PlatformDR["slug"], + Sound = (string?)PlatformDR["sound"], + Storage = (string?)PlatformDR["storage"], + Summary = (string?)PlatformDR["summary"], + Url = (string?)PlatformDR["url"] + }; + + return returnPlatformVersion; + } + + private static void DBInsertPlatformVersion(PlatformVersion PlatformVersionItem, bool Insert) + { + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "INSERT INTO platforms_versions (id, checksum, connectivity, cpu, graphics, main_manufacturer, media, memory, name, os, output, platform_logo, platform_version_release_dates, resolutions, slug, sound, storage, summary, url, dateAdded, lastUpdated) VALUES (@id, @checksum, @connectivity, @cpu, @graphics, @main_manufacturer, @media, @memory, @name, @os, @output, @platform_logo, @platform_version_release_dates, @resolutions, @slug, @sound, @storage, @summary, @url, @lastUpdated, @lastUpdated)"; + if (Insert == false) + { + sql = "UPDATE platforms_versions SET checksum=@checksum, connectivity=@connectivity, cpu=@cpu, graphics=@graphics, main_manufacturer=@main_manufacturer, media=@media, memory=@memory, name=@name, os=@os, output=@output, platform_logo=@platform_logo, platform_version_release_dates=@platform_version_release_dates, resolutions=@resolutions, slug=@slug, sound=@sound, storage=@storage, summary=@summary, url=@url, lastUpdated=@lastUpdated WHERE id=@id"; + } + Dictionary dbDict = new Dictionary(); + string EmptyJson = "{\"Ids\": [], \"Values\": null}"; + dbDict.Add("id", PlatformVersionItem.Id); + dbDict.Add("checksum", Common.ReturnValueIfNull(PlatformVersionItem.Checksum, "")); + dbDict.Add("connectivity", Common.ReturnValueIfNull(PlatformVersionItem.Connectivity, "")); + dbDict.Add("cpu", Common.ReturnValueIfNull(PlatformVersionItem.CPU, "")); + dbDict.Add("graphics", Common.ReturnValueIfNull(PlatformVersionItem.Graphics, "")); + if (PlatformVersionItem.MainManufacturer == null) + { + dbDict.Add("main_manufacturer", 0); + } + else + { + dbDict.Add("main_manufacturer", Common.ReturnValueIfNull(PlatformVersionItem.MainManufacturer.Id, 0)); + } + dbDict.Add("media", Common.ReturnValueIfNull(PlatformVersionItem.Media, "")); + dbDict.Add("memory", Common.ReturnValueIfNull(PlatformVersionItem.Memory, "")); + dbDict.Add("name", Common.ReturnValueIfNull(PlatformVersionItem.Name, "")); + dbDict.Add("os", Common.ReturnValueIfNull(PlatformVersionItem.OS, "")); + dbDict.Add("output", Common.ReturnValueIfNull(PlatformVersionItem.Output, "")); + if (PlatformVersionItem.PlatformLogo == null) + { + dbDict.Add("platform_logo", 0); + } + else + { + dbDict.Add("platform_logo", Common.ReturnValueIfNull(PlatformVersionItem.PlatformLogo.Id, 0)); + } + if (PlatformVersionItem.PlatformVersionReleaseDates == null) + { + dbDict.Add("platform_version_release_dates", EmptyJson); + } + else + { + dbDict.Add("platform_version_release_dates", Newtonsoft.Json.JsonConvert.SerializeObject(PlatformVersionItem.PlatformVersionReleaseDates)); + } + dbDict.Add("resolutions", Common.ReturnValueIfNull(PlatformVersionItem.Resolutions, "")); + dbDict.Add("slug", Common.ReturnValueIfNull(PlatformVersionItem.Slug, "")); + dbDict.Add("sound", Common.ReturnValueIfNull(PlatformVersionItem.Sound, "")); + dbDict.Add("storage", Common.ReturnValueIfNull(PlatformVersionItem.Storage, "")); + dbDict.Add("summary", Common.ReturnValueIfNull(PlatformVersionItem.Summary, "")); + dbDict.Add("url", Common.ReturnValueIfNull(PlatformVersionItem.Url, "")); + dbDict.Add("lastUpdated", DateTime.UtcNow); + + db.ExecuteCMD(sql, dbDict); + } + } +} + diff --git a/gaseous-server/Classes/Metadata/Platforms.cs b/gaseous-server/Classes/Metadata/Platforms.cs index 17ef5d7..d181b29 100644 --- a/gaseous-server/Classes/Metadata/Platforms.cs +++ b/gaseous-server/Classes/Metadata/Platforms.cs @@ -99,6 +99,12 @@ namespace gaseous_server.Classes.Metadata PlatformLogos.GetPlatformLogo((long)result.PlatformLogo.Id, Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(result), "platform_logo.jpg")); } + // get platform versions + foreach (long platformVersionId in result.Versions.Ids) + { + PlatformVersions.GetPlatformVersion(platformVersionId, result); + } + return result; } else From 3b3cf3c2391bc04acf8f6c9c6a165789506507f9 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Mon, 1 May 2023 00:42:23 +1000 Subject: [PATCH 10/71] feat: platforms and games from IGDB are now imported successfully --- gaseous-server/Classes/ImportGames.cs | 136 ++++---- gaseous-server/Classes/Metadata/Artworks.cs | 168 ++++++++++ gaseous-server/Classes/Metadata/Covers.cs | 166 +++++++++ gaseous-server/Classes/Metadata/Games.cs | 167 ++++++++++ .../Classes/Metadata/PlatformLogos.cs | 130 +++++++- .../Classes/Metadata/PlatformVersions.cs | 203 +++-------- gaseous-server/Classes/Metadata/Platforms.cs | 214 +++--------- .../Classes/Metadata/Screenshots.cs | 168 ++++++++++ gaseous-server/Classes/Metadata/Storage.cs | 266 +++++++++++++++ gaseous-server/Models/PlatformMapping.cs | 1 + gaseous-server/Models/Signatures_Games.cs | 1 + gaseous-server/gaseous-server.csproj | 4 +- .../gaseous-signature-ingestor.csproj | 2 +- gaseous-tools/Config.cs | 9 +- gaseous-tools/Database/MySQL/gaseous-1000.sql | 314 ++++++++++++++++-- gaseous-tools/Database/MySQL/gaseous-1001.sql | 21 -- gaseous-tools/Database/MySQL/gaseous-1002.sql | 6 - gaseous-tools/Database/MySQL/gaseous-1003.sql | 24 -- gaseous-tools/gaseous-tools.csproj | 8 +- 19 files changed, 1530 insertions(+), 478 deletions(-) create mode 100644 gaseous-server/Classes/Metadata/Artworks.cs create mode 100644 gaseous-server/Classes/Metadata/Covers.cs create mode 100644 gaseous-server/Classes/Metadata/Games.cs create mode 100644 gaseous-server/Classes/Metadata/Screenshots.cs create mode 100644 gaseous-server/Classes/Metadata/Storage.cs delete mode 100644 gaseous-tools/Database/MySQL/gaseous-1001.sql delete mode 100644 gaseous-tools/Database/MySQL/gaseous-1002.sql delete mode 100644 gaseous-tools/Database/MySQL/gaseous-1003.sql diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs index fd2c474..c4c4aed 100644 --- a/gaseous-server/Classes/ImportGames.cs +++ b/gaseous-server/Classes/ImportGames.cs @@ -36,66 +36,84 @@ namespace gaseous_server.Classes public void ImportGameFile(string GameFileImportPath, bool IsDirectory = false) { - Logging.Log(Logging.LogType.Information, "Import Game", "Processing item " + GameFileImportPath); - if (IsDirectory == false) - { - FileInfo fi = new FileInfo(GameFileImportPath); - - // process as a single file - // check 1: do we have a signature for it? - Common.hashObject hash = new Common.hashObject(GameFileImportPath); - gaseous_server.Controllers.SignaturesController sc = new Controllers.SignaturesController(); - List signatures = sc.GetSignature(hash.md5hash); - if (signatures.Count == 0) - { - // no md5 signature found - try sha1 - signatures = sc.GetSignature("", hash.sha1hash); - } - - Models.Signatures_Games discoveredSignature = new Models.Signatures_Games(); - if (signatures.Count == 1) - { - // only 1 signature found! - discoveredSignature = signatures.ElementAt(0); - gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, false); - } - else if (signatures.Count > 1) - { - // more than one signature found - find one with highest score - foreach(Models.Signatures_Games Sig in signatures) - { - if (Sig.Score > discoveredSignature.Score) - { - discoveredSignature = Sig; - gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, false); - } - } - } - else - { - // no signature match found - try alternate methods - Models.Signatures_Games.GameItem gi = new Models.Signatures_Games.GameItem(); - Models.Signatures_Games.RomItem ri = new Models.Signatures_Games.RomItem(); - - // game title is the file name without the extension or path - gi.Name = Path.GetFileNameWithoutExtension(GameFileImportPath); - - // guess platform - gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, true); - - // get rom data - ri.Name = Path.GetFileName(GameFileImportPath); - ri.Md5 = hash.md5hash; - ri.Sha1 = hash.sha1hash; - - discoveredSignature.Game = gi; - discoveredSignature.Rom = ri; - } - - //Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(discoveredSignature)); - - IGDB.Models.Platform determinedPlatform = Metadata.Platforms.GetPlatform(discoveredSignature.Flags.IGDBPlatformId); + if (String.Equals(Path.GetFileName(GameFileImportPath),".DS_STORE", StringComparison.OrdinalIgnoreCase)) + { + Logging.Log(Logging.LogType.Information, "Import Game", "Skipping item " + GameFileImportPath); } + else + { + Logging.Log(Logging.LogType.Information, "Import Game", "Processing item " + GameFileImportPath); + if (IsDirectory == false) + { + FileInfo fi = new FileInfo(GameFileImportPath); + + // process as a single file + // check 1: do we have a signature for it? + Common.hashObject hash = new Common.hashObject(GameFileImportPath); + gaseous_server.Controllers.SignaturesController sc = new Controllers.SignaturesController(); + List signatures = sc.GetSignature(hash.md5hash); + if (signatures.Count == 0) + { + // no md5 signature found - try sha1 + signatures = sc.GetSignature("", hash.sha1hash); + } + + Models.Signatures_Games discoveredSignature = new Models.Signatures_Games(); + if (signatures.Count == 1) + { + // only 1 signature found! + discoveredSignature = signatures.ElementAt(0); + gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, false); + } + else if (signatures.Count > 1) + { + // more than one signature found - find one with highest score + foreach (Models.Signatures_Games Sig in signatures) + { + if (Sig.Score > discoveredSignature.Score) + { + discoveredSignature = Sig; + gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, false); + } + } + } + else + { + // no signature match found - try alternate methods + Models.Signatures_Games.GameItem gi = new Models.Signatures_Games.GameItem(); + Models.Signatures_Games.RomItem ri = new Models.Signatures_Games.RomItem(); + + discoveredSignature.Game = gi; + discoveredSignature.Rom = ri; + + // game title is the file name without the extension or path + gi.Name = Path.GetFileNameWithoutExtension(GameFileImportPath); + + // guess platform + gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, true); + + // get rom data + ri.Name = Path.GetFileName(GameFileImportPath); + ri.Md5 = hash.md5hash; + ri.Sha1 = hash.sha1hash; + } + + Console.WriteLine("Importing " + discoveredSignature.Game.Name + " (" + discoveredSignature.Game.Year + ") " + discoveredSignature.Game.System); + // get discovered platform + IGDB.Models.Platform determinedPlatform = Metadata.Platforms.GetPlatform(discoveredSignature.Flags.IGDBPlatformId); + // search discovered game + IGDB.Models.Game[] games = Metadata.Games.SearchForGame(discoveredSignature.Game.Name, discoveredSignature.Flags.IGDBPlatformId, Metadata.Games.SearchType.where); + if (games.Length == 0) + { + games = Metadata.Games.SearchForGame(discoveredSignature.Game.Name, discoveredSignature.Flags.IGDBPlatformId, Metadata.Games.SearchType.search); + } + if (games.Length > 0) + { + IGDB.Models.Game determinedGame = Metadata.Games.GetGame((long)games[0].Id); + Console.WriteLine(" IGDB game: " + determinedGame.Name); + } + } + } } } } diff --git a/gaseous-server/Classes/Metadata/Artworks.cs b/gaseous-server/Classes/Metadata/Artworks.cs new file mode 100644 index 0000000..4348941 --- /dev/null +++ b/gaseous-server/Classes/Metadata/Artworks.cs @@ -0,0 +1,168 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; +using MySqlX.XDevAPI.Common; +using static gaseous_tools.Config.ConfigFile; + +namespace gaseous_server.Classes.Metadata +{ + public class Artworks + { + const string fieldList = "fields alpha_channel,animated,checksum,game,height,image_id,url,width;"; + + public Artworks() + { + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static Artwork? GetArtwork(long? Id, string LogoPath) + { + if ((Id == 0) || (Id == null)) + { + return null; + } + else + { + Task RetVal = _GetArtwork(SearchUsing.id, Id, LogoPath); + return RetVal.Result; + } + } + + public static Artwork GetArtwork(string Slug, string LogoPath) + { + Task RetVal = _GetArtwork(SearchUsing.slug, Slug, LogoPath); + return RetVal.Result; + } + + private static async Task _GetArtwork(SearchUsing searchUsing, object searchValue, string LogoPath) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("Artwork", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("Artwork", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + Artwork returnValue = new Artwork(); + bool forceImageDownload = false; + LogoPath = Path.Combine(LogoPath, "Artwork"); + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause, LogoPath); + Storage.NewCacheValue(returnValue); + forceImageDownload = true; + break; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause, LogoPath); + Storage.NewCacheValue(returnValue, true); + forceImageDownload = true; + break; + case Storage.CacheStatus.Current: + returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + break; + default: + throw new Exception("How did you get here?"); + } + + if ((!File.Exists(Path.Combine(LogoPath, returnValue.ImageId + ".jpg"))) || forceImageDownload == true) + { + //GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_thumb, returnValue.ImageId); + //GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_logo_med, returnValue.ImageId); + GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_original, returnValue.ImageId); + } + + return returnValue; + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause, string LogoPath) + { + // get Artwork metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.Artworks, query: fieldList + " " + WhereClause + ";"); + var result = results.First(); + + //GetImageFromServer(result.Url, LogoPath, LogoSize.t_thumb, result.ImageId); + //GetImageFromServer(result.Url, LogoPath, LogoSize.t_logo_med, result.ImageId); + GetImageFromServer(result.Url, LogoPath, LogoSize.t_original, result.ImageId); + + return result; + } + + private static void GetImageFromServer(string Url, string LogoPath, LogoSize logoSize, string ImageId) + { + using (var client = new HttpClient()) + { + string fileName = "Artwork.jpg"; + string extension = "jpg"; + switch (logoSize) + { + case LogoSize.t_thumb: + fileName = "_Thumb"; + extension = "jpg"; + break; + case LogoSize.t_logo_med: + fileName = "_Medium"; + extension = "png"; + break; + case LogoSize.t_original: + fileName = "_Original"; + extension = "png"; + break; + default: + fileName = "Artwork"; + extension = "jpg"; + break; + } + fileName = ImageId + fileName; + string imageUrl = Url.Replace(LogoSize.t_thumb.ToString(), logoSize.ToString()).Replace("jpg", extension); + + using (var s = client.GetStreamAsync("https:" + imageUrl)) + { + if (!Directory.Exists(LogoPath)) { Directory.CreateDirectory(LogoPath); } + using (var fs = new FileStream(Path.Combine(LogoPath, fileName + "." + extension), FileMode.OpenOrCreate)) + { + s.Result.CopyTo(fs); + } + } + } + } + + private enum LogoSize + { + t_thumb, + t_logo_med, + t_original + } + } +} + diff --git a/gaseous-server/Classes/Metadata/Covers.cs b/gaseous-server/Classes/Metadata/Covers.cs new file mode 100644 index 0000000..a0276cc --- /dev/null +++ b/gaseous-server/Classes/Metadata/Covers.cs @@ -0,0 +1,166 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; +using MySqlX.XDevAPI.Common; +using static gaseous_tools.Config.ConfigFile; + +namespace gaseous_server.Classes.Metadata +{ + public class Covers + { + const string fieldList = "fields alpha_channel,animated,checksum,game,height,image_id,url,width;"; + + public Covers() + { + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static Cover? GetCover(long? Id, string LogoPath) + { + if ((Id == 0) || (Id == null)) + { + return null; + } + else + { + Task RetVal = _GetCover(SearchUsing.id, Id, LogoPath); + return RetVal.Result; + } + } + + public static Cover GetCover(string Slug, string LogoPath) + { + Task RetVal = _GetCover(SearchUsing.slug, Slug, LogoPath); + return RetVal.Result; + } + + private static async Task _GetCover(SearchUsing searchUsing, object searchValue, string LogoPath) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("Cover", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("Cover", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + Cover returnValue = new Cover(); + bool forceImageDownload = false; + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause, LogoPath); + Storage.NewCacheValue(returnValue); + forceImageDownload = true; + break; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause, LogoPath); + Storage.NewCacheValue(returnValue, true); + forceImageDownload = true; + break; + case Storage.CacheStatus.Current: + returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + break; + default: + throw new Exception("How did you get here?"); + } + + if ((!File.Exists(Path.Combine(LogoPath, "Cover.jpg"))) || forceImageDownload == true) + { + //GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_thumb, returnValue.ImageId); + //GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_logo_med, returnValue.ImageId); + GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_original, returnValue.ImageId); + } + + return returnValue; + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause, string LogoPath) + { + // get Cover metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.Covers, query: fieldList + " " + WhereClause + ";"); + var result = results.First(); + + //GetImageFromServer(result.Url, LogoPath, LogoSize.t_thumb, result.ImageId); + //GetImageFromServer(result.Url, LogoPath, LogoSize.t_logo_med, result.ImageId); + GetImageFromServer(result.Url, LogoPath, LogoSize.t_original, result.ImageId); + + return result; + } + + private static void GetImageFromServer(string Url, string LogoPath, LogoSize logoSize, string ImageId) + { + using (var client = new HttpClient()) + { + string fileName = "Cover.jpg"; + string extension = "jpg"; + switch (logoSize) + { + case LogoSize.t_thumb: + fileName = "Cover_Thumb"; + extension = "jpg"; + break; + case LogoSize.t_logo_med: + fileName = "Cover_Medium"; + extension = "png"; + break; + case LogoSize.t_original: + fileName = "Cover_Original"; + extension = "png"; + break; + default: + fileName = "Cover"; + extension = "jpg"; + break; + } + string imageUrl = Url.Replace(LogoSize.t_thumb.ToString(), logoSize.ToString()).Replace("jpg", extension); + + using (var s = client.GetStreamAsync("https:" + imageUrl)) + { + if (!Directory.Exists(LogoPath)) { Directory.CreateDirectory(LogoPath); } + using (var fs = new FileStream(Path.Combine(LogoPath, fileName + "." + extension), FileMode.OpenOrCreate)) + { + s.Result.CopyTo(fs); + } + } + } + } + + private enum LogoSize + { + t_thumb, + t_logo_med, + t_original + } + } +} + diff --git a/gaseous-server/Classes/Metadata/Games.cs b/gaseous-server/Classes/Metadata/Games.cs new file mode 100644 index 0000000..27dc20d --- /dev/null +++ b/gaseous-server/Classes/Metadata/Games.cs @@ -0,0 +1,167 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; + +namespace gaseous_server.Classes.Metadata +{ + public class Games + { + const string fieldList = "fields age_ratings,aggregated_rating,aggregated_rating_count,alternative_names,artworks,bundles,category,checksum,collection,cover,created_at,dlcs,expanded_games,expansions,external_games,first_release_date,follows,forks,franchise,franchises,game_engines,game_localizations,game_modes,genres,hypes,involved_companies,keywords,language_supports,multiplayer_modes,name,parent_game,platforms,player_perspectives,ports,rating,rating_count,release_dates,remakes,remasters,screenshots,similar_games,slug,standalone_expansions,status,storyline,summary,tags,themes,total_rating,total_rating_count,updated_at,url,version_parent,version_title,videos,websites;"; + + public Games() + { + + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static Game? GetGame(long Id) + { + if (Id == 0) + { + return null; + } + else + { + Task RetVal = _GetGame(SearchUsing.id, Id); + return RetVal.Result; + } + } + + public static Game GetGame(string Slug) + { + Task RetVal = _GetGame(SearchUsing.slug, Slug); + return RetVal.Result; + } + + private static async Task _GetGame(SearchUsing searchUsing, object searchValue) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("Game", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("Game", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + Game returnValue = new Game(); + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue); + UpdateSubClasses(returnValue); + return returnValue; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue, true); + UpdateSubClasses(returnValue); + return returnValue; + case Storage.CacheStatus.Current: + return Storage.GetCacheValue(returnValue, "id", (long)searchValue); + default: + throw new Exception("How did you get here?"); + } + } + + private static void UpdateSubClasses(Game Game) + { + if (Game.Artworks != null) + { + foreach (long ArtworkId in Game.Artworks.Ids) + { + Artwork GameArtwork = Artworks.GetArtwork(ArtworkId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(Game)); + } + } + if (Game.Cover != null) + { + Cover GameCover = Covers.GetCover(Game.Cover.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(Game)); + } + if (Game.Platforms != null) + { + foreach (long PlatformId in Game.Platforms.Ids) + { + Platform GamePlatform = Platforms.GetPlatform(PlatformId); + } + } + if (Game.Screenshots != null) + { + foreach (long ScreenshotId in Game.Screenshots.Ids) + { + Screenshot GameScreenshot = Screenshots.GetScreenshot(ScreenshotId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(Game)); + } + } + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause) + { + // get Game metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.Games, query: fieldList + " " + WhereClause + ";"); + var result = results.First(); + + return result; + } + + public static Game[] SearchForGame(string SearchString, long PlatformId, SearchType searchType) + { + Task games = _SearchForGame(SearchString, PlatformId, searchType); + return games.Result; + } + + private static async Task _SearchForGame(string SearchString, long PlatformId, SearchType searchType) + { + string searchBody = ""; + searchBody += "fields id,name,slug,platforms,summary; "; + switch (searchType) + { + case SearchType.search: + searchBody += "search \"" + SearchString + "\"; "; + searchBody += "where platforms = (" + PlatformId + ");"; + break; + case SearchType.where: + searchBody += "where platforms = (" + PlatformId + ") & name ~ *\"" + SearchString + "\"*;"; + break; + } + + + // get Game metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.Games, query: searchBody); + + return results; + } + + public enum SearchType + { + where, + search + } + } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Metadata/PlatformLogos.cs b/gaseous-server/Classes/Metadata/PlatformLogos.cs index aeefb33..f873dad 100644 --- a/gaseous-server/Classes/Metadata/PlatformLogos.cs +++ b/gaseous-server/Classes/Metadata/PlatformLogos.cs @@ -9,6 +9,8 @@ namespace gaseous_server.Classes.Metadata { public class PlatformLogos { + const string fieldList = "fields alpha_channel,animated,checksum,height,image_id,url,width;"; + public PlatformLogos() { } @@ -19,23 +21,139 @@ namespace gaseous_server.Classes.Metadata Config.IGDB.Secret ); - public async static void GetPlatformLogo(long Id, string LogoPath) + public static PlatformLogo? GetPlatformLogo(long? Id, string LogoPath) { - var logo_results = await igdb.QueryAsync(IGDBClient.Endpoints.PlatformLogos, query: "fields alpha_channel,animated,checksum,height,image_id,url,width; where id = " + Id + ";"); - var logo_result = logo_results.First(); + if ((Id == 0) || (Id == null)) + { + return null; + } + else + { + Task RetVal = _GetPlatformLogo(SearchUsing.id, Id, LogoPath); + return RetVal.Result; + } + } + public static PlatformLogo GetPlatformLogo(string Slug, string LogoPath) + { + Task RetVal = _GetPlatformLogo(SearchUsing.slug, Slug, LogoPath); + return RetVal.Result; + } + + private static async Task _GetPlatformLogo(SearchUsing searchUsing, object searchValue, string LogoPath) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("PlatformLogo", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("PlatformLogo", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + PlatformLogo returnValue = new PlatformLogo(); + bool forceImageDownload = false; + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause, LogoPath); + Storage.NewCacheValue(returnValue); + forceImageDownload = true; + break; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause, LogoPath); + Storage.NewCacheValue(returnValue, true); + forceImageDownload = true; + break; + case Storage.CacheStatus.Current: + returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + break; + default: + throw new Exception("How did you get here?"); + } + + if ((!File.Exists(Path.Combine(LogoPath, "Logo.jpg"))) || forceImageDownload == true) + { + GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_thumb); + GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_logo_med); + } + + return returnValue; + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause, string LogoPath) + { + // get PlatformLogo metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.PlatformLogos, query: fieldList + " " + WhereClause + ";"); + var result = results.First(); + + GetImageFromServer(result.Url, LogoPath, LogoSize.t_thumb); + GetImageFromServer(result.Url, LogoPath, LogoSize.t_logo_med); + + return result; + } + + private static void GetImageFromServer(string Url, string LogoPath, LogoSize logoSize) + { using (var client = new HttpClient()) { - using (var s = client.GetStreamAsync("https:" + logo_result.Url)) + string fileName = "Logo.jpg"; + string extension = "jpg"; + switch (logoSize) { - if (!Directory.Exists(Path.GetDirectoryName(LogoPath))) { Directory.CreateDirectory(Path.GetDirectoryName(LogoPath)); } - using (var fs = new FileStream(LogoPath, FileMode.OpenOrCreate)) + case LogoSize.t_thumb: + fileName = "Logo_Thumb"; + extension = "jpg"; + break; + case LogoSize.t_logo_med: + fileName = "Logo_Medium"; + extension = "png"; + break; + default: + fileName = "Logo"; + extension = "jpg"; + break; + } + string imageUrl = Url.Replace(LogoSize.t_thumb.ToString(), logoSize.ToString()).Replace("jpg", extension); + + using (var s = client.GetStreamAsync("https:" + imageUrl)) + { + if (!Directory.Exists(LogoPath)) { Directory.CreateDirectory(LogoPath); } + using (var fs = new FileStream(Path.Combine(LogoPath, fileName + "." + extension), FileMode.OpenOrCreate)) { s.Result.CopyTo(fs); } } } } + + private enum LogoSize + { + t_thumb, + t_logo_med + } } } diff --git a/gaseous-server/Classes/Metadata/PlatformVersions.cs b/gaseous-server/Classes/Metadata/PlatformVersions.cs index cfb3ee7..a15f6a0 100644 --- a/gaseous-server/Classes/Metadata/PlatformVersions.cs +++ b/gaseous-server/Classes/Metadata/PlatformVersions.cs @@ -8,53 +8,23 @@ namespace gaseous_server.Classes.Metadata { public class PlatformVersions { - public PlatformVersions() + const string fieldList = "fields checksum,companies,connectivity,cpu,graphics,main_manufacturer,media,memory,name,online,os,output,platform_logo,platform_version_release_dates,resolutions,slug,sound,storage,summary,url;"; + + public PlatformVersions() { } - public static PlatformVersion UnknownPlatformVersion - { - get - { - PlatformVersion unkownPlatformVersion = new PlatformVersion - { - Id = 0, - Checksum = "", - Companies = new IdentitiesOrValues(), - Connectivity = "", - CPU = "", - Graphics = "", - MainManufacturer = new IdentityOrValue(0), - Media = "", - Memory = "", - Name = "Unknown", - OS = "", - Output = "", - PlatformLogo = new IdentityOrValue(0), - PlatformVersionReleaseDates = new IdentitiesOrValues(), - Resolutions = "", - Slug = "Unknown", - Sound = "", - Storage = "", - Summary = "", - Url = "" - }; - - return unkownPlatformVersion; - } - } - private static IGDBClient igdb = new IGDBClient( // Found in Twitch Developer portal for your app Config.IGDB.ClientId, Config.IGDB.Secret ); - public static PlatformVersion GetPlatformVersion(long Id, Platform ParentPlatform) + public static PlatformVersion? GetPlatformVersion(long Id, Platform ParentPlatform) { if (Id == 0) { - return UnknownPlatformVersion; + return null; } else { @@ -72,7 +42,15 @@ namespace gaseous_server.Classes.Metadata private static async Task _GetPlatformVersion(SearchUsing searchUsing, object searchValue, Platform ParentPlatform) { // check database first - PlatformVersion? platformVersion = DBGetPlatformVersion(searchUsing, searchValue); + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("PlatformVersion", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("PlatformVersion", (string)searchValue); + } // set up where clause string WhereClause = ""; @@ -88,45 +66,31 @@ namespace gaseous_server.Classes.Metadata throw new Exception("Invalid search type"); } - if (platformVersion == null) + PlatformVersion returnValue = new PlatformVersion(); + switch (cacheStatus) { - // get platform version metadata - var results = await igdb.QueryAsync(IGDBClient.Endpoints.PlatformVersions, query: "fields checksum,companies,connectivity,cpu,graphics,main_manufacturer,media,memory,name,online,os,output,platform_logo,platform_version_release_dates,resolutions,slug,sound,storage,summary,url; " + WhereClause + ";"); - var result = results.First(); - - DBInsertPlatformVersion(result, true); - - // get platform logo - if (result.PlatformLogo != null) - { - PlatformLogos.GetPlatformLogo((long)result.PlatformLogo.Id, Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(ParentPlatform), result.Slug, "platform_logo.jpg")); - } - - return result; - } - else - { - return platformVersion; + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue); + UpdateSubClasses(ParentPlatform, returnValue); + return returnValue; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue, true); + UpdateSubClasses(ParentPlatform, returnValue); + return returnValue; + case Storage.CacheStatus.Current: + return Storage.GetCacheValue(returnValue, "id", (long)searchValue); + default: + throw new Exception("How did you get here?"); } } - private static PlatformVersion? DBGetPlatformVersion(SearchUsing searchUsing, object searchValue) + private static void UpdateSubClasses(Platform ParentPlatform, PlatformVersion platformVersion) { - Dictionary dbDict = new Dictionary(); - switch (searchUsing) + if (platformVersion.PlatformLogo != null) { - case SearchUsing.id: - dbDict.Add("id", searchValue); - - return _DBGetPlatformVersion("SELECT * FROM platforms_versions WHERE id = @id", dbDict); - - case SearchUsing.slug: - dbDict.Add("slug", searchValue); - - return _DBGetPlatformVersion("SELECT * FROM platforms_versions WHERE slug = @slug", dbDict); - - default: - throw new Exception("Invalid Search Type"); + PlatformLogo platformLogo = PlatformLogos.GetPlatformLogo(platformVersion.PlatformLogo.Id, Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(ParentPlatform), "Versions", platformVersion.Slug)); } } @@ -136,104 +100,13 @@ namespace gaseous_server.Classes.Metadata slug } - private static PlatformVersion? _DBGetPlatformVersion(string sql, Dictionary searchParams) + private static async Task GetObjectFromServer(string WhereClause) { - Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + // get PlatformVersion metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.PlatformVersions, query: fieldList + " " + WhereClause + ";"); + var result = results.First(); - DataTable dbResponse = db.ExecuteCMD(sql, searchParams); - - if (dbResponse.Rows.Count > 0) - { - return ConvertDataRowToPlatformVersion(dbResponse.Rows[0]); - } - else - { - return null; - } - } - - private static PlatformVersion ConvertDataRowToPlatformVersion(DataRow PlatformDR) - { - PlatformVersion returnPlatformVersion = new PlatformVersion - { - Id = (long)(UInt64)PlatformDR["id"], - Checksum = (string?)PlatformDR["checksum"], - Companies = Newtonsoft.Json.JsonConvert.DeserializeObject>((string?)PlatformDR["companies"]), - Connectivity = (string?)PlatformDR["connectivity"], - CPU = (string?)PlatformDR["cpu"], - Graphics = (string?)PlatformDR["graphics"], - MainManufacturer = new IdentityOrValue((int?)PlatformDR["main_manufacturer"]), - Media = (string?)PlatformDR["media"], - Memory = (string?)PlatformDR["memory"], - Name = (string?)PlatformDR["name"], - OS = (string?)PlatformDR["os"], - Output = (string?)PlatformDR["output"], - PlatformLogo = new IdentityOrValue((int?)PlatformDR["platform_logo"]), - PlatformVersionReleaseDates = Newtonsoft.Json.JsonConvert.DeserializeObject>((string?)PlatformDR["platform_version_release_dates"]), - Resolutions = (string?)PlatformDR["resolutions"], - Slug = (string?)PlatformDR["slug"], - Sound = (string?)PlatformDR["sound"], - Storage = (string?)PlatformDR["storage"], - Summary = (string?)PlatformDR["summary"], - Url = (string?)PlatformDR["url"] - }; - - return returnPlatformVersion; - } - - private static void DBInsertPlatformVersion(PlatformVersion PlatformVersionItem, bool Insert) - { - Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "INSERT INTO platforms_versions (id, checksum, connectivity, cpu, graphics, main_manufacturer, media, memory, name, os, output, platform_logo, platform_version_release_dates, resolutions, slug, sound, storage, summary, url, dateAdded, lastUpdated) VALUES (@id, @checksum, @connectivity, @cpu, @graphics, @main_manufacturer, @media, @memory, @name, @os, @output, @platform_logo, @platform_version_release_dates, @resolutions, @slug, @sound, @storage, @summary, @url, @lastUpdated, @lastUpdated)"; - if (Insert == false) - { - sql = "UPDATE platforms_versions SET checksum=@checksum, connectivity=@connectivity, cpu=@cpu, graphics=@graphics, main_manufacturer=@main_manufacturer, media=@media, memory=@memory, name=@name, os=@os, output=@output, platform_logo=@platform_logo, platform_version_release_dates=@platform_version_release_dates, resolutions=@resolutions, slug=@slug, sound=@sound, storage=@storage, summary=@summary, url=@url, lastUpdated=@lastUpdated WHERE id=@id"; - } - Dictionary dbDict = new Dictionary(); - string EmptyJson = "{\"Ids\": [], \"Values\": null}"; - dbDict.Add("id", PlatformVersionItem.Id); - dbDict.Add("checksum", Common.ReturnValueIfNull(PlatformVersionItem.Checksum, "")); - dbDict.Add("connectivity", Common.ReturnValueIfNull(PlatformVersionItem.Connectivity, "")); - dbDict.Add("cpu", Common.ReturnValueIfNull(PlatformVersionItem.CPU, "")); - dbDict.Add("graphics", Common.ReturnValueIfNull(PlatformVersionItem.Graphics, "")); - if (PlatformVersionItem.MainManufacturer == null) - { - dbDict.Add("main_manufacturer", 0); - } - else - { - dbDict.Add("main_manufacturer", Common.ReturnValueIfNull(PlatformVersionItem.MainManufacturer.Id, 0)); - } - dbDict.Add("media", Common.ReturnValueIfNull(PlatformVersionItem.Media, "")); - dbDict.Add("memory", Common.ReturnValueIfNull(PlatformVersionItem.Memory, "")); - dbDict.Add("name", Common.ReturnValueIfNull(PlatformVersionItem.Name, "")); - dbDict.Add("os", Common.ReturnValueIfNull(PlatformVersionItem.OS, "")); - dbDict.Add("output", Common.ReturnValueIfNull(PlatformVersionItem.Output, "")); - if (PlatformVersionItem.PlatformLogo == null) - { - dbDict.Add("platform_logo", 0); - } - else - { - dbDict.Add("platform_logo", Common.ReturnValueIfNull(PlatformVersionItem.PlatformLogo.Id, 0)); - } - if (PlatformVersionItem.PlatformVersionReleaseDates == null) - { - dbDict.Add("platform_version_release_dates", EmptyJson); - } - else - { - dbDict.Add("platform_version_release_dates", Newtonsoft.Json.JsonConvert.SerializeObject(PlatformVersionItem.PlatformVersionReleaseDates)); - } - dbDict.Add("resolutions", Common.ReturnValueIfNull(PlatformVersionItem.Resolutions, "")); - dbDict.Add("slug", Common.ReturnValueIfNull(PlatformVersionItem.Slug, "")); - dbDict.Add("sound", Common.ReturnValueIfNull(PlatformVersionItem.Sound, "")); - dbDict.Add("storage", Common.ReturnValueIfNull(PlatformVersionItem.Storage, "")); - dbDict.Add("summary", Common.ReturnValueIfNull(PlatformVersionItem.Summary, "")); - dbDict.Add("url", Common.ReturnValueIfNull(PlatformVersionItem.Url, "")); - dbDict.Add("lastUpdated", DateTime.UtcNow); - - db.ExecuteCMD(sql, dbDict); + return result; } } } diff --git a/gaseous-server/Classes/Metadata/Platforms.cs b/gaseous-server/Classes/Metadata/Platforms.cs index d181b29..93ad177 100644 --- a/gaseous-server/Classes/Metadata/Platforms.cs +++ b/gaseous-server/Classes/Metadata/Platforms.cs @@ -7,51 +7,26 @@ using IGDB.Models; namespace gaseous_server.Classes.Metadata { - public class Platforms + public class Platforms { - public Platforms() + const string fieldList = "fields abbreviation,alternative_name,category,checksum,created_at,generation,name,platform_family,platform_logo,slug,summary,updated_at,url,versions,websites;"; + + public Platforms() { + } - public static Platform UnknownPlatform - { - get - { - Platform unkownPlatform = new Platform - { - Id = 0, - Abbreviation = "", - AlternativeName = "", - Category = PlatformCategory.Computer, - Checksum = "", - CreatedAt = DateTime.UtcNow, - Generation = 1, - Name = "Unknown", - PlatformFamily = new IdentityOrValue(0), - PlatformLogo = new IdentityOrValue(0), - Slug = "Unknown", - Summary = "", - UpdatedAt = DateTime.UtcNow, - Url = "", - Versions = new IdentitiesOrValues(), - Websites = new IdentitiesOrValues() - }; - - return unkownPlatform; - } - } - private static IGDBClient igdb = new IGDBClient( // Found in Twitch Developer portal for your app Config.IGDB.ClientId, Config.IGDB.Secret ); - public static Platform GetPlatform(int Id) + public static Platform? GetPlatform(long Id) { if (Id == 0) { - return UnknownPlatform; + return null; } else { @@ -69,7 +44,15 @@ namespace gaseous_server.Classes.Metadata private static async Task _GetPlatform(SearchUsing searchUsing, object searchValue) { // check database first - Platform? platform = DBGetPlatform(searchUsing, searchValue); + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("platform", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("platform", (string)searchValue); + } // set up where clause string WhereClause = ""; @@ -85,51 +68,39 @@ namespace gaseous_server.Classes.Metadata throw new Exception("Invalid search type"); } - if (platform == null) + Platform returnValue = new Platform(); + switch (cacheStatus) { - // get platform metadata - var results = await igdb.QueryAsync(IGDBClient.Endpoints.Platforms, query: "fields abbreviation,alternative_name,category,checksum,created_at,generation,name,platform_family,platform_logo,slug,summary,updated_at,url,versions,websites; " + WhereClause + ";"); - var result = results.First(); - - DBInsertPlatform(result, true); - - // get platform logo - if (result.PlatformLogo != null) - { - PlatformLogos.GetPlatformLogo((long)result.PlatformLogo.Id, Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(result), "platform_logo.jpg")); - } - - // get platform versions - foreach (long platformVersionId in result.Versions.Ids) - { - PlatformVersions.GetPlatformVersion(platformVersionId, result); - } - - return result; - } - else - { - return platform; + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue); + UpdateSubClasses(returnValue); + return returnValue; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue, true); + UpdateSubClasses(returnValue); + return returnValue; + case Storage.CacheStatus.Current: + return Storage.GetCacheValue(returnValue, "id", (long)searchValue); + default: + throw new Exception("How did you get here?"); } } - private static Platform? DBGetPlatform(SearchUsing searchUsing, object searchValue) + private static void UpdateSubClasses(Platform platform) { - Dictionary dbDict = new Dictionary(); - switch (searchUsing) + if (platform.Versions != null) { - case SearchUsing.id: - dbDict.Add("id", searchValue); + foreach (long PlatformVersionId in platform.Versions.Ids) + { + PlatformVersion platformVersion = PlatformVersions.GetPlatformVersion(PlatformVersionId, platform); + } + } - return _DBGetPlatform("SELECT * FROM platforms WHERE id = @id", dbDict); - - case SearchUsing.slug: - dbDict.Add("slug", searchValue); - - return _DBGetPlatform("SELECT * FROM platforms WHERE slug = @slug", dbDict); - - default: - throw new Exception("Invalid Search Type"); + if (platform.PlatformLogo != null) + { + PlatformLogo platformLogo = PlatformLogos.GetPlatformLogo(platform.PlatformLogo.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(platform)); } } @@ -139,104 +110,13 @@ namespace gaseous_server.Classes.Metadata slug } - private static Platform? _DBGetPlatform(string sql, Dictionary searchParams) + private static async Task GetObjectFromServer(string WhereClause) { - Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + // get platform metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.Platforms, query: fieldList + " " + WhereClause + ";"); + var result = results.First(); - DataTable dbResponse = db.ExecuteCMD(sql, searchParams); - - if (dbResponse.Rows.Count > 0) - { - return ConvertDataRowToPlatform(dbResponse.Rows[0]); - } - else - { - return null; - } - } - - private static Platform ConvertDataRowToPlatform(DataRow PlatformDR) - { - Platform returnPlatform = new Platform - { - Id = (long)(UInt64)PlatformDR["id"], - Abbreviation = (string?)PlatformDR["abbreviation"], - AlternativeName = (string?)PlatformDR["alternative_name"], - Category = (PlatformCategory)PlatformDR["category"], - Checksum = (string?)PlatformDR["checksum"], - CreatedAt = (DateTime?)PlatformDR["created_at"], - Generation = (int?)PlatformDR["generation"], - Name = (string?)PlatformDR["name"], - PlatformFamily = new IdentityOrValue((int?)PlatformDR["platform_family"]), - PlatformLogo = new IdentityOrValue((int?)PlatformDR["platform_logo"]), - Slug = (string?)PlatformDR["slug"], - Summary = (string?)PlatformDR["summary"], - UpdatedAt = (DateTime?)PlatformDR["updated_at"], - Url = (string?)PlatformDR["url"], - Versions = Newtonsoft.Json.JsonConvert.DeserializeObject>((string?)PlatformDR["versions"]), - Websites = Newtonsoft.Json.JsonConvert.DeserializeObject>((string?)PlatformDR["websites"]) - }; - - return returnPlatform; - } - - private static void DBInsertPlatform(Platform PlatformItem, bool Insert) - { - Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "INSERT INTO platforms (id, abbreviation, alternative_name, category, checksum, created_at, generation, name, platform_family, platform_logo, slug, summary, updated_at, url, versions, websites, dateAdded, lastUpdated) VALUES (@id, @abbreviation, @alternative_name, @category, @checksum, @created_at, @generation, @name, @platform_family, @platform_logo, @slug, @summary, @updated_at, @url, @versions, @websites, @lastUpdated, @lastUpdated)"; - if (Insert == false) - { - sql = "UPDATE platforms SET abbreviation=@abbreviation, alternative_name=@alternative_name, category=@category, checksum=@checksum, created_at=@created_at, generation=@generation, name=@name, platform_family=@platform_family, platform_logo=@platform_logo, slug=@slug, summary=@summary, updated_at=@updated_at, url=@url, versions=@versions, websites=@websites, lastUpdated=@lastUpdated WHERE id=@id"; - } - Dictionary dbDict = new Dictionary(); - dbDict.Add("id", PlatformItem.Id); - dbDict.Add("abbreviation", Common.ReturnValueIfNull(PlatformItem.Abbreviation, "")); - dbDict.Add("alternative_name", Common.ReturnValueIfNull(PlatformItem.AlternativeName, "")); - dbDict.Add("category", Common.ReturnValueIfNull(PlatformItem.Category, PlatformCategory.Computer)); - dbDict.Add("checksum", Common.ReturnValueIfNull(PlatformItem.Checksum, "")); - dbDict.Add("created_at", Common.ReturnValueIfNull(PlatformItem.CreatedAt, DateTime.UtcNow)); - dbDict.Add("generation", Common.ReturnValueIfNull(PlatformItem.Generation, 1)); - dbDict.Add("name", Common.ReturnValueIfNull(PlatformItem.Name, "")); - if (PlatformItem.PlatformFamily == null) - { - dbDict.Add("platform_family", 0); - } - else - { - dbDict.Add("platform_family", Common.ReturnValueIfNull(PlatformItem.PlatformFamily.Id, 0)); - } - if (PlatformItem.PlatformLogo == null) - { - dbDict.Add("platform_logo", 0); - } - else - { - dbDict.Add("platform_logo", Common.ReturnValueIfNull(PlatformItem.PlatformLogo.Id, 0)); - } - dbDict.Add("slug", Common.ReturnValueIfNull(PlatformItem.Slug, "")); - dbDict.Add("summary", Common.ReturnValueIfNull(PlatformItem.Summary, "")); - dbDict.Add("updated_at", Common.ReturnValueIfNull(PlatformItem.UpdatedAt, DateTime.UtcNow)); - dbDict.Add("url", Common.ReturnValueIfNull(PlatformItem.Url, "")); - dbDict.Add("lastUpdated", DateTime.UtcNow); - string EmptyJson = "{\"Ids\": [], \"Values\": null}"; - if (PlatformItem.Versions == null) - { - dbDict.Add("versions", EmptyJson); - } - else - { - dbDict.Add("versions", Newtonsoft.Json.JsonConvert.SerializeObject(PlatformItem.Versions)); - } - if (PlatformItem.Websites == null) - { - dbDict.Add("websites", EmptyJson); - } - else - { - dbDict.Add("websites", Newtonsoft.Json.JsonConvert.SerializeObject(PlatformItem.Websites)); - } - - db.ExecuteCMD(sql, dbDict); + return result; } } } diff --git a/gaseous-server/Classes/Metadata/Screenshots.cs b/gaseous-server/Classes/Metadata/Screenshots.cs new file mode 100644 index 0000000..7d1ddc6 --- /dev/null +++ b/gaseous-server/Classes/Metadata/Screenshots.cs @@ -0,0 +1,168 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; +using MySqlX.XDevAPI.Common; +using static gaseous_tools.Config.ConfigFile; + +namespace gaseous_server.Classes.Metadata +{ + public class Screenshots + { + const string fieldList = "fields alpha_channel,animated,checksum,game,height,image_id,url,width;"; + + public Screenshots() + { + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static Screenshot? GetScreenshot(long? Id, string LogoPath) + { + if ((Id == 0) || (Id == null)) + { + return null; + } + else + { + Task RetVal = _GetScreenshot(SearchUsing.id, Id, LogoPath); + return RetVal.Result; + } + } + + public static Screenshot GetScreenshot(string Slug, string LogoPath) + { + Task RetVal = _GetScreenshot(SearchUsing.slug, Slug, LogoPath); + return RetVal.Result; + } + + private static async Task _GetScreenshot(SearchUsing searchUsing, object searchValue, string LogoPath) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("Screenshot", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("Screenshot", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + Screenshot returnValue = new Screenshot(); + bool forceImageDownload = false; + LogoPath = Path.Combine(LogoPath, "Screenshots"); + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause, LogoPath); + Storage.NewCacheValue(returnValue); + forceImageDownload = true; + break; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause, LogoPath); + Storage.NewCacheValue(returnValue, true); + forceImageDownload = true; + break; + case Storage.CacheStatus.Current: + returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + break; + default: + throw new Exception("How did you get here?"); + } + + if ((!File.Exists(Path.Combine(LogoPath, "Screenshot.jpg"))) || forceImageDownload == true) + { + //GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_thumb, returnValue.ImageId); + //GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_logo_med, returnValue.ImageId); + GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_original, returnValue.ImageId); + } + + return returnValue; + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause, string LogoPath) + { + // get Screenshot metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.Screenshots, query: fieldList + " " + WhereClause + ";"); + var result = results.First(); + + //GetImageFromServer(result.Url, LogoPath, LogoSize.t_thumb, result.ImageId); + //GetImageFromServer(result.Url, LogoPath, LogoSize.t_logo_med, result.ImageId); + GetImageFromServer(result.Url, LogoPath, LogoSize.t_original, result.ImageId); + + return result; + } + + private static void GetImageFromServer(string Url, string LogoPath, LogoSize logoSize, string ImageId) + { + using (var client = new HttpClient()) + { + string fileName = "Artwork.jpg"; + string extension = "jpg"; + switch (logoSize) + { + case LogoSize.t_thumb: + fileName = "_Thumb"; + extension = "jpg"; + break; + case LogoSize.t_logo_med: + fileName = "_Medium"; + extension = "png"; + break; + case LogoSize.t_original: + fileName = "_Original"; + extension = "png"; + break; + default: + fileName = "Artwork"; + extension = "jpg"; + break; + } + fileName = ImageId + fileName; + string imageUrl = Url.Replace(LogoSize.t_thumb.ToString(), logoSize.ToString()).Replace("jpg", extension); + + using (var s = client.GetStreamAsync("https:" + imageUrl)) + { + if (!Directory.Exists(LogoPath)) { Directory.CreateDirectory(LogoPath); } + using (var fs = new FileStream(Path.Combine(LogoPath, fileName + "." + extension), FileMode.OpenOrCreate)) + { + s.Result.CopyTo(fs); + } + } + } + } + + private enum LogoSize + { + t_thumb, + t_logo_med, + t_original + } + } +} + diff --git a/gaseous-server/Classes/Metadata/Storage.cs b/gaseous-server/Classes/Metadata/Storage.cs new file mode 100644 index 0000000..8ac0ac6 --- /dev/null +++ b/gaseous-server/Classes/Metadata/Storage.cs @@ -0,0 +1,266 @@ +using System; +using System.Data; +using System.Reflection; +using gaseous_tools; +using IGDB; +using IGDB.Models; + +namespace gaseous_server.Classes.Metadata +{ + public class Storage + { + public enum CacheStatus + { + NotPresent, + Current, + Expired + } + + public static CacheStatus GetCacheStatus(string Endpoint, string Slug) + { + return _GetCacheStatus(Endpoint, "slug", Slug); + } + + public static CacheStatus GetCacheStatus(string Endpoint, long Id) + { + return _GetCacheStatus(Endpoint, "id", Id); + } + + private static CacheStatus _GetCacheStatus(string Endpoint, string SearchField, object SearchValue) + { + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + + string sql = "SELECT lastUpdated FROM " + Endpoint + " WHERE " + SearchField + " = @" + SearchField; + + Dictionary dbDict = new Dictionary(); + dbDict.Add("Endpoint", Endpoint); + dbDict.Add(SearchField, SearchValue); + + DataTable dt = db.ExecuteCMD(sql, dbDict); + if (dt.Rows.Count == 0) + { + // no data stored for this item, or lastUpdated + return CacheStatus.NotPresent; + } + else + { + DateTime CacheExpiryTime = DateTime.UtcNow.AddHours(-24); + if ((DateTime)dt.Rows[0]["lastUpdated"] < CacheExpiryTime) + { + return CacheStatus.Expired; + } + else + { + return CacheStatus.Current; + } + } + } + + public static void NewCacheValue(object ObjectToCache, bool UpdateRecord = false) + { + // get the object type name + string ObjectTypeName = ObjectToCache.GetType().Name; + + // build dictionary + string objectJson = Newtonsoft.Json.JsonConvert.SerializeObject(ObjectToCache); + Dictionary objectDict = Newtonsoft.Json.JsonConvert.DeserializeObject>(objectJson); + objectDict.Add("dateAdded", DateTime.UtcNow); + objectDict.Add("lastUpdated", DateTime.UtcNow); + + // generate sql + string fieldList = ""; + string valueList = ""; + string updateFieldValueList = ""; + foreach (KeyValuePair key in objectDict) + { + if (fieldList.Length > 0) + { + fieldList = fieldList + ", "; + valueList = valueList + ", "; + updateFieldValueList = updateFieldValueList + ", "; + } + fieldList = fieldList + key.Key; + valueList = valueList + "@" + key.Key; + if ((key.Key != "id") && (key.Key != "dateAdded")) + { + updateFieldValueList = key.Key + " = @" + key.Key; + } + + // check property type + Type objectType = ObjectToCache.GetType(); + if (objectType != null) + { + PropertyInfo objectProperty = objectType.GetProperty(key.Key); + if (objectProperty != null) + { + string compareName = objectProperty.PropertyType.Name.ToLower().Split("`")[0]; + var objectValue = objectProperty.GetValue(ObjectToCache); + if (objectValue != null) + { + string newObjectValue; + Dictionary newDict; + switch (compareName) + { + case "identityorvalue": + newObjectValue = Newtonsoft.Json.JsonConvert.SerializeObject(objectValue); + newDict = Newtonsoft.Json.JsonConvert.DeserializeObject>(newObjectValue); + objectDict[key.Key] = newDict["Id"]; + break; + case "identitiesorvalues": + newObjectValue = Newtonsoft.Json.JsonConvert.SerializeObject(objectValue); + newDict = Newtonsoft.Json.JsonConvert.DeserializeObject>(newObjectValue); + newObjectValue = Newtonsoft.Json.JsonConvert.SerializeObject(newDict["Ids"]); + objectDict[key.Key] = newObjectValue; + break; + case "int32[]": + newObjectValue = Newtonsoft.Json.JsonConvert.SerializeObject(objectValue); + objectDict[key.Key] = newObjectValue; + break; + } + } + } + } + } + + string sql = ""; + if (UpdateRecord == false) + { + sql = "INSERT INTO " + ObjectTypeName + " (" + fieldList + ") VALUES (" + valueList + ")"; + } + else + { + sql = "UPDATE " + ObjectTypeName + " SET " + updateFieldValueList + " WHERE Id = @Id"; + } + + // execute sql + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + db.ExecuteCMD(sql, objectDict); + } + + public static T GetCacheValue(T EndpointType, string SearchField, object SearchValue) + { + string Endpoint = EndpointType.GetType().Name; + + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + + string sql = "SELECT * FROM " + Endpoint + " WHERE " + SearchField + " = @" + SearchField; + + Dictionary dbDict = new Dictionary(); + dbDict.Add("Endpoint", Endpoint); + dbDict.Add(SearchField, SearchValue); + + DataTable dt = db.ExecuteCMD(sql, dbDict); + if (dt.Rows.Count == 0) + { + // no data stored for this item + throw new Exception("No record found that matches endpoint " + Endpoint + " with search value " + SearchValue); + } + else + { + DataRow dataRow = dt.Rows[0]; + foreach (PropertyInfo property in EndpointType.GetType().GetProperties()) + { + if (dataRow.Table.Columns.Contains(property.Name)) + { + if (dataRow[property.Name] != DBNull.Value) + { + string objectTypeName = property.PropertyType.Name.ToLower().Split("`")[0]; + string subObjectTypeName = ""; + object? objectToStore = null; + if (objectTypeName == "nullable") + { + objectTypeName = property.PropertyType.UnderlyingSystemType.ToString().Split("`1")[1].Replace("[System.", "").Replace("]", "").ToLower(); + } + try + { + switch (objectTypeName) + { + case "datetimeoffset": + DateTimeOffset? storedDate = (DateTime?)dataRow[property.Name]; + property.SetValue(EndpointType, storedDate); + break; + //case "nullable": + // Console.WriteLine("Nullable: " + property.PropertyType.UnderlyingSystemType); + // break; + case "identityorvalue": + subObjectTypeName = property.PropertyType.UnderlyingSystemType.ToString().Split("`1")[1].Replace("[IGDB.Models.", "").Replace("]", "").ToLower(); + + switch (subObjectTypeName) + { + case "platformfamily": + objectToStore = new IdentityOrValue(id: (long)(int)dataRow[property.Name]); + break; + case "platformlogo": + objectToStore = new IdentityOrValue(id: (long)(int)dataRow[property.Name]); + break; + case "platformversioncompany": + objectToStore = new IdentityOrValue(id: (long)(int)dataRow[property.Name]); + break; + } + + if (objectToStore != null) + { + property.SetValue(EndpointType, objectToStore); + } + + break; + case "identitiesorvalues": + subObjectTypeName = property.PropertyType.UnderlyingSystemType.ToString().Split("`1")[1].Replace("[IGDB.Models.", "").Replace("]", "").ToLower(); + + long[] fromJsonObject = Newtonsoft.Json.JsonConvert.DeserializeObject((string)dataRow[property.Name]); + + switch (subObjectTypeName) + { + case "platformversion": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "platformwebsite": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "platformversioncompany": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "platformversionreleasedate": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + } + + if (objectToStore != null) + { + property.SetValue(EndpointType, objectToStore); + } + + break; + case "int32[]": + Int32[] fromJsonObject_int32Array = Newtonsoft.Json.JsonConvert.DeserializeObject((string)dataRow[property.Name]); + if (fromJsonObject_int32Array != null) + { + property.SetValue(EndpointType, fromJsonObject_int32Array); + } + break; + case "[igdb.models.category": + property.SetValue(EndpointType, (Category)dataRow[property.Name]); + break; + case "[igdb.models.gamestatus": + property.SetValue(EndpointType, (GameStatus)dataRow[property.Name]); + break; + default: + property.SetValue(EndpointType, dataRow[property.Name]); + break; + } + } + catch (Exception ex) + { + Console.WriteLine("Error occurred in column " + property.Name); + Console.WriteLine(ex.ToString()); + } + } + } + } + + return EndpointType; + } + } + } +} + diff --git a/gaseous-server/Models/PlatformMapping.cs b/gaseous-server/Models/PlatformMapping.cs index 86a1614..a8c0798 100644 --- a/gaseous-server/Models/PlatformMapping.cs +++ b/gaseous-server/Models/PlatformMapping.cs @@ -44,6 +44,7 @@ namespace gaseous_server.Models if (Signature.Game != null) { Signature.Game.System = PlatformMapping.IGDBName; } } Signature.Flags.IGDBPlatformId = PlatformMapping.IGDBId; + Signature.Flags.IGDBPlatformName = PlatformMapping.IGDBName; } } } diff --git a/gaseous-server/Models/Signatures_Games.cs b/gaseous-server/Models/Signatures_Games.cs index 8aec7a4..89f4524 100644 --- a/gaseous-server/Models/Signatures_Games.cs +++ b/gaseous-server/Models/Signatures_Games.cs @@ -229,6 +229,7 @@ namespace gaseous_server.Models public class SignatureFlags { public int IGDBPlatformId { get; set; } + public string IGDBPlatformName { get; set; } } } } diff --git a/gaseous-server/gaseous-server.csproj b/gaseous-server/gaseous-server.csproj index 8c81cda..8c80069 100644 --- a/gaseous-server/gaseous-server.csproj +++ b/gaseous-server/gaseous-server.csproj @@ -10,9 +10,9 @@ - + - + diff --git a/gaseous-signature-ingestor/gaseous-signature-ingestor.csproj b/gaseous-signature-ingestor/gaseous-signature-ingestor.csproj index 119e791..a83ccc7 100644 --- a/gaseous-signature-ingestor/gaseous-signature-ingestor.csproj +++ b/gaseous-signature-ingestor/gaseous-signature-ingestor.csproj @@ -14,7 +14,7 @@ - + diff --git a/gaseous-tools/Config.cs b/gaseous-tools/Config.cs index 5ca8588..de72de4 100644 --- a/gaseous-tools/Config.cs +++ b/gaseous-tools/Config.cs @@ -257,7 +257,14 @@ namespace gaseous_tools public string LibraryMetadataDirectory_Platform(Platform platform) { - string MetadataPath = Path.Combine(LibraryMetadataDirectory, platform.Slug); + string MetadataPath = Path.Combine(LibraryMetadataDirectory, "Platforms", platform.Slug); + if (!Directory.Exists(MetadataPath)) { Directory.CreateDirectory(MetadataPath); } + return MetadataPath; + } + + public string LibraryMetadataDirectory_Game(Game game) + { + string MetadataPath = Path.Combine(LibraryMetadataDirectory, "Games", game.Slug); if (!Directory.Exists(MetadataPath)) { Directory.CreateDirectory(MetadataPath); } return MetadataPath; } diff --git a/gaseous-tools/Database/MySQL/gaseous-1000.sql b/gaseous-tools/Database/MySQL/gaseous-1000.sql index ba3f120..13b74ae 100644 --- a/gaseous-tools/Database/MySQL/gaseous-1000.sql +++ b/gaseous-tools/Database/MySQL/gaseous-1000.sql @@ -15,12 +15,274 @@ /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; +-- +-- Table structure for table `artwork` +-- + +DROP TABLE IF EXISTS `artwork`; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `artwork` ( + `id` bigint NOT NULL, + `alphachannel` tinyint(1) DEFAULT NULL, + `animated` tinyint(1) DEFAULT NULL, + `checksum` varchar(45) DEFAULT NULL, + `game` bigint DEFAULT NULL, + `height` int DEFAULT NULL, + `imageid` varchar(45) DEFAULT NULL, + `url` varchar(255) DEFAULT NULL, + `width` int DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + + +-- +-- Table structure for table `cover` +-- + +DROP TABLE IF EXISTS `cover`; + +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `cover` ( + `id` bigint NOT NULL, + `alphachannel` tinyint(1) DEFAULT NULL, + `animated` tinyint(1) DEFAULT NULL, + `checksum` varchar(45) DEFAULT NULL, + `game` bigint DEFAULT NULL, + `height` int DEFAULT NULL, + `imageid` varchar(45) DEFAULT NULL, + `url` varchar(255) DEFAULT NULL, + `width` int DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + + +-- +-- Table structure for table `game` +-- + +DROP TABLE IF EXISTS `game`; + +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `game` ( + `id` bigint NOT NULL, + `ageratings` json DEFAULT NULL, + `aggregatedrating` double DEFAULT NULL, + `aggregatedratingcount` int DEFAULT NULL, + `alternativenames` json DEFAULT NULL, + `artworks` json DEFAULT NULL, + `bundles` json DEFAULT NULL, + `category` int DEFAULT NULL, + `checksum` varchar(45) DEFAULT NULL, + `collection` bigint DEFAULT NULL, + `cover` bigint DEFAULT NULL, + `createdat` datetime DEFAULT NULL, + `dlcs` json DEFAULT NULL, + `expansions` json DEFAULT NULL, + `externalgames` json DEFAULT NULL, + `firstreleasedate` datetime DEFAULT NULL, + `follows` int DEFAULT NULL, + `franchise` bigint DEFAULT NULL, + `franchises` json DEFAULT NULL, + `gameengines` json DEFAULT NULL, + `gamemodes` json DEFAULT NULL, + `genres` json DEFAULT NULL, + `hypes` int DEFAULT NULL, + `involvedcompanies` json DEFAULT NULL, + `keywords` json DEFAULT NULL, + `multiplayermodes` json DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `parentgame` bigint DEFAULT NULL, + `platforms` json DEFAULT NULL, + `playerperspectives` json DEFAULT NULL, + `rating` double DEFAULT NULL, + `ratingcount` int DEFAULT NULL, + `releasedates` json DEFAULT NULL, + `screenshots` json DEFAULT NULL, + `similargames` json DEFAULT NULL, + `slug` varchar(100) DEFAULT NULL, + `standaloneexpansions` json DEFAULT NULL, + `status` int DEFAULT NULL, + `storyline` longtext, + `summary` longtext, + `tags` json DEFAULT NULL, + `themes` json DEFAULT NULL, + `totalrating` double DEFAULT NULL, + `totalratingcount` int DEFAULT NULL, + `updatedat` datetime DEFAULT NULL, + `url` varchar(100) DEFAULT NULL, + `versionparent` bigint DEFAULT NULL, + `versiontitle` varchar(100) DEFAULT NULL, + `videos` json DEFAULT NULL, + `websites` json DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + + +-- +-- Table structure for table `games_roms` +-- + +DROP TABLE IF EXISTS `games_roms`; + +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `games_roms` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `platformid` bigint DEFAULT NULL, + `gameid` bigint DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `size` bigint DEFAULT NULL, + `crc` varchar(20) DEFAULT NULL, + `md5` varchar(100) DEFAULT NULL, + `sha1` varchar(100) DEFAULT NULL, + `developmentstatus` varchar(100) DEFAULT NULL, + `flags` json DEFAULT NULL, + `romtype` int DEFAULT NULL, + `romtypemedia` varchar(100) DEFAULT NULL, + `medialabel` varchar(100) DEFAULT NULL, + `path` longtext, + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + + +-- +-- Table structure for table `platform` +-- + +DROP TABLE IF EXISTS `platform`; + +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `platform` ( + `id` bigint NOT NULL, + `abbreviation` varchar(45) DEFAULT NULL, + `alternativename` varchar(45) DEFAULT NULL, + `category` int DEFAULT NULL, + `checksum` varchar(45) DEFAULT NULL, + `createdat` datetime DEFAULT NULL, + `generation` int DEFAULT NULL, + `name` varchar(45) DEFAULT NULL, + `platformfamily` int DEFAULT NULL, + `platformlogo` int DEFAULT NULL, + `slug` varchar(45) DEFAULT NULL, + `summary` longtext, + `updatedat` datetime DEFAULT NULL, + `url` varchar(255) DEFAULT NULL, + `versions` json DEFAULT NULL, + `websites` json DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `id_UNIQUE` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + + +-- +-- Table structure for table `platformlogo` +-- + +DROP TABLE IF EXISTS `platformlogo`; + +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `platformlogo` ( + `id` bigint NOT NULL, + `alphachannel` tinyint(1) DEFAULT NULL, + `animated` tinyint(1) DEFAULT NULL, + `checksum` varchar(45) DEFAULT NULL, + `height` int DEFAULT NULL, + `imageid` varchar(45) DEFAULT NULL, + `url` varchar(255) DEFAULT NULL, + `width` int DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + + +-- +-- Table structure for table `platformversion` +-- + +DROP TABLE IF EXISTS `platformversion`; + +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `platformversion` ( + `id` bigint NOT NULL, + `checksum` varchar(45) DEFAULT NULL, + `companies` json DEFAULT NULL, + `connectivity` longtext, + `cpu` longtext, + `graphics` longtext, + `mainmanufacturer` bigint DEFAULT NULL, + `media` longtext, + `memory` longtext, + `name` longtext, + `os` longtext, + `output` longtext, + `platformlogo` int DEFAULT NULL, + `platformversionreleasedates` json DEFAULT NULL, + `resolutions` longtext, + `slug` longtext, + `sound` longtext, + `storage` longtext, + `summary` longtext, + `url` varchar(255) DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + + +-- +-- Table structure for table `screenshot` +-- + +DROP TABLE IF EXISTS `screenshot`; + +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `screenshot` ( + `id` bigint NOT NULL, + `alphachannel` tinyint(1) DEFAULT NULL, + `animated` tinyint(1) DEFAULT NULL, + `checksum` varchar(45) DEFAULT NULL, + `game` bigint DEFAULT NULL, + `height` int DEFAULT NULL, + `imageid` varchar(45) DEFAULT NULL, + `url` varchar(255) DEFAULT NULL, + `width` int DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + + +-- +-- Table structure for table `settings` +-- + +DROP TABLE IF EXISTS `settings`; + +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `settings` ( + `setting` varchar(45) NOT NULL, + `value` longtext, + PRIMARY KEY (`setting`), + UNIQUE KEY `setting_UNIQUE` (`setting`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + + -- -- Table structure for table `signatures_games` -- DROP TABLE IF EXISTS `signatures_games`; -/*!40101 SET @saved_cs_client = @@character_set_client */; + /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `signatures_games` ( `id` int NOT NULL AUTO_INCREMENT, @@ -43,14 +305,14 @@ CREATE TABLE `signatures_games` ( CONSTRAINT `publisher` FOREIGN KEY (`publisherid`) REFERENCES `signatures_publishers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `system` FOREIGN KEY (`systemid`) REFERENCES `signatures_platforms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `signatures_platforms` -- DROP TABLE IF EXISTS `signatures_platforms`; -/*!40101 SET @saved_cs_client = @@character_set_client */; + /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `signatures_platforms` ( `id` int NOT NULL AUTO_INCREMENT, @@ -59,14 +321,14 @@ CREATE TABLE `signatures_platforms` ( UNIQUE KEY `idsignatures_platforms_UNIQUE` (`id`), KEY `platforms_idx` (`platform`,`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `signatures_publishers` -- DROP TABLE IF EXISTS `signatures_publishers`; -/*!40101 SET @saved_cs_client = @@character_set_client */; + /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `signatures_publishers` ( `id` int NOT NULL AUTO_INCREMENT, @@ -75,14 +337,14 @@ CREATE TABLE `signatures_publishers` ( UNIQUE KEY `id_UNIQUE` (`id`), KEY `publisher_idx` (`publisher`,`id`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `signatures_roms` -- DROP TABLE IF EXISTS `signatures_roms`; -/*!40101 SET @saved_cs_client = @@character_set_client */; + /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `signatures_roms` ( `id` int NOT NULL AUTO_INCREMENT, @@ -105,14 +367,14 @@ CREATE TABLE `signatures_roms` ( KEY `flags_idx` ((cast(`flags` as char(255) array))), CONSTRAINT `gameid` FOREIGN KEY (`gameid`) REFERENCES `signatures_games` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `signatures_sources` -- DROP TABLE IF EXISTS `signatures_sources`; -/*!40101 SET @saved_cs_client = @@character_set_client */; + /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `signatures_sources` ( `id` int NOT NULL AUTO_INCREMENT, @@ -132,15 +394,29 @@ CREATE TABLE `signatures_sources` ( KEY `sourcemd5_idx` (`sourcemd5`,`id`) USING BTREE, KEY `sourcesha1_idx` (`sourcesha1`,`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; -/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; -/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; -/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; -/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; +-- +-- Final view structure for view `view_signatures_games` +-- --- Dump completed on 2023-02-27 8:54:22 +DROP VIEW IF EXISTS `view_signatures_games`; +CREATE VIEW `view_signatures_games` AS + SELECT + `signatures_games`.`id` AS `id`, + `signatures_games`.`name` AS `name`, + `signatures_games`.`description` AS `description`, + `signatures_games`.`year` AS `year`, + `signatures_games`.`publisherid` AS `publisherid`, + `signatures_publishers`.`publisher` AS `publisher`, + `signatures_games`.`demo` AS `demo`, + `signatures_games`.`systemid` AS `platformid`, + `signatures_platforms`.`platform` AS `platform`, + `signatures_games`.`systemvariant` AS `systemvariant`, + `signatures_games`.`video` AS `video`, + `signatures_games`.`country` AS `country`, + `signatures_games`.`language` AS `language`, + `signatures_games`.`copyright` AS `copyright` + FROM + ((`signatures_games` + JOIN `signatures_publishers` ON ((`signatures_games`.`publisherid` = `signatures_publishers`.`id`))) + JOIN `signatures_platforms` ON ((`signatures_games`.`systemid` = `signatures_platforms`.`id`))); \ No newline at end of file diff --git a/gaseous-tools/Database/MySQL/gaseous-1001.sql b/gaseous-tools/Database/MySQL/gaseous-1001.sql deleted file mode 100644 index 98eb6e0..0000000 --- a/gaseous-tools/Database/MySQL/gaseous-1001.sql +++ /dev/null @@ -1,21 +0,0 @@ -CREATE VIEW `view_signatures_games` AS - SELECT - `signatures_games`.`id` AS `id`, - `signatures_games`.`name` AS `name`, - `signatures_games`.`description` AS `description`, - `signatures_games`.`year` AS `year`, - `signatures_games`.`publisherid` AS `publisherid`, - `signatures_publishers`.`publisher` AS `publisher`, - `signatures_games`.`demo` AS `demo`, - `signatures_games`.`systemid` AS `platformid`, - `signatures_platforms`.`platform` AS `platform`, - `signatures_games`.`systemvariant` AS `systemvariant`, - `signatures_games`.`video` AS `video`, - `signatures_games`.`country` AS `country`, - `signatures_games`.`language` AS `language`, - `signatures_games`.`copyright` AS `copyright` - FROM - ((`signatures_games` - JOIN `signatures_publishers` ON ((`signatures_games`.`publisherid` = `signatures_publishers`.`id`))) - JOIN `signatures_platforms` ON ((`signatures_games`.`systemid` = `signatures_platforms`.`id`))); - diff --git a/gaseous-tools/Database/MySQL/gaseous-1002.sql b/gaseous-tools/Database/MySQL/gaseous-1002.sql deleted file mode 100644 index 29ed498..0000000 --- a/gaseous-tools/Database/MySQL/gaseous-1002.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE `gaseous`.`settings` ( - `setting` VARCHAR(45) NOT NULL, - `value` LONGTEXT NULL, - UNIQUE INDEX `setting_UNIQUE` (`setting` ASC) VISIBLE, - PRIMARY KEY (`setting`)); - diff --git a/gaseous-tools/Database/MySQL/gaseous-1003.sql b/gaseous-tools/Database/MySQL/gaseous-1003.sql deleted file mode 100644 index 38f81ee..0000000 --- a/gaseous-tools/Database/MySQL/gaseous-1003.sql +++ /dev/null @@ -1,24 +0,0 @@ - -CREATE TABLE `platforms` ( - `id` bigint unsigned NOT NULL, - `abbreviation` varchar(45) DEFAULT NULL, - `alternative_name` varchar(45) DEFAULT NULL, - `category` int DEFAULT NULL, - `checksum` varchar(45) DEFAULT NULL, - `created_at` datetime DEFAULT NULL, - `generation` int DEFAULT NULL, - `name` varchar(45) DEFAULT NULL, - `platform_family` int DEFAULT NULL, - `platform_logo` int DEFAULT NULL, - `slug` varchar(45) DEFAULT NULL, - `summary` varchar(255) DEFAULT NULL, - `updated_at` datetime DEFAULT NULL, - `url` varchar(255) DEFAULT NULL, - `versions` json DEFAULT NULL, - `websites` json DEFAULT NULL, - `dateAdded` datetime DEFAULT NULL, - `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -SELECT * FROM gaseous.platforms; \ No newline at end of file diff --git a/gaseous-tools/gaseous-tools.csproj b/gaseous-tools/gaseous-tools.csproj index 025a8d2..400f6b9 100644 --- a/gaseous-tools/gaseous-tools.csproj +++ b/gaseous-tools/gaseous-tools.csproj @@ -8,7 +8,7 @@ - + @@ -16,9 +16,6 @@ - - - @@ -26,8 +23,5 @@ - - - From f16b2aabbf42bec733b3ebb88d9d1248614f66e7 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Wed, 3 May 2023 23:36:07 +1000 Subject: [PATCH 11/71] fix: updated import code and added organise library command --- gaseous-server/Classes/ImportGames.cs | 299 ++++++++++++++---- gaseous-server/Classes/Metadata/Games.cs | 10 +- .../Classes/Metadata/PlatformLogos.cs | 40 ++- .../Classes/Metadata/PlatformVersions.cs | 27 +- gaseous-server/Classes/Roms.cs | 64 ++++ gaseous-server/Program.cs | 3 + 6 files changed, 360 insertions(+), 83 deletions(-) create mode 100644 gaseous-server/Classes/Roms.cs diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs index c4c4aed..257f27a 100644 --- a/gaseous-server/Classes/ImportGames.cs +++ b/gaseous-server/Classes/ImportGames.cs @@ -2,6 +2,7 @@ using System.Data; using System.Threading.Tasks; using gaseous_tools; +using Org.BouncyCastle.Utilities.IO.Pem; namespace gaseous_server.Classes { @@ -16,8 +17,7 @@ namespace gaseous_server.Classes // import files first foreach (string importContent in importContents_Files) { - ImportGame importGame = new ImportGame(); - importGame.ImportGameFile(importContent); + ImportGame.ImportGameFile(importContent); } } else @@ -34,9 +34,13 @@ namespace gaseous_server.Classes { private Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - public void ImportGameFile(string GameFileImportPath, bool IsDirectory = false) + public static void ImportGameFile(string GameFileImportPath, bool IsDirectory = false, bool ForceImport = false) { - if (String.Equals(Path.GetFileName(GameFileImportPath),".DS_STORE", StringComparison.OrdinalIgnoreCase)) + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = ""; + Dictionary dbDict = new Dictionary(); + + if (String.Equals(Path.GetFileName(GameFileImportPath),".DS_STORE", StringComparison.OrdinalIgnoreCase)) { Logging.Log(Logging.LogType.Information, "Import Game", "Skipping item " + GameFileImportPath); } @@ -46,75 +50,248 @@ namespace gaseous_server.Classes if (IsDirectory == false) { FileInfo fi = new FileInfo(GameFileImportPath); + Common.hashObject hash = new Common.hashObject(GameFileImportPath); - // process as a single file - // check 1: do we have a signature for it? - Common.hashObject hash = new Common.hashObject(GameFileImportPath); - gaseous_server.Controllers.SignaturesController sc = new Controllers.SignaturesController(); - List signatures = sc.GetSignature(hash.md5hash); - if (signatures.Count == 0) + // check to make sure we don't already have this file imported + sql = "SELECT COUNT(Id) AS count FROM games_roms WHERE md5=@md5 AND sha1=@sha1"; + dbDict.Add("md5", hash.md5hash); + dbDict.Add("sha1", hash.sha1hash); + DataTable importDB = db.ExecuteCMD(sql, dbDict); + if ((Int64)importDB.Rows[0]["count"] > 0) { - // no md5 signature found - try sha1 - signatures = sc.GetSignature("", hash.sha1hash); - } - - Models.Signatures_Games discoveredSignature = new Models.Signatures_Games(); - if (signatures.Count == 1) - { - // only 1 signature found! - discoveredSignature = signatures.ElementAt(0); - gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, false); - } - else if (signatures.Count > 1) - { - // more than one signature found - find one with highest score - foreach (Models.Signatures_Games Sig in signatures) - { - if (Sig.Score > discoveredSignature.Score) - { - discoveredSignature = Sig; - gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, false); - } - } + Logging.Log(Logging.LogType.Information, "Import Game", " " + GameFileImportPath + " already in database - skipping"); } else { - // no signature match found - try alternate methods - Models.Signatures_Games.GameItem gi = new Models.Signatures_Games.GameItem(); - Models.Signatures_Games.RomItem ri = new Models.Signatures_Games.RomItem(); + Logging.Log(Logging.LogType.Information, "Import Game", " " + GameFileImportPath + " not in database - processing"); - discoveredSignature.Game = gi; - discoveredSignature.Rom = ri; + // process as a single file + // check 1: do we have a signature for it? + gaseous_server.Controllers.SignaturesController sc = new Controllers.SignaturesController(); + List signatures = sc.GetSignature(hash.md5hash); + if (signatures.Count == 0) + { + // no md5 signature found - try sha1 + signatures = sc.GetSignature("", hash.sha1hash); + } - // game title is the file name without the extension or path - gi.Name = Path.GetFileNameWithoutExtension(GameFileImportPath); + Models.Signatures_Games discoveredSignature = new Models.Signatures_Games(); + if (signatures.Count == 1) + { + // only 1 signature found! + discoveredSignature = signatures.ElementAt(0); + gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, false); + } + else if (signatures.Count > 1) + { + // more than one signature found - find one with highest score + foreach (Models.Signatures_Games Sig in signatures) + { + if (Sig.Score > discoveredSignature.Score) + { + discoveredSignature = Sig; + gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, false); + } + } + } + else + { + // no signature match found - try alternate methods + Models.Signatures_Games.GameItem gi = new Models.Signatures_Games.GameItem(); + Models.Signatures_Games.RomItem ri = new Models.Signatures_Games.RomItem(); - // guess platform - gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, true); + discoveredSignature.Game = gi; + discoveredSignature.Rom = ri; - // get rom data - ri.Name = Path.GetFileName(GameFileImportPath); - ri.Md5 = hash.md5hash; - ri.Sha1 = hash.sha1hash; - } + // game title is the file name without the extension or path + gi.Name = Path.GetFileNameWithoutExtension(GameFileImportPath); - Console.WriteLine("Importing " + discoveredSignature.Game.Name + " (" + discoveredSignature.Game.Year + ") " + discoveredSignature.Game.System); - // get discovered platform - IGDB.Models.Platform determinedPlatform = Metadata.Platforms.GetPlatform(discoveredSignature.Flags.IGDBPlatformId); - // search discovered game - IGDB.Models.Game[] games = Metadata.Games.SearchForGame(discoveredSignature.Game.Name, discoveredSignature.Flags.IGDBPlatformId, Metadata.Games.SearchType.where); - if (games.Length == 0) - { - games = Metadata.Games.SearchForGame(discoveredSignature.Game.Name, discoveredSignature.Flags.IGDBPlatformId, Metadata.Games.SearchType.search); - } - if (games.Length > 0) - { - IGDB.Models.Game determinedGame = Metadata.Games.GetGame((long)games[0].Id); - Console.WriteLine(" IGDB game: " + determinedGame.Name); - } + // guess platform + gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, true); + + // get rom data + ri.Name = Path.GetFileName(GameFileImportPath); + ri.Md5 = hash.md5hash; + ri.Sha1 = hash.sha1hash; + ri.Size = fi.Length; + } + + Logging.Log(Logging.LogType.Information, "Import Game", " Determined import file as: " + discoveredSignature.Game.Name + " (" + discoveredSignature.Game.Year + ") " + discoveredSignature.Game.System); + // get discovered platform + IGDB.Models.Platform determinedPlatform = Metadata.Platforms.GetPlatform(discoveredSignature.Flags.IGDBPlatformId); + if (determinedPlatform == null) + { + determinedPlatform = new IGDB.Models.Platform(); + } + + // search discovered game - case insensitive exact match first + IGDB.Models.Game determinedGame = new IGDB.Models.Game(); + + foreach (Metadata.Games.SearchType searchType in Enum.GetValues(typeof(Metadata.Games.SearchType))) + { + Logging.Log(Logging.LogType.Information, "Import Game", " Search type: " + searchType.ToString()); + IGDB.Models.Game[] games = Metadata.Games.SearchForGame(discoveredSignature.Game.Name, discoveredSignature.Flags.IGDBPlatformId, searchType); + if (games.Length == 1) + { + // exact match! + determinedGame = Metadata.Games.GetGame((long)games[0].Id); + Logging.Log(Logging.LogType.Information, "Import Game", " IGDB game: " + determinedGame.Name); + break; + } + } + if (determinedGame == null) + { + determinedGame = new IGDB.Models.Game(); + } + + string destSlug = ""; + if (determinedGame.Id == null) + { + Logging.Log(Logging.LogType.Information, "Import Game", " Unable to determine game"); + } + + // add to database + sql = "INSERT INTO games_roms (platformid, gameid, name, size, crc, md5, sha1, developmentstatus, flags, romtype, romtypemedia, medialabel, path) VALUES (@platformid, @gameid, @name, @size, @crc, @md5, @sha1, @developmentstatus, @flags, @romtype, @romtypemedia, @medialabel, @path); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; + dbDict.Add("platformid", Common.ReturnValueIfNull(determinedPlatform.Id, 0)); + dbDict.Add("gameid", Common.ReturnValueIfNull(determinedGame.Id, 0)); + dbDict.Add("name", Common.ReturnValueIfNull(discoveredSignature.Rom.Name, "")); + dbDict.Add("size", Common.ReturnValueIfNull(discoveredSignature.Rom.Size, 0)); + dbDict.Add("crc", Common.ReturnValueIfNull(discoveredSignature.Rom.Crc, "")); + dbDict.Add("developmentstatus", Common.ReturnValueIfNull(discoveredSignature.Rom.DevelopmentStatus, "")); + + if (discoveredSignature.Rom.flags != null) + { + if (discoveredSignature.Rom.flags.Count > 0) + { + dbDict.Add("flags", Newtonsoft.Json.JsonConvert.SerializeObject(discoveredSignature.Rom.flags)); + } + else + { + dbDict.Add("flags", "[ ]"); + } + } + else + { + dbDict.Add("flags", "[ ]"); + } + dbDict.Add("romtype", (int)discoveredSignature.Rom.RomType); + dbDict.Add("romtypemedia", Common.ReturnValueIfNull(discoveredSignature.Rom.RomTypeMedia, "")); + dbDict.Add("medialabel", Common.ReturnValueIfNull(discoveredSignature.Rom.MediaLabel, "")); + dbDict.Add("path", GameFileImportPath); + + DataTable romInsert = db.ExecuteCMD(sql, dbDict); + long romId = (long)romInsert.Rows[0][0]; + + // move to destination + MoveGameFile(romId); + } } } } - } + + public static string ComputeROMPath(long RomId) + { + Classes.Roms.RomItem rom = Classes.Roms.GetRom(RomId); + + // get metadata + IGDB.Models.Platform platform = gaseous_server.Classes.Metadata.Platforms.GetPlatform(rom.PlatformId); + IGDB.Models.Game game = gaseous_server.Classes.Metadata.Games.GetGame(rom.GameId); + + // build path + string platformSlug = "Unknown Platform"; + if (platform != null) + { + platformSlug = platform.Slug; + } + string gameSlug = "Unknown Title"; + if (game != null) + { + gameSlug = game.Slug; + } + string DestinationPath = Path.Combine(Config.LibraryConfiguration.LibraryDataDirectory, gameSlug, platformSlug); + if (!Directory.Exists(DestinationPath)) + { + Directory.CreateDirectory(DestinationPath); + } + + string DestinationPathName = Path.Combine(DestinationPath, rom.Name); + + return DestinationPathName; + } + + public static void MoveGameFile(long RomId) + { + Classes.Roms.RomItem rom = Classes.Roms.GetRom(RomId); + string romPath = rom.Path; + + if (File.Exists(romPath)) + { + string DestinationPath = ComputeROMPath(RomId); + + if (romPath == DestinationPath) + { + Logging.Log(Logging.LogType.Debug, "Move Game ROM", "Destination path is the same as the current path - aborting"); + } + else + { + Logging.Log(Logging.LogType.Information, "Move Game ROM", "Moving " + romPath + " to " + DestinationPath); + if (File.Exists(DestinationPath)) + { + Logging.Log(Logging.LogType.Information, "Move Game ROM", "A file with the same name exists at the destination - aborting"); + } + else + { + File.Move(romPath, DestinationPath); + + // update the db + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "UPDATE games_roms SET path=@path WHERE id=@id"; + Dictionary dbDict = new Dictionary(); + dbDict.Add("id", RomId); + dbDict.Add("path", DestinationPath); + db.ExecuteCMD(sql, dbDict); + + } + } + } + else + { + Logging.Log(Logging.LogType.Warning, "Move Game ROM", "File " + romPath + " appears to be missing!"); + } + } + + public static void OrganiseLibrary() + { + // move rom files to their new location + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "SELECT * FROM games_roms"; + DataTable romDT = db.ExecuteCMD(sql); + + if (romDT.Rows.Count > 0) + { + foreach (DataRow dr in romDT.Rows) + { + long RomId = (long)dr["id"]; + MoveGameFile(RomId); + } + } + + // clean up empty directories + processDirectory(Config.LibraryConfiguration.LibraryDataDirectory); + } + + private static void processDirectory(string startLocation) + { + foreach (var directory in Directory.GetDirectories(startLocation)) + { + processDirectory(directory); + if (Directory.GetFiles(directory).Length == 0 && + Directory.GetDirectories(directory).Length == 0) + { + Directory.Delete(directory, false); + } + } + } + } } diff --git a/gaseous-server/Classes/Metadata/Games.cs b/gaseous-server/Classes/Metadata/Games.cs index 27dc20d..a23f3e2 100644 --- a/gaseous-server/Classes/Metadata/Games.cs +++ b/gaseous-server/Classes/Metadata/Games.cs @@ -146,9 +146,12 @@ namespace gaseous_server.Classes.Metadata searchBody += "search \"" + SearchString + "\"; "; searchBody += "where platforms = (" + PlatformId + ");"; break; - case SearchType.where: + case SearchType.wherefuzzy: searchBody += "where platforms = (" + PlatformId + ") & name ~ *\"" + SearchString + "\"*;"; break; + case SearchType.where: + searchBody += "where platforms = (" + PlatformId + ") & name ~ \"" + SearchString + "\";"; + break; } @@ -160,8 +163,9 @@ namespace gaseous_server.Classes.Metadata public enum SearchType { - where, - search + where = 0, + wherefuzzy = 1, + search = 2 } } } \ No newline at end of file diff --git a/gaseous-server/Classes/Metadata/PlatformLogos.cs b/gaseous-server/Classes/Metadata/PlatformLogos.cs index f873dad..8bf419c 100644 --- a/gaseous-server/Classes/Metadata/PlatformLogos.cs +++ b/gaseous-server/Classes/Metadata/PlatformLogos.cs @@ -73,13 +73,19 @@ namespace gaseous_server.Classes.Metadata { case Storage.CacheStatus.NotPresent: returnValue = await GetObjectFromServer(WhereClause, LogoPath); - Storage.NewCacheValue(returnValue); - forceImageDownload = true; + if (returnValue != null) + { + Storage.NewCacheValue(returnValue); + forceImageDownload = true; + } break; case Storage.CacheStatus.Expired: returnValue = await GetObjectFromServer(WhereClause, LogoPath); - Storage.NewCacheValue(returnValue, true); - forceImageDownload = true; + if (returnValue != null) + { + Storage.NewCacheValue(returnValue, true); + forceImageDownload = true; + } break; case Storage.CacheStatus.Current: returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); @@ -88,10 +94,13 @@ namespace gaseous_server.Classes.Metadata throw new Exception("How did you get here?"); } - if ((!File.Exists(Path.Combine(LogoPath, "Logo.jpg"))) || forceImageDownload == true) + if (returnValue != null) { - GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_thumb); - GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_logo_med); + if ((!File.Exists(Path.Combine(LogoPath, "Logo.jpg"))) || forceImageDownload == true) + { + GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_thumb); + GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_logo_med); + } } return returnValue; @@ -103,16 +112,23 @@ namespace gaseous_server.Classes.Metadata slug } - private static async Task GetObjectFromServer(string WhereClause, string LogoPath) + private static async Task GetObjectFromServer(string WhereClause, string LogoPath) { // get PlatformLogo metadata var results = await igdb.QueryAsync(IGDBClient.Endpoints.PlatformLogos, query: fieldList + " " + WhereClause + ";"); - var result = results.First(); + if (results.Length > 0) + { + var result = results.First(); - GetImageFromServer(result.Url, LogoPath, LogoSize.t_thumb); - GetImageFromServer(result.Url, LogoPath, LogoSize.t_logo_med); + GetImageFromServer(result.Url, LogoPath, LogoSize.t_thumb); + GetImageFromServer(result.Url, LogoPath, LogoSize.t_logo_med); - return result; + return result; + } + else + { + return null; + } } private static void GetImageFromServer(string Url, string LogoPath, LogoSize logoSize) diff --git a/gaseous-server/Classes/Metadata/PlatformVersions.cs b/gaseous-server/Classes/Metadata/PlatformVersions.cs index a15f6a0..39b98ff 100644 --- a/gaseous-server/Classes/Metadata/PlatformVersions.cs +++ b/gaseous-server/Classes/Metadata/PlatformVersions.cs @@ -71,13 +71,19 @@ namespace gaseous_server.Classes.Metadata { case Storage.CacheStatus.NotPresent: returnValue = await GetObjectFromServer(WhereClause); - Storage.NewCacheValue(returnValue); - UpdateSubClasses(ParentPlatform, returnValue); + if (returnValue != null) + { + Storage.NewCacheValue(returnValue); + UpdateSubClasses(ParentPlatform, returnValue); + } return returnValue; case Storage.CacheStatus.Expired: returnValue = await GetObjectFromServer(WhereClause); - Storage.NewCacheValue(returnValue, true); - UpdateSubClasses(ParentPlatform, returnValue); + if (returnValue != null) + { + Storage.NewCacheValue(returnValue, true); + UpdateSubClasses(ParentPlatform, returnValue); + } return returnValue; case Storage.CacheStatus.Current: return Storage.GetCacheValue(returnValue, "id", (long)searchValue); @@ -100,13 +106,20 @@ namespace gaseous_server.Classes.Metadata slug } - private static async Task GetObjectFromServer(string WhereClause) + private static async Task GetObjectFromServer(string WhereClause) { // get PlatformVersion metadata var results = await igdb.QueryAsync(IGDBClient.Endpoints.PlatformVersions, query: fieldList + " " + WhereClause + ";"); - var result = results.First(); + if (results.Length > 0) + { + var result = results.First(); - return result; + return result; + } + else + { + return null; + } } } } diff --git a/gaseous-server/Classes/Roms.cs b/gaseous-server/Classes/Roms.cs new file mode 100644 index 0000000..20f8a17 --- /dev/null +++ b/gaseous-server/Classes/Roms.cs @@ -0,0 +1,64 @@ +using System; +using System.Data; +using gaseous_tools; + +namespace gaseous_server.Classes +{ + public class Roms + { + public static RomItem GetRom(long RomId) + { + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "SELECT * FROM games_roms WHERE id = @id"; + Dictionary dbDict = new Dictionary(); + dbDict.Add("id", RomId); + DataTable romDT = db.ExecuteCMD(sql, dbDict); + + if (romDT.Rows.Count > 0) + { + DataRow romDR = romDT.Rows[0]; + RomItem romItem = new RomItem + { + Id = (long)romDR["id"], + PlatformId = (long)romDR["platformid"], + GameId = (long)romDR["gameid"], + Name = (string)romDR["name"], + Size = (long)romDR["size"], + CRC = (string)romDR["crc"], + MD5 = (string)romDR["md5"], + SHA1 = (string)romDR["sha1"], + DevelopmentStatus = (string)romDR["developmentstatus"], + Flags = Newtonsoft.Json.JsonConvert.DeserializeObject((string)romDR["flags"]), + RomType = (int)romDR["romtype"], + RomTypeMedia = (string)romDR["romtypemedia"], + MediaLabel = (string)romDR["medialabel"], + Path = (string)romDR["path"] + }; + return romItem; + } + else + { + throw new Exception("Unknown ROM Id"); + } + } + + public class RomItem + { + public long Id { get; set; } + public long PlatformId { get; set; } + public long GameId { get; set; } + public string? Name { get; set; } + public long Size { get; set; } + public string? CRC { get; set; } + public string? MD5 { get; set; } + public string? SHA1 { get; set; } + public string? DevelopmentStatus { get; set; } + public string[]? Flags { get; set; } + public int RomType { get; set; } + public string? RomTypeMedia { get; set; } + public string? MediaLabel { get; set; } + public string? Path { get; set; } + } + } +} + diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index dd471da..92a3abb 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -53,6 +53,9 @@ app.MapControllers(); // setup library directories Config.LibraryConfiguration.InitLibrary(); +// organise library +gaseous_server.Classes.ImportGame.OrganiseLibrary(); + // add background tasks ProcessQueue.QueueItems.Add(new ProcessQueue.QueueItem(ProcessQueue.QueueItemType.SignatureIngestor, 60)); ProcessQueue.QueueItems.Add(new ProcessQueue.QueueItem(ProcessQueue.QueueItemType.TitleIngestor, 1)); From e68f6003ba0fcfdd9b30825c8fbfabcf85362c2a Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Fri, 5 May 2023 00:29:38 +1000 Subject: [PATCH 12/71] feat: initial support for metadata refresh --- gaseous-server/Classes/ImportGames.cs | 13 +++++--- gaseous-server/Classes/Metadata/Games.cs | 32 +++++++++++++++----- gaseous-server/Classes/Metadata/Storage.cs | 2 +- gaseous-server/Classes/MetadataManagement.cs | 23 ++++++++++++++ gaseous-server/ProcessQueue.cs | 8 ++++- gaseous-server/Program.cs | 3 +- 6 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 gaseous-server/Classes/MetadataManagement.cs diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs index 257f27a..b13f618 100644 --- a/gaseous-server/Classes/ImportGames.cs +++ b/gaseous-server/Classes/ImportGames.cs @@ -134,7 +134,7 @@ namespace gaseous_server.Classes if (games.Length == 1) { // exact match! - determinedGame = Metadata.Games.GetGame((long)games[0].Id); + determinedGame = Metadata.Games.GetGame((long)games[0].Id, false); Logging.Log(Logging.LogType.Information, "Import Game", " IGDB game: " + determinedGame.Name); break; } @@ -195,7 +195,7 @@ namespace gaseous_server.Classes // get metadata IGDB.Models.Platform platform = gaseous_server.Classes.Metadata.Platforms.GetPlatform(rom.PlatformId); - IGDB.Models.Game game = gaseous_server.Classes.Metadata.Games.GetGame(rom.GameId); + IGDB.Models.Game game = gaseous_server.Classes.Metadata.Games.GetGame(rom.GameId, false); // build path string platformSlug = "Unknown Platform"; @@ -262,7 +262,9 @@ namespace gaseous_server.Classes public static void OrganiseLibrary() { - // move rom files to their new location + Logging.Log(Logging.LogType.Information, "Organise Library", "Starting library organisation"); + + // move rom files to their new location Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); string sql = "SELECT * FROM games_roms"; DataTable romDT = db.ExecuteCMD(sql); @@ -271,13 +273,16 @@ namespace gaseous_server.Classes { foreach (DataRow dr in romDT.Rows) { - long RomId = (long)dr["id"]; + Logging.Log(Logging.LogType.Information, "Organise Library", "Processing ROM " + dr["name"]); + long RomId = (long)dr["id"]; MoveGameFile(RomId); } } // clean up empty directories processDirectory(Config.LibraryConfiguration.LibraryDataDirectory); + + Logging.Log(Logging.LogType.Information, "Organise Library", "Finsihed library organisation"); } private static void processDirectory(string startLocation) diff --git a/gaseous-server/Classes/Metadata/Games.cs b/gaseous-server/Classes/Metadata/Games.cs index a23f3e2..9b60e23 100644 --- a/gaseous-server/Classes/Metadata/Games.cs +++ b/gaseous-server/Classes/Metadata/Games.cs @@ -20,7 +20,7 @@ namespace gaseous_server.Classes.Metadata Config.IGDB.Secret ); - public static Game? GetGame(long Id) + public static Game? GetGame(long Id, bool followSubGames) { if (Id == 0) { @@ -28,18 +28,18 @@ namespace gaseous_server.Classes.Metadata } else { - Task RetVal = _GetGame(SearchUsing.id, Id); + Task RetVal = _GetGame(SearchUsing.id, Id, followSubGames); return RetVal.Result; } } - public static Game GetGame(string Slug) + public static Game GetGame(string Slug, bool followSubGames) { - Task RetVal = _GetGame(SearchUsing.slug, Slug); + Task RetVal = _GetGame(SearchUsing.slug, Slug, followSubGames); return RetVal.Result; } - private static async Task _GetGame(SearchUsing searchUsing, object searchValue) + private static async Task _GetGame(SearchUsing searchUsing, object searchValue, bool followSubGames = false) { // check database first Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); @@ -72,12 +72,12 @@ namespace gaseous_server.Classes.Metadata case Storage.CacheStatus.NotPresent: returnValue = await GetObjectFromServer(WhereClause); Storage.NewCacheValue(returnValue); - UpdateSubClasses(returnValue); + UpdateSubClasses(returnValue, followSubGames); return returnValue; case Storage.CacheStatus.Expired: returnValue = await GetObjectFromServer(WhereClause); Storage.NewCacheValue(returnValue, true); - UpdateSubClasses(returnValue); + UpdateSubClasses(returnValue, followSubGames); return returnValue; case Storage.CacheStatus.Current: return Storage.GetCacheValue(returnValue, "id", (long)searchValue); @@ -86,7 +86,7 @@ namespace gaseous_server.Classes.Metadata } } - private static void UpdateSubClasses(Game Game) + private static void UpdateSubClasses(Game Game, bool followSubGames) { if (Game.Artworks != null) { @@ -95,6 +95,22 @@ namespace gaseous_server.Classes.Metadata Artwork GameArtwork = Artworks.GetArtwork(ArtworkId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(Game)); } } + if (followSubGames) + { + List gamesToFetch = new List(); + if (Game.Bundles != null) { gamesToFetch.AddRange(Game.Bundles.Ids); } + if (Game.Dlcs != null) { gamesToFetch.AddRange(Game.Dlcs.Ids); } + if (Game.Expansions != null) { gamesToFetch.AddRange(Game.Expansions.Ids); } + if (Game.ParentGame != null) { gamesToFetch.Add((long)Game.ParentGame.Id); } + if (Game.SimilarGames != null) { gamesToFetch.AddRange(Game.SimilarGames.Ids); } + if (Game.StandaloneExpansions != null) { gamesToFetch.AddRange(Game.StandaloneExpansions.Ids); } + if (Game.VersionParent != null) { gamesToFetch.Add((long)Game.VersionParent.Id); } + + foreach (long gameId in gamesToFetch) + { + Game relatedGame = GetGame(gameId, false); + } + } if (Game.Cover != null) { Cover GameCover = Covers.GetCover(Game.Cover.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(Game)); diff --git a/gaseous-server/Classes/Metadata/Storage.cs b/gaseous-server/Classes/Metadata/Storage.cs index 8ac0ac6..f712385 100644 --- a/gaseous-server/Classes/Metadata/Storage.cs +++ b/gaseous-server/Classes/Metadata/Storage.cs @@ -44,7 +44,7 @@ namespace gaseous_server.Classes.Metadata } else { - DateTime CacheExpiryTime = DateTime.UtcNow.AddHours(-24); + DateTime CacheExpiryTime = DateTime.UtcNow.AddHours(-168); if ((DateTime)dt.Rows[0]["lastUpdated"] < CacheExpiryTime) { return CacheStatus.Expired; diff --git a/gaseous-server/Classes/MetadataManagement.cs b/gaseous-server/Classes/MetadataManagement.cs new file mode 100644 index 0000000..90e9f96 --- /dev/null +++ b/gaseous-server/Classes/MetadataManagement.cs @@ -0,0 +1,23 @@ +using System; +using System.Data; +using gaseous_tools; + +namespace gaseous_server.Classes +{ + public class MetadataManagement + { + public static void RefreshMetadata() + { + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "SELECT id, `name` FROM game;"; + DataTable dt = db.ExecuteCMD(sql); + + foreach (DataRow dr in dt.Rows) + { + Logging.Log(Logging.LogType.Information, "Metadata Refresh", "Refreshing metadata for game " + dr["name"] + " (" + dr["id"] + ")"); + Metadata.Games.GetGame((long)dr["id"], true); + } + } + } +} + diff --git a/gaseous-server/ProcessQueue.cs b/gaseous-server/ProcessQueue.cs index 07b01ad..4a9a781 100644 --- a/gaseous-server/ProcessQueue.cs +++ b/gaseous-server/ProcessQueue.cs @@ -69,6 +69,11 @@ namespace gaseous_server Logging.Log(Logging.LogType.Information, "Timered Event", "Starting Title Ingestor"); Classes.ImportGames importGames = new Classes.ImportGames(Config.LibraryConfiguration.LibraryImportDirectory); break; + + case QueueItemType.MetadataRefresh: + Logging.Log(Logging.LogType.Information, "Timered Event", "Starting Metadata Refresher"); + Classes.MetadataManagement.RefreshMetadata(); + break; } } catch (Exception ex) @@ -95,7 +100,8 @@ namespace gaseous_server { NotConfigured, SignatureIngestor, - TitleIngestor + TitleIngestor, + MetadataRefresh } public enum QueueItemState diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index 92a3abb..3c5fa0d 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -54,11 +54,12 @@ app.MapControllers(); Config.LibraryConfiguration.InitLibrary(); // organise library -gaseous_server.Classes.ImportGame.OrganiseLibrary(); +//gaseous_server.Classes.ImportGame.OrganiseLibrary(); // add background tasks ProcessQueue.QueueItems.Add(new ProcessQueue.QueueItem(ProcessQueue.QueueItemType.SignatureIngestor, 60)); ProcessQueue.QueueItems.Add(new ProcessQueue.QueueItem(ProcessQueue.QueueItemType.TitleIngestor, 1)); +ProcessQueue.QueueItems.Add(new ProcessQueue.QueueItem(ProcessQueue.QueueItemType.MetadataRefresh, 360)); // start the app app.Run(); From 520380243d4caab4db33ffccc842c5c36da7c5a5 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Fri, 5 May 2023 09:53:36 +1000 Subject: [PATCH 13/71] fix: suppress log entries for items in the import folder that already exist in the database --- gaseous-server/Classes/ImportGames.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs index b13f618..be35aa9 100644 --- a/gaseous-server/Classes/ImportGames.cs +++ b/gaseous-server/Classes/ImportGames.cs @@ -46,7 +46,7 @@ namespace gaseous_server.Classes } else { - Logging.Log(Logging.LogType.Information, "Import Game", "Processing item " + GameFileImportPath); + //Logging.Log(Logging.LogType.Information, "Import Game", "Processing item " + GameFileImportPath); if (IsDirectory == false) { FileInfo fi = new FileInfo(GameFileImportPath); @@ -59,7 +59,10 @@ namespace gaseous_server.Classes DataTable importDB = db.ExecuteCMD(sql, dbDict); if ((Int64)importDB.Rows[0]["count"] > 0) { - Logging.Log(Logging.LogType.Information, "Import Game", " " + GameFileImportPath + " already in database - skipping"); + if (!GameFileImportPath.StartsWith(Config.LibraryConfiguration.LibraryImportDirectory)) + { + Logging.Log(Logging.LogType.Information, "Import Game", " " + GameFileImportPath + " already in database - skipping"); + } } else { From 5732c19ca49ac81867d3ddd5b1ee5ddf4ec8cf90 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Tue, 16 May 2023 00:39:54 +1000 Subject: [PATCH 14/71] feat: enhanced title matching, added more metadata types --- gaseous-server/Classes/ImportGames.cs | 64 +++-- gaseous-server/Classes/Metadata/AgeRating.cs | 120 ++++++++ .../Metadata/AgeRatingContentDescriptions.cs | 107 +++++++ .../Classes/Metadata/AlternativeNames.cs | 107 +++++++ .../Classes/Metadata/Collections.cs | 107 +++++++ .../Classes/Metadata/ExternalGames.cs | 120 ++++++++ gaseous-server/Classes/Metadata/Franchises.cs | 107 +++++++ gaseous-server/Classes/Metadata/GameVideos.cs | 110 +++++++ gaseous-server/Classes/Metadata/Games.cs | 69 ++++- gaseous-server/Classes/Metadata/Genres.cs | 110 +++++++ gaseous-server/Classes/Metadata/Storage.cs | 9 + gaseous-server/Classes/MetadataManagement.cs | 4 +- .../Controllers/SignaturesController.cs | 13 + gaseous-server/ProcessQueue.cs | 2 +- gaseous-tools/Database/MySQL/gaseous-1000.sql | 272 +++++++++++++++--- 15 files changed, 1260 insertions(+), 61 deletions(-) create mode 100644 gaseous-server/Classes/Metadata/AgeRating.cs create mode 100644 gaseous-server/Classes/Metadata/AgeRatingContentDescriptions.cs create mode 100644 gaseous-server/Classes/Metadata/AlternativeNames.cs create mode 100644 gaseous-server/Classes/Metadata/Collections.cs create mode 100644 gaseous-server/Classes/Metadata/ExternalGames.cs create mode 100644 gaseous-server/Classes/Metadata/Franchises.cs create mode 100644 gaseous-server/Classes/Metadata/GameVideos.cs create mode 100644 gaseous-server/Classes/Metadata/Genres.cs diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs index be35aa9..5e69368 100644 --- a/gaseous-server/Classes/ImportGames.cs +++ b/gaseous-server/Classes/ImportGames.cs @@ -3,6 +3,7 @@ using System.Data; using System.Threading.Tasks; using gaseous_tools; using Org.BouncyCastle.Utilities.IO.Pem; +using static gaseous_server.Classes.Metadata.Games; namespace gaseous_server.Classes { @@ -99,24 +100,48 @@ namespace gaseous_server.Classes } else { - // no signature match found - try alternate methods - Models.Signatures_Games.GameItem gi = new Models.Signatures_Games.GameItem(); - Models.Signatures_Games.RomItem ri = new Models.Signatures_Games.RomItem(); + // no signature match found - try name search + signatures = sc.GetByTosecName(fi.Name); - discoveredSignature.Game = gi; - discoveredSignature.Rom = ri; + if (signatures.Count == 1) + { + // only 1 signature found! + discoveredSignature = signatures.ElementAt(0); + gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, false); + } + else if (signatures.Count > 1) + { + // more than one signature found - find one with highest score + foreach (Models.Signatures_Games Sig in signatures) + { + if (Sig.Score > discoveredSignature.Score) + { + discoveredSignature = Sig; + gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, false); + } + } + } + else + { + // still no search - try alternate method + Models.Signatures_Games.GameItem gi = new Models.Signatures_Games.GameItem(); + Models.Signatures_Games.RomItem ri = new Models.Signatures_Games.RomItem(); - // game title is the file name without the extension or path - gi.Name = Path.GetFileNameWithoutExtension(GameFileImportPath); + discoveredSignature.Game = gi; + discoveredSignature.Rom = ri; - // guess platform - gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, true); + // game title is the file name without the extension or path + gi.Name = Path.GetFileNameWithoutExtension(GameFileImportPath); - // get rom data - ri.Name = Path.GetFileName(GameFileImportPath); - ri.Md5 = hash.md5hash; - ri.Sha1 = hash.sha1hash; - ri.Size = fi.Length; + // guess platform + gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, true); + + // get rom data + ri.Name = Path.GetFileName(GameFileImportPath); + ri.Md5 = hash.md5hash; + ri.Sha1 = hash.sha1hash; + ri.Size = fi.Length; + } } Logging.Log(Logging.LogType.Information, "Import Game", " Determined import file as: " + discoveredSignature.Game.Name + " (" + discoveredSignature.Game.Year + ") " + discoveredSignature.Game.System); @@ -127,6 +152,13 @@ namespace gaseous_server.Classes determinedPlatform = new IGDB.Models.Platform(); } + // remove string ending ", The" if present + if (discoveredSignature.Game.Name.Contains(", The")) + { + Logging.Log(Logging.LogType.Information, "Import Game", " Removing ', The' from end of game title for search"); + discoveredSignature.Game.Name.Replace(", The", ""); + } + // search discovered game - case insensitive exact match first IGDB.Models.Game determinedGame = new IGDB.Models.Game(); @@ -137,7 +169,7 @@ namespace gaseous_server.Classes if (games.Length == 1) { // exact match! - determinedGame = Metadata.Games.GetGame((long)games[0].Id, false); + determinedGame = Metadata.Games.GetGame((long)games[0].Id, false, false); Logging.Log(Logging.LogType.Information, "Import Game", " IGDB game: " + determinedGame.Name); break; } @@ -198,7 +230,7 @@ namespace gaseous_server.Classes // get metadata IGDB.Models.Platform platform = gaseous_server.Classes.Metadata.Platforms.GetPlatform(rom.PlatformId); - IGDB.Models.Game game = gaseous_server.Classes.Metadata.Games.GetGame(rom.GameId, false); + IGDB.Models.Game game = gaseous_server.Classes.Metadata.Games.GetGame(rom.GameId, false, false); // build path string platformSlug = "Unknown Platform"; diff --git a/gaseous-server/Classes/Metadata/AgeRating.cs b/gaseous-server/Classes/Metadata/AgeRating.cs new file mode 100644 index 0000000..7057fa6 --- /dev/null +++ b/gaseous-server/Classes/Metadata/AgeRating.cs @@ -0,0 +1,120 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; +using MySqlX.XDevAPI.Common; +using static gaseous_tools.Config.ConfigFile; + +namespace gaseous_server.Classes.Metadata +{ + public class AgeRatings + { + const string fieldList = "fields category,checksum,content_descriptions,rating,rating_cover_url,synopsis;"; + + public AgeRatings() + { + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static AgeRating? GetAgeRatings(long? Id) + { + if ((Id == 0) || (Id == null)) + { + return null; + } + else + { + Task RetVal = _GetAgeRatings(SearchUsing.id, Id); + return RetVal.Result; + } + } + + public static AgeRating GetAgeRatings(string Slug) + { + Task RetVal = _GetAgeRatings(SearchUsing.slug, Slug); + return RetVal.Result; + } + + private static async Task _GetAgeRatings(SearchUsing searchUsing, object searchValue) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("AgeRating", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("AgeRating", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + AgeRating returnValue = new AgeRating(); + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue); + UpdateSubClasses(returnValue); + break; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue, true); + UpdateSubClasses(returnValue); + break; + case Storage.CacheStatus.Current: + returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + break; + default: + throw new Exception("How did you get here?"); + } + + return returnValue; + } + + private static void UpdateSubClasses(AgeRating ageRating) + { + if (ageRating.ContentDescriptions != null) + { + foreach (long AgeRatingContentDescriptionId in ageRating.ContentDescriptions.Ids) + { + AgeRatingContentDescription ageRatingContentDescription = AgeRatingContentDescriptions.GetAgeRatingContentDescriptions(AgeRatingContentDescriptionId); + } + } + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause) + { + // get AgeRatings metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.AgeRating, query: fieldList + " " + WhereClause + ";"); + var result = results.First(); + + return result; + } + } +} + diff --git a/gaseous-server/Classes/Metadata/AgeRatingContentDescriptions.cs b/gaseous-server/Classes/Metadata/AgeRatingContentDescriptions.cs new file mode 100644 index 0000000..eeec99c --- /dev/null +++ b/gaseous-server/Classes/Metadata/AgeRatingContentDescriptions.cs @@ -0,0 +1,107 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; +using MySqlX.XDevAPI.Common; +using static gaseous_tools.Config.ConfigFile; + +namespace gaseous_server.Classes.Metadata +{ + public class AgeRatingContentDescriptions + { + const string fieldList = "fields category,checksum,description;"; + + public AgeRatingContentDescriptions() + { + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static AgeRatingContentDescription? GetAgeRatingContentDescriptions(long? Id) + { + if ((Id == 0) || (Id == null)) + { + return null; + } + else + { + Task RetVal = _GetAgeRatingContentDescriptions(SearchUsing.id, Id); + return RetVal.Result; + } + } + + public static AgeRatingContentDescription GetAgeRatingContentDescriptions(string Slug) + { + Task RetVal = _GetAgeRatingContentDescriptions(SearchUsing.slug, Slug); + return RetVal.Result; + } + + private static async Task _GetAgeRatingContentDescriptions(SearchUsing searchUsing, object searchValue) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("AgeRatingContentDescription", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("AgeRatingContentDescription", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + AgeRatingContentDescription returnValue = new AgeRatingContentDescription(); + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue); + break; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue, true); + break; + case Storage.CacheStatus.Current: + returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + break; + default: + throw new Exception("How did you get here?"); + } + + return returnValue; + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause) + { + // get AgeRatingContentDescriptionContentDescriptions metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.AgeRatingContentDescriptions, query: fieldList + " " + WhereClause + ";"); + var result = results.First(); + + return result; + } + } +} + diff --git a/gaseous-server/Classes/Metadata/AlternativeNames.cs b/gaseous-server/Classes/Metadata/AlternativeNames.cs new file mode 100644 index 0000000..3a5fe91 --- /dev/null +++ b/gaseous-server/Classes/Metadata/AlternativeNames.cs @@ -0,0 +1,107 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; +using MySqlX.XDevAPI.Common; +using static gaseous_tools.Config.ConfigFile; + +namespace gaseous_server.Classes.Metadata +{ + public class AlternativeNames + { + const string fieldList = "fields checksum,comment,game,name;"; + + public AlternativeNames() + { + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static AlternativeName? GetAlternativeNames(long? Id) + { + if ((Id == 0) || (Id == null)) + { + return null; + } + else + { + Task RetVal = _GetAlternativeNames(SearchUsing.id, Id); + return RetVal.Result; + } + } + + public static AlternativeName GetAlternativeNames(string Slug) + { + Task RetVal = _GetAlternativeNames(SearchUsing.slug, Slug); + return RetVal.Result; + } + + private static async Task _GetAlternativeNames(SearchUsing searchUsing, object searchValue) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("AlternativeName", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("AlternativeName", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + AlternativeName returnValue = new AlternativeName(); + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue); + break; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue, true); + break; + case Storage.CacheStatus.Current: + returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + break; + default: + throw new Exception("How did you get here?"); + } + + return returnValue; + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause) + { + // get AlternativeNames metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.AlternativeNames, query: fieldList + " " + WhereClause + ";"); + var result = results.First(); + + return result; + } + } +} + diff --git a/gaseous-server/Classes/Metadata/Collections.cs b/gaseous-server/Classes/Metadata/Collections.cs new file mode 100644 index 0000000..956ddf6 --- /dev/null +++ b/gaseous-server/Classes/Metadata/Collections.cs @@ -0,0 +1,107 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; +using MySqlX.XDevAPI.Common; +using static gaseous_tools.Config.ConfigFile; + +namespace gaseous_server.Classes.Metadata +{ + public class Collections + { + const string fieldList = "fields checksum,created_at,games,name,slug,updated_at,url;"; + + public Collections() + { + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static Collection? GetCollections(long? Id) + { + if ((Id == 0) || (Id == null)) + { + return null; + } + else + { + Task RetVal = _GetCollections(SearchUsing.id, Id); + return RetVal.Result; + } + } + + public static Collection GetCollections(string Slug) + { + Task RetVal = _GetCollections(SearchUsing.slug, Slug); + return RetVal.Result; + } + + private static async Task _GetCollections(SearchUsing searchUsing, object searchValue) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("Collection", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("Collection", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + Collection returnValue = new Collection(); + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue); + break; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue, true); + break; + case Storage.CacheStatus.Current: + returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + break; + default: + throw new Exception("How did you get here?"); + } + + return returnValue; + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause) + { + // get Collections metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.Collections, query: fieldList + " " + WhereClause + ";"); + var result = results.First(); + + return result; + } + } +} + diff --git a/gaseous-server/Classes/Metadata/ExternalGames.cs b/gaseous-server/Classes/Metadata/ExternalGames.cs new file mode 100644 index 0000000..348365d --- /dev/null +++ b/gaseous-server/Classes/Metadata/ExternalGames.cs @@ -0,0 +1,120 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; +using MySqlX.XDevAPI.Common; +using static gaseous_tools.Config.ConfigFile; + +namespace gaseous_server.Classes.Metadata +{ + public class ExternalGames + { + const string fieldList = "fields category,checksum,countries,created_at,game,media,name,platform,uid,updated_at,url,year;"; + + public ExternalGames() + { + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static ExternalGame? GetExternalGames(long? Id) + { + if ((Id == 0) || (Id == null)) + { + return null; + } + else + { + Task RetVal = _GetExternalGames(SearchUsing.id, Id); + return RetVal.Result; + } + } + + public static ExternalGame GetExternalGames(string Slug) + { + Task RetVal = _GetExternalGames(SearchUsing.slug, Slug); + return RetVal.Result; + } + + private static async Task _GetExternalGames(SearchUsing searchUsing, object searchValue) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("ExternalGame", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("ExternalGame", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + ExternalGame returnValue = new ExternalGame(); + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause); + if (returnValue != null) + { + Storage.NewCacheValue(returnValue); + } + break; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause); + if (returnValue != null) + { + Storage.NewCacheValue(returnValue, true); + } + break; + case Storage.CacheStatus.Current: + returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + break; + default: + throw new Exception("How did you get here?"); + } + + return returnValue; + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause) + { + // get ExternalGames metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.ExternalGames, query: fieldList + " " + WhereClause + ";"); + if (results.Length > 0) + { + var result = results.First(); + + return result; + } + else + { + return null; + } + } + } +} + diff --git a/gaseous-server/Classes/Metadata/Franchises.cs b/gaseous-server/Classes/Metadata/Franchises.cs new file mode 100644 index 0000000..f688ceb --- /dev/null +++ b/gaseous-server/Classes/Metadata/Franchises.cs @@ -0,0 +1,107 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; +using MySqlX.XDevAPI.Common; +using static gaseous_tools.Config.ConfigFile; + +namespace gaseous_server.Classes.Metadata +{ + public class Franchises + { + const string fieldList = "fields checksum,created_at,games,name,slug,updated_at,url;"; + + public Franchises() + { + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static Franchise? GetFranchises(long? Id) + { + if ((Id == 0) || (Id == null)) + { + return null; + } + else + { + Task RetVal = _GetFranchises(SearchUsing.id, Id); + return RetVal.Result; + } + } + + public static Franchise GetFranchises(string Slug) + { + Task RetVal = _GetFranchises(SearchUsing.slug, Slug); + return RetVal.Result; + } + + private static async Task _GetFranchises(SearchUsing searchUsing, object searchValue) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("Franchise", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("Franchise", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + Franchise returnValue = new Franchise(); + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue); + break; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue, true); + break; + case Storage.CacheStatus.Current: + returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + break; + default: + throw new Exception("How did you get here?"); + } + + return returnValue; + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause) + { + // get FranchiseContentDescriptions metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.Franchies, query: fieldList + " " + WhereClause + ";"); + var result = results.First(); + + return result; + } + } +} + diff --git a/gaseous-server/Classes/Metadata/GameVideos.cs b/gaseous-server/Classes/Metadata/GameVideos.cs new file mode 100644 index 0000000..63c8f21 --- /dev/null +++ b/gaseous-server/Classes/Metadata/GameVideos.cs @@ -0,0 +1,110 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; +using MySqlX.XDevAPI.Common; +using static gaseous_tools.Config.ConfigFile; + +namespace gaseous_server.Classes.Metadata +{ + public class GamesVideos + { + const string fieldList = "fields checksum,game,name,video_id;"; + + public GamesVideos() + { + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static GameVideo? GetGame_Videos(long? Id) + { + if ((Id == 0) || (Id == null)) + { + return null; + } + else + { + Task RetVal = _GetGame_Videos(SearchUsing.id, Id); + return RetVal.Result; + } + } + + public static GameVideo GetGame_Videos(string Slug) + { + Task RetVal = _GetGame_Videos(SearchUsing.slug, Slug); + return RetVal.Result; + } + + private static async Task _GetGame_Videos(SearchUsing searchUsing, object searchValue) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("GameVideo", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("GameVideo", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + GameVideo returnValue = new GameVideo(); + bool forceImageDownload = false; + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue); + forceImageDownload = true; + break; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue, true); + forceImageDownload = true; + break; + case Storage.CacheStatus.Current: + returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + break; + default: + throw new Exception("How did you get here?"); + } + + return returnValue; + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause) + { + // get Game_Videos metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.GameVideos, query: fieldList + " " + WhereClause + ";"); + var result = results.First(); + + return result; + } + } +} + diff --git a/gaseous-server/Classes/Metadata/Games.cs b/gaseous-server/Classes/Metadata/Games.cs index 9b60e23..ba235de 100644 --- a/gaseous-server/Classes/Metadata/Games.cs +++ b/gaseous-server/Classes/Metadata/Games.cs @@ -20,7 +20,7 @@ namespace gaseous_server.Classes.Metadata Config.IGDB.Secret ); - public static Game? GetGame(long Id, bool followSubGames) + public static Game? GetGame(long Id, bool followSubGames, bool forceRefresh) { if (Id == 0) { @@ -28,18 +28,18 @@ namespace gaseous_server.Classes.Metadata } else { - Task RetVal = _GetGame(SearchUsing.id, Id, followSubGames); + Task RetVal = _GetGame(SearchUsing.id, Id, followSubGames, forceRefresh); return RetVal.Result; } } - public static Game GetGame(string Slug, bool followSubGames) + public static Game GetGame(string Slug, bool followSubGames, bool forceRefresh) { - Task RetVal = _GetGame(SearchUsing.slug, Slug, followSubGames); + Task RetVal = _GetGame(SearchUsing.slug, Slug, followSubGames, forceRefresh); return RetVal.Result; } - private static async Task _GetGame(SearchUsing searchUsing, object searchValue, bool followSubGames = false) + private static async Task _GetGame(SearchUsing searchUsing, object searchValue, bool followSubGames = false, bool forceRefresh = false) { // check database first Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); @@ -52,6 +52,11 @@ namespace gaseous_server.Classes.Metadata cacheStatus = Storage.GetCacheStatus("Game", (string)searchValue); } + if (forceRefresh == true) + { + if (cacheStatus == Storage.CacheStatus.Current) { cacheStatus = Storage.CacheStatus.Expired; } + } + // set up where clause string WhereClause = ""; switch (searchUsing) @@ -88,6 +93,20 @@ namespace gaseous_server.Classes.Metadata private static void UpdateSubClasses(Game Game, bool followSubGames) { + if (Game.AgeRatings != null) + { + foreach (long AgeRatingId in Game.AgeRatings.Ids) + { + AgeRating GameAgeRating = AgeRatings.GetAgeRatings(AgeRatingId); + } + } + if (Game.AlternativeNames != null) + { + foreach (long AlternativeNameId in Game.AlternativeNames.Ids) + { + AlternativeName GameAlternativeName = AlternativeNames.GetAlternativeNames(AlternativeNameId); + } + } if (Game.Artworks != null) { foreach (long ArtworkId in Game.Artworks.Ids) @@ -102,19 +121,48 @@ namespace gaseous_server.Classes.Metadata if (Game.Dlcs != null) { gamesToFetch.AddRange(Game.Dlcs.Ids); } if (Game.Expansions != null) { gamesToFetch.AddRange(Game.Expansions.Ids); } if (Game.ParentGame != null) { gamesToFetch.Add((long)Game.ParentGame.Id); } - if (Game.SimilarGames != null) { gamesToFetch.AddRange(Game.SimilarGames.Ids); } + //if (Game.SimilarGames != null) { gamesToFetch.AddRange(Game.SimilarGames.Ids); } if (Game.StandaloneExpansions != null) { gamesToFetch.AddRange(Game.StandaloneExpansions.Ids); } if (Game.VersionParent != null) { gamesToFetch.Add((long)Game.VersionParent.Id); } foreach (long gameId in gamesToFetch) { - Game relatedGame = GetGame(gameId, false); + Game relatedGame = GetGame(gameId, false, false); } } + if (Game.Collection != null) + { + Collection GameCollection = Collections.GetCollections(Game.Collection.Id); + } if (Game.Cover != null) { Cover GameCover = Covers.GetCover(Game.Cover.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(Game)); } + if (Game.ExternalGames != null) + { + foreach (long ExternalGameId in Game.ExternalGames.Ids) + { + ExternalGame GameExternalGame = ExternalGames.GetExternalGames(ExternalGameId); + } + } + if (Game.Franchise != null) + { + Franchise GameFranchise = Franchises.GetFranchises(Game.Franchise.Id); + } + if (Game.Franchises != null) + { + foreach (long FranchiseId in Game.Franchises.Ids) + { + Franchise GameFranchise = Franchises.GetFranchises(FranchiseId); + } + } + if (Game.Genres != null) + { + foreach (long GenreId in Game.Genres.Ids) + { + Genre GameGenre = Genres.GetGenres(GenreId); + } + } if (Game.Platforms != null) { foreach (long PlatformId in Game.Platforms.Ids) @@ -129,6 +177,13 @@ namespace gaseous_server.Classes.Metadata Screenshot GameScreenshot = Screenshots.GetScreenshot(ScreenshotId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(Game)); } } + if (Game.Videos != null) + { + foreach (long GameVideoId in Game.Videos.Ids) + { + GameVideo gameVideo = GamesVideos.GetGame_Videos(GameVideoId); + } + } } private enum SearchUsing diff --git a/gaseous-server/Classes/Metadata/Genres.cs b/gaseous-server/Classes/Metadata/Genres.cs new file mode 100644 index 0000000..5995e48 --- /dev/null +++ b/gaseous-server/Classes/Metadata/Genres.cs @@ -0,0 +1,110 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; +using MySqlX.XDevAPI.Common; +using static gaseous_tools.Config.ConfigFile; + +namespace gaseous_server.Classes.Metadata +{ + public class Genres + { + const string fieldList = "fields checksum,created_at,name,slug,updated_at,url;"; + + public Genres() + { + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static Genre? GetGenres(long? Id) + { + if ((Id == 0) || (Id == null)) + { + return null; + } + else + { + Task RetVal = _GetGenres(SearchUsing.id, Id); + return RetVal.Result; + } + } + + public static Genre GetGenres(string Slug) + { + Task RetVal = _GetGenres(SearchUsing.slug, Slug); + return RetVal.Result; + } + + private static async Task _GetGenres(SearchUsing searchUsing, object searchValue) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("Genre", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("Genre", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + Genre returnValue = new Genre(); + bool forceImageDownload = false; + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue); + forceImageDownload = true; + break; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue, true); + forceImageDownload = true; + break; + case Storage.CacheStatus.Current: + returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + break; + default: + throw new Exception("How did you get here?"); + } + + return returnValue; + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause) + { + // get Genres metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.Genres, query: fieldList + " " + WhereClause + ";"); + var result = results.First(); + + return result; + } + } +} + diff --git a/gaseous-server/Classes/Metadata/Storage.cs b/gaseous-server/Classes/Metadata/Storage.cs index f712385..decba18 100644 --- a/gaseous-server/Classes/Metadata/Storage.cs +++ b/gaseous-server/Classes/Metadata/Storage.cs @@ -243,6 +243,15 @@ namespace gaseous_server.Classes.Metadata break; case "[igdb.models.gamestatus": property.SetValue(EndpointType, (GameStatus)dataRow[property.Name]); + break; + case "[igdb.models.ageratingcategory": + property.SetValue(EndpointType, (AgeRatingCategory)dataRow[property.Name]); + break; + case "[igdb.models.ageratingtitle": + property.SetValue(EndpointType, (AgeRatingTitle)dataRow[property.Name]); + break; + case "[igdb.models.externalcategory": + property.SetValue(EndpointType, (ExternalCategory)dataRow[property.Name]); break; default: property.SetValue(EndpointType, dataRow[property.Name]); diff --git a/gaseous-server/Classes/MetadataManagement.cs b/gaseous-server/Classes/MetadataManagement.cs index 90e9f96..4c51df2 100644 --- a/gaseous-server/Classes/MetadataManagement.cs +++ b/gaseous-server/Classes/MetadataManagement.cs @@ -6,7 +6,7 @@ namespace gaseous_server.Classes { public class MetadataManagement { - public static void RefreshMetadata() + public static void RefreshMetadata(bool forceRefresh = false) { Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); string sql = "SELECT id, `name` FROM game;"; @@ -15,7 +15,7 @@ namespace gaseous_server.Classes foreach (DataRow dr in dt.Rows) { Logging.Log(Logging.LogType.Information, "Metadata Refresh", "Refreshing metadata for game " + dr["name"] + " (" + dr["id"] + ")"); - Metadata.Games.GetGame((long)dr["id"], true); + Metadata.Games.GetGame((long)dr["id"], true, forceRefresh); } } } diff --git a/gaseous-server/Controllers/SignaturesController.cs b/gaseous-server/Controllers/SignaturesController.cs index fd14de2..1e272d2 100644 --- a/gaseous-server/Controllers/SignaturesController.cs +++ b/gaseous-server/Controllers/SignaturesController.cs @@ -39,6 +39,19 @@ namespace gaseous_server.Controllers } } + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public List GetByTosecName(string TosecName = "") + { + if (TosecName.Length > 0) + { + return _GetSignature("signatures_roms.name = @searchstring", TosecName); + } else + { + return null; + } + } + private List _GetSignature(string sqlWhere, string searchString) { Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); diff --git a/gaseous-server/ProcessQueue.cs b/gaseous-server/ProcessQueue.cs index 4a9a781..4057ced 100644 --- a/gaseous-server/ProcessQueue.cs +++ b/gaseous-server/ProcessQueue.cs @@ -72,7 +72,7 @@ namespace gaseous_server case QueueItemType.MetadataRefresh: Logging.Log(Logging.LogType.Information, "Timered Event", "Starting Metadata Refresher"); - Classes.MetadataManagement.RefreshMetadata(); + Classes.MetadataManagement.RefreshMetadata(true); break; } } diff --git a/gaseous-tools/Database/MySQL/gaseous-1000.sql b/gaseous-tools/Database/MySQL/gaseous-1000.sql index 13b74ae..5e2b19f 100644 --- a/gaseous-tools/Database/MySQL/gaseous-1000.sql +++ b/gaseous-tools/Database/MySQL/gaseous-1000.sql @@ -1,13 +1,13 @@ --- MySQL dump 10.13 Distrib 8.0.32, for macos13.0 (arm64) +-- MySQL dump 10.13 Distrib 8.0.29, for macos12 (x86_64) -- -- Host: localhost Database: gaseous -- ------------------------------------------------------ --- Server version 8.0.32 +-- Server version 8.0.33 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!50503 SET NAMES utf8mb4 */; +/*!50503 SET NAMES utf8 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; @@ -15,11 +15,70 @@ /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; +-- +-- Table structure for table `agerating` +-- + +DROP TABLE IF EXISTS `agerating`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `agerating` ( + `id` bigint NOT NULL, + `category` int DEFAULT NULL, + `checksum` varchar(45) DEFAULT NULL, + `contentdescriptions` json DEFAULT NULL, + `rating` int DEFAULT NULL, + `ratingcoverurl` varchar(255) DEFAULT NULL, + `synopsis` longtext, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `ageratingcontentdescription` +-- + +DROP TABLE IF EXISTS `ageratingcontentdescription`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `ageratingcontentdescription` ( + `id` bigint NOT NULL, + `category` int DEFAULT NULL, + `checksum` varchar(45) DEFAULT NULL, + `description` varchar(255) DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `alternativename` +-- + +DROP TABLE IF EXISTS `alternativename`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `alternativename` ( + `id` bigint NOT NULL, + `checksum` varchar(45) DEFAULT NULL, + `comment` longtext, + `game` int DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `artwork` -- DROP TABLE IF EXISTS `artwork`; +/*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `artwork` ( `id` bigint NOT NULL, @@ -35,14 +94,36 @@ CREATE TABLE `artwork` ( `lastUpdated` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `collection` +-- + +DROP TABLE IF EXISTS `collection`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `collection` ( + `id` bigint NOT NULL, + `checksum` varchar(45) DEFAULT NULL, + `games` json DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `slug` varchar(100) DEFAULT NULL, + `createdAt` datetime DEFAULT NULL, + `updatedAt` datetime DEFAULT NULL, + `url` varchar(255) DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `cover` -- DROP TABLE IF EXISTS `cover`; - +/*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `cover` ( `id` bigint NOT NULL, @@ -58,14 +139,63 @@ CREATE TABLE `cover` ( `lastUpdated` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `externalgame` +-- + +DROP TABLE IF EXISTS `externalgame`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `externalgame` ( + `id` bigint NOT NULL, + `category` int DEFAULT NULL, + `checksum` varchar(45) DEFAULT NULL, + `createdat` datetime DEFAULT NULL, + `countries` json DEFAULT NULL, + `game` bigint DEFAULT NULL, + `media` int DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `platform` bigint DEFAULT NULL, + `uid` varchar(255) DEFAULT NULL, + `updatedat` datetime DEFAULT NULL, + `url` varchar(255) DEFAULT NULL, + `year` int DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `franchise` +-- + +DROP TABLE IF EXISTS `franchise`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `franchise` ( + `id` bigint NOT NULL, + `checksum` varchar(45) DEFAULT NULL, + `createdat` datetime DEFAULT NULL, + `updatedat` datetime DEFAULT NULL, + `games` json DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `slug` varchar(255) DEFAULT NULL, + `url` varchar(255) DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `game` -- DROP TABLE IF EXISTS `game`; - +/*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `game` ( `id` bigint NOT NULL, @@ -113,7 +243,7 @@ CREATE TABLE `game` ( `totalrating` double DEFAULT NULL, `totalratingcount` int DEFAULT NULL, `updatedat` datetime DEFAULT NULL, - `url` varchar(100) DEFAULT NULL, + `url` varchar(255) DEFAULT NULL, `versionparent` bigint DEFAULT NULL, `versiontitle` varchar(100) DEFAULT NULL, `videos` json DEFAULT NULL, @@ -121,16 +251,39 @@ CREATE TABLE `game` ( `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`) + UNIQUE KEY `id_UNIQUE` (`id`), + KEY `idx_genres` ((cast(`genres` as unsigned array))), + KEY `idx_alternativenames` ((cast(`alternativenames` as unsigned array))), + KEY `idx_artworks` ((cast(`artworks` as unsigned array))), + KEY `idx_bundles` ((cast(`bundles` as unsigned array))), + KEY `idx_dlcs` ((cast(`dlcs` as unsigned array))), + KEY `idx_expansions` ((cast(`expansions` as unsigned array))), + KEY `idx_externalgames` ((cast(`externalgames` as unsigned array))), + KEY `idx_franchises` ((cast(`franchises` as unsigned array))), + KEY `idx_gameengines` ((cast(`gameengines` as unsigned array))), + KEY `idx_gamemodes` ((cast(`gamemodes` as unsigned array))), + KEY `idx_involvedcompanies` ((cast(`involvedcompanies` as unsigned array))), + KEY `idx_keywords` ((cast(`keywords` as unsigned array))), + KEY `idx_multiplayermodes` ((cast(`multiplayermodes` as unsigned array))), + KEY `idx_platforms` ((cast(`platforms` as unsigned array))), + KEY `idx_playerperspectives` ((cast(`playerperspectives` as unsigned array))), + KEY `idx_releasedates` ((cast(`releasedates` as unsigned array))), + KEY `idx_screenshots` ((cast(`screenshots` as unsigned array))), + KEY `idx_similargames` ((cast(`similargames` as unsigned array))), + KEY `idx_standaloneexpansions` ((cast(`standaloneexpansions` as unsigned array))), + KEY `idx_tags` ((cast(`tags` as unsigned array))), + KEY `idx_themes` ((cast(`themes` as unsigned array))), + KEY `idx_videos` ((cast(`videos` as unsigned array))), + KEY `idx_websites` ((cast(`websites` as unsigned array))) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - +/*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `games_roms` -- DROP TABLE IF EXISTS `games_roms`; - +/*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `games_roms` ( `id` bigint NOT NULL AUTO_INCREMENT, @@ -149,20 +302,60 @@ CREATE TABLE `games_roms` ( `path` longtext, PRIMARY KEY (`id`), UNIQUE KEY `id_UNIQUE` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=398 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `gamevideo` +-- + +DROP TABLE IF EXISTS `gamevideo`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `gamevideo` ( + `id` bigint NOT NULL, + `checksum` varchar(45) DEFAULT NULL, + `game` bigint DEFAULT NULL, + `name` varchar(100) DEFAULT NULL, + `videoid` varchar(45) DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `genre` +-- + +DROP TABLE IF EXISTS `genre`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `genre` ( + `id` bigint NOT NULL, + `checksum` varchar(45) DEFAULT NULL, + `createdat` datetime DEFAULT NULL, + `updatedat` datetime DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `slug` varchar(100) DEFAULT NULL, + `url` varchar(255) DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `platform` -- DROP TABLE IF EXISTS `platform`; - +/*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `platform` ( `id` bigint NOT NULL, `abbreviation` varchar(45) DEFAULT NULL, - `alternativename` varchar(45) DEFAULT NULL, + `alternativename` varchar(255) DEFAULT NULL, `category` int DEFAULT NULL, `checksum` varchar(45) DEFAULT NULL, `createdat` datetime DEFAULT NULL, @@ -181,14 +374,14 @@ CREATE TABLE `platform` ( PRIMARY KEY (`id`), UNIQUE KEY `id_UNIQUE` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - +/*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `platformlogo` -- DROP TABLE IF EXISTS `platformlogo`; - +/*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `platformlogo` ( `id` bigint NOT NULL, @@ -203,14 +396,14 @@ CREATE TABLE `platformlogo` ( `lastUpdated` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - +/*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `platformversion` -- DROP TABLE IF EXISTS `platformversion`; - +/*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `platformversion` ( `id` bigint NOT NULL, @@ -237,14 +430,14 @@ CREATE TABLE `platformversion` ( `lastUpdated` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - +/*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `screenshot` -- DROP TABLE IF EXISTS `screenshot`; - +/*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `screenshot` ( `id` bigint NOT NULL, @@ -260,14 +453,14 @@ CREATE TABLE `screenshot` ( `lastUpdated` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - +/*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `settings` -- DROP TABLE IF EXISTS `settings`; - +/*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `settings` ( `setting` varchar(45) NOT NULL, @@ -275,14 +468,14 @@ CREATE TABLE `settings` ( PRIMARY KEY (`setting`), UNIQUE KEY `setting_UNIQUE` (`setting`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - +/*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `signatures_games` -- DROP TABLE IF EXISTS `signatures_games`; - +/*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `signatures_games` ( `id` int NOT NULL AUTO_INCREMENT, @@ -304,15 +497,15 @@ CREATE TABLE `signatures_games` ( KEY `ingest_idx` (`name`,`year`,`publisherid`,`systemid`,`country`,`language`) USING BTREE, CONSTRAINT `publisher` FOREIGN KEY (`publisherid`) REFERENCES `signatures_publishers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `system` FOREIGN KEY (`systemid`) REFERENCES `signatures_platforms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - +) ENGINE=InnoDB AUTO_INCREMENT=785672 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `signatures_platforms` -- DROP TABLE IF EXISTS `signatures_platforms`; - +/*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `signatures_platforms` ( `id` int NOT NULL AUTO_INCREMENT, @@ -320,15 +513,15 @@ CREATE TABLE `signatures_platforms` ( PRIMARY KEY (`id`), UNIQUE KEY `idsignatures_platforms_UNIQUE` (`id`), KEY `platforms_idx` (`platform`,`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - +) ENGINE=InnoDB AUTO_INCREMENT=417 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `signatures_publishers` -- DROP TABLE IF EXISTS `signatures_publishers`; - +/*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `signatures_publishers` ( `id` int NOT NULL AUTO_INCREMENT, @@ -336,15 +529,15 @@ CREATE TABLE `signatures_publishers` ( PRIMARY KEY (`id`), UNIQUE KEY `id_UNIQUE` (`id`), KEY `publisher_idx` (`publisher`,`id`) -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - +) ENGINE=InnoDB AUTO_INCREMENT=52259 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `signatures_roms` -- DROP TABLE IF EXISTS `signatures_roms`; - +/*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `signatures_roms` ( `id` int NOT NULL AUTO_INCREMENT, @@ -366,15 +559,15 @@ CREATE TABLE `signatures_roms` ( KEY `sha1_idx` (`sha1`) USING BTREE, KEY `flags_idx` ((cast(`flags` as char(255) array))), CONSTRAINT `gameid` FOREIGN KEY (`gameid`) REFERENCES `signatures_games` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - +) ENGINE=InnoDB AUTO_INCREMENT=1734101 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `signatures_sources` -- DROP TABLE IF EXISTS `signatures_sources`; - +/*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `signatures_sources` ( `id` int NOT NULL AUTO_INCREMENT, @@ -393,7 +586,16 @@ CREATE TABLE `signatures_sources` ( UNIQUE KEY `id_UNIQUE` (`id`), KEY `sourcemd5_idx` (`sourcemd5`,`id`) USING BTREE, KEY `sourcesha1_idx` (`sourcesha1`,`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=3109 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping events for database 'gaseous' +-- + +-- +-- Dumping routines for database 'gaseous' +-- -- -- Final view structure for view `view_signatures_games` From be6e12be19ef296d349d38ad2b20420453e6b5ea Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Tue, 16 May 2023 14:44:33 +1000 Subject: [PATCH 15/71] fix: sha1 hash generator now no longer always generates the same hash --- gaseous-tools/Common.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/gaseous-tools/Common.cs b/gaseous-tools/Common.cs index ef95b17..4421be3 100644 --- a/gaseous-tools/Common.cs +++ b/gaseous-tools/Common.cs @@ -41,6 +41,7 @@ namespace gaseous_tools _md5hash = md5Hash; var sha1 = SHA1.Create(); + xmlStream.Position = 0; byte[] sha1HashByte = sha1.ComputeHash(xmlStream); string sha1Hash = BitConverter.ToString(sha1HashByte).Replace("-", "").ToLowerInvariant(); _sha1hash = sha1Hash; From 42d5f9f6f2e7f77d47d9f58826f11151112f9b6f Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Tue, 16 May 2023 16:22:21 +1000 Subject: [PATCH 16/71] =?UTF-8?q?fix:=20remove=20string=20=E2=80=9Corigina?= =?UTF-8?q?l=E2=80=9D=20from=20downloaded=20image=20file=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gaseous-server/Classes/Metadata/Artworks.cs | 2 +- gaseous-server/Classes/Metadata/Covers.cs | 2 +- gaseous-server/Classes/Metadata/Screenshots.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gaseous-server/Classes/Metadata/Artworks.cs b/gaseous-server/Classes/Metadata/Artworks.cs index 4348941..af0e713 100644 --- a/gaseous-server/Classes/Metadata/Artworks.cs +++ b/gaseous-server/Classes/Metadata/Artworks.cs @@ -135,7 +135,7 @@ namespace gaseous_server.Classes.Metadata extension = "png"; break; case LogoSize.t_original: - fileName = "_Original"; + fileName = ""; extension = "png"; break; default: diff --git a/gaseous-server/Classes/Metadata/Covers.cs b/gaseous-server/Classes/Metadata/Covers.cs index a0276cc..e1f63a2 100644 --- a/gaseous-server/Classes/Metadata/Covers.cs +++ b/gaseous-server/Classes/Metadata/Covers.cs @@ -134,7 +134,7 @@ namespace gaseous_server.Classes.Metadata extension = "png"; break; case LogoSize.t_original: - fileName = "Cover_Original"; + fileName = "Cover"; extension = "png"; break; default: diff --git a/gaseous-server/Classes/Metadata/Screenshots.cs b/gaseous-server/Classes/Metadata/Screenshots.cs index 7d1ddc6..6165353 100644 --- a/gaseous-server/Classes/Metadata/Screenshots.cs +++ b/gaseous-server/Classes/Metadata/Screenshots.cs @@ -135,7 +135,7 @@ namespace gaseous_server.Classes.Metadata extension = "png"; break; case LogoSize.t_original: - fileName = "_Original"; + fileName = ""; extension = "png"; break; default: From 5ffc33472de994f357977d493fda087a4d26c891 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Tue, 16 May 2023 19:14:54 +1000 Subject: [PATCH 17/71] fix: improved title matching --- gaseous-server/Classes/ImportGames.cs | 31 ++++++++++++++++++------ gaseous-server/Models/PlatformMapping.cs | 23 ++++++++++++++++++ 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs index 5e69368..d05d39c 100644 --- a/gaseous-server/Classes/ImportGames.cs +++ b/gaseous-server/Classes/ImportGames.cs @@ -1,5 +1,6 @@ using System; using System.Data; +using System.Text.RegularExpressions; using System.Threading.Tasks; using gaseous_tools; using Org.BouncyCastle.Utilities.IO.Pem; @@ -133,6 +134,15 @@ namespace gaseous_server.Classes // game title is the file name without the extension or path gi.Name = Path.GetFileNameWithoutExtension(GameFileImportPath); + // remove everything after brackets - leaving (hopefully) only the name + if (gi.Name.Contains("(")) + { + gi.Name = gi.Name.Substring(0, gi.Name.IndexOf("(")); + } + + // remove special characters like dashes + gi.Name = gi.Name.Replace("-", ""); + // guess platform gaseous_server.Models.PlatformMapping.GetIGDBPlatformMapping(ref discoveredSignature, fi, true); @@ -152,16 +162,15 @@ namespace gaseous_server.Classes determinedPlatform = new IGDB.Models.Platform(); } - // remove string ending ", The" if present - if (discoveredSignature.Game.Name.Contains(", The")) - { - Logging.Log(Logging.LogType.Information, "Import Game", " Removing ', The' from end of game title for search"); - discoveredSignature.Game.Name.Replace(", The", ""); - } - // search discovered game - case insensitive exact match first IGDB.Models.Game determinedGame = new IGDB.Models.Game(); + // remove version numbers from name + discoveredSignature.Game.Name = Regex.Replace(discoveredSignature.Game.Name, @"v(\d+\.)?(\d+\.)?(\*|\d+)$", "").Trim(); + discoveredSignature.Game.Name = Regex.Replace(discoveredSignature.Game.Name, @"Rev (\d+\.)?(\d+\.)?(\*|\d+)$", "").Trim(); + + Logging.Log(Logging.LogType.Information, "Import Game", " Searching for title: " + discoveredSignature.Game.Name); + foreach (Metadata.Games.SearchType searchType in Enum.GetValues(typeof(Metadata.Games.SearchType))) { Logging.Log(Logging.LogType.Information, "Import Game", " Search type: " + searchType.ToString()); @@ -172,7 +181,13 @@ namespace gaseous_server.Classes determinedGame = Metadata.Games.GetGame((long)games[0].Id, false, false); Logging.Log(Logging.LogType.Information, "Import Game", " IGDB game: " + determinedGame.Name); break; - } + } else if (games.Length > 0) + { + Logging.Log(Logging.LogType.Information, "Import Game", " " + games.Length + " search results found"); + } else + { + Logging.Log(Logging.LogType.Information, "Import Game", " No search results found"); + } } if (determinedGame == null) { diff --git a/gaseous-server/Models/PlatformMapping.cs b/gaseous-server/Models/PlatformMapping.cs index a8c0798..f97dd7b 100644 --- a/gaseous-server/Models/PlatformMapping.cs +++ b/gaseous-server/Models/PlatformMapping.cs @@ -35,6 +35,7 @@ namespace gaseous_server.Models public static void GetIGDBPlatformMapping(ref Models.Signatures_Games Signature, FileInfo RomFileInfo, bool SetSystemName) { + bool PlatformFound = false; foreach (Models.PlatformMapping.PlatformMapItem PlatformMapping in Models.PlatformMapping.PlatformMap) { if (PlatformMapping.KnownFileExtensions.Contains(RomFileInfo.Extension, StringComparer.OrdinalIgnoreCase)) @@ -45,6 +46,28 @@ namespace gaseous_server.Models } Signature.Flags.IGDBPlatformId = PlatformMapping.IGDBId; Signature.Flags.IGDBPlatformName = PlatformMapping.IGDBName; + + PlatformFound = true; + break; + } + } + + if (PlatformFound == false) + { + foreach (Models.PlatformMapping.PlatformMapItem PlatformMapping in Models.PlatformMapping.PlatformMap) + { + if (PlatformMapping.AlternateNames.Contains(Signature.Game.System, StringComparer.OrdinalIgnoreCase)) + { + if (SetSystemName == true) + { + if (Signature.Game != null) { Signature.Game.System = PlatformMapping.IGDBName; } + } + Signature.Flags.IGDBPlatformId = PlatformMapping.IGDBId; + Signature.Flags.IGDBPlatformName = PlatformMapping.IGDBName; + + PlatformFound = true; + break; + } } } } From 95872ac8475ef1c1d7a5809b1990043c20eabbb9 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Fri, 19 May 2023 22:41:25 +1000 Subject: [PATCH 18/71] refactor: upgraded IGDB library --- gaseous-server/gaseous-server.csproj | 2 +- gaseous-tools/gaseous-tools.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gaseous-server/gaseous-server.csproj b/gaseous-server/gaseous-server.csproj index 8c80069..69b1685 100644 --- a/gaseous-server/gaseous-server.csproj +++ b/gaseous-server/gaseous-server.csproj @@ -13,7 +13,7 @@ - + diff --git a/gaseous-tools/gaseous-tools.csproj b/gaseous-tools/gaseous-tools.csproj index 400f6b9..b67489d 100644 --- a/gaseous-tools/gaseous-tools.csproj +++ b/gaseous-tools/gaseous-tools.csproj @@ -10,7 +10,7 @@ - + From ae49f2f47c92b7e35fb6ef29452a67e39b1c9259 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Fri, 19 May 2023 23:25:46 +1000 Subject: [PATCH 19/71] refactor: made the read and set settings functions more efficient --- gaseous-server/Program.cs | 3 ++ gaseous-tools/Config.cs | 69 ++++++++++++++++++++++++++++++--------- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index 3c5fa0d..c3ee292 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -8,6 +8,9 @@ Logging.Log(Logging.LogType.Information, "Startup", "Starting Gaseous Server"); Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); db.InitDB(); +// load app settings +Config.InitSettings(); + // set initial values Guid APIKey = Guid.NewGuid(); if (Config.ReadSetting("API Key", "Test API Key") == "Test API Key") diff --git a/gaseous-tools/Config.cs b/gaseous-tools/Config.cs index de72de4..05ac47f 100644 --- a/gaseous-tools/Config.cs +++ b/gaseous-tools/Config.cs @@ -138,32 +138,62 @@ namespace gaseous_tools File.WriteAllText(ConfigurationFilePath, configRaw); } - public static string ReadSetting(string SettingName, string DefaultValue) + private static Dictionary AppSettings = new Dictionary(); + + public static void InitSettings() { Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT * FROM settings WHERE setting = @settingname"; - Dictionary dbDict = new Dictionary(); - dbDict.Add("settingname", SettingName); - dbDict.Add("value", DefaultValue); + string sql = "SELECT * FROM settings"; - try + DataTable dbResponse = db.ExecuteCMD(sql); + foreach (DataRow dataRow in dbResponse.Rows) { - Logging.Log(Logging.LogType.Debug, "Database", "Reading setting '" + SettingName + "'"); - DataTable dbResponse = db.ExecuteCMD(sql, dbDict); - if (dbResponse.Rows.Count == 0) + if (AppSettings.ContainsKey((string)dataRow["setting"])) { - // no value with that name stored - respond with the default value - return DefaultValue; + AppSettings[(string)dataRow["setting"]] = (string)dataRow["value"]; } else { - return (string)dbResponse.Rows[0][0]; + AppSettings.Add((string)dataRow["setting"], (string)dataRow["value"]); } } - catch (Exception ex) + } + + public static string ReadSetting(string SettingName, string DefaultValue) + { + if (AppSettings.ContainsKey(SettingName)) { - Logging.Log(Logging.LogType.Critical, "Database", "Failed reading setting " + SettingName, ex); - throw; + return AppSettings[SettingName]; + } + else + { + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "SELECT * FROM settings WHERE setting = @settingname"; + Dictionary dbDict = new Dictionary(); + dbDict.Add("settingname", SettingName); + dbDict.Add("value", DefaultValue); + + try + { + Logging.Log(Logging.LogType.Debug, "Database", "Reading setting '" + SettingName + "'"); + DataTable dbResponse = db.ExecuteCMD(sql, dbDict); + if (dbResponse.Rows.Count == 0) + { + // no value with that name stored - respond with the default value + SetSetting(SettingName, DefaultValue); + return DefaultValue; + } + else + { + AppSettings.Add(SettingName, (string)dbResponse.Rows[0][0]); + return (string)dbResponse.Rows[0][0]; + } + } + catch (Exception ex) + { + Logging.Log(Logging.LogType.Critical, "Database", "Failed reading setting " + SettingName, ex); + throw; + } } } @@ -179,6 +209,15 @@ namespace gaseous_tools try { db.ExecuteCMD(sql, dbDict); + + if (AppSettings.ContainsKey(SettingName)) + { + AppSettings[SettingName] = Value; + } + else + { + AppSettings.Add(SettingName, Value); + } } catch (Exception ex) { From a6b0c85ad0b4d98cd7c26d2ced1e3c34a628d0fa Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Fri, 26 May 2023 23:25:38 +1000 Subject: [PATCH 20/71] refactor: added an array of files to skip during import --- gaseous-server/Classes/ImportGames.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs index d05d39c..e5cea16 100644 --- a/gaseous-server/Classes/ImportGames.cs +++ b/gaseous-server/Classes/ImportGames.cs @@ -42,9 +42,13 @@ namespace gaseous_server.Classes string sql = ""; Dictionary dbDict = new Dictionary(); - if (String.Equals(Path.GetFileName(GameFileImportPath),".DS_STORE", StringComparison.OrdinalIgnoreCase)) + string[] SkippableFiles = { + ".DS_STORE", + "desktop.ini" + }; + if (SkippableFiles.Contains(Path.GetFileName(GameFileImportPath), StringComparer.OrdinalIgnoreCase)) { - Logging.Log(Logging.LogType.Information, "Import Game", "Skipping item " + GameFileImportPath); + Logging.Log(Logging.LogType.Debug, "Import Game", "Skipping item " + GameFileImportPath); } else { @@ -63,7 +67,7 @@ namespace gaseous_server.Classes { if (!GameFileImportPath.StartsWith(Config.LibraryConfiguration.LibraryImportDirectory)) { - Logging.Log(Logging.LogType.Information, "Import Game", " " + GameFileImportPath + " already in database - skipping"); + Logging.Log(Logging.LogType.Warning, "Import Game", " " + GameFileImportPath + " already in database - skipping"); } } else From 0e70c9f9999999ae6e6977471903eb0272a97372 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Mon, 29 May 2023 22:43:59 +1000 Subject: [PATCH 21/71] feat: basic game query api support --- gaseous-server/Classes/Metadata/Storage.cs | 78 ++++++++++++- gaseous-server/Controllers/GamesController.cs | 104 ++++++++++++++++++ 2 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 gaseous-server/Controllers/GamesController.cs diff --git a/gaseous-server/Classes/Metadata/Storage.cs b/gaseous-server/Classes/Metadata/Storage.cs index decba18..15eb9f1 100644 --- a/gaseous-server/Classes/Metadata/Storage.cs +++ b/gaseous-server/Classes/Metadata/Storage.cs @@ -187,18 +187,30 @@ namespace gaseous_server.Classes.Metadata switch (subObjectTypeName) { - case "platformfamily": - objectToStore = new IdentityOrValue(id: (long)(int)dataRow[property.Name]); + case "collection": + objectToStore = new IdentityOrValue(id: (long)dataRow[property.Name]); + break; + case "cover": + objectToStore = new IdentityOrValue(id: (long)dataRow[property.Name]); + break; + case "franchise": + objectToStore = new IdentityOrValue(id: (long)dataRow[property.Name]); + break; + case "game": + objectToStore = new IdentityOrValue(id: (long)dataRow[property.Name]); + break; + case "platformfamily": + objectToStore = new IdentityOrValue(id: (long)dataRow[property.Name]); break; case "platformlogo": - objectToStore = new IdentityOrValue(id: (long)(int)dataRow[property.Name]); + objectToStore = new IdentityOrValue(id: (long)dataRow[property.Name]); break; case "platformversioncompany": - objectToStore = new IdentityOrValue(id: (long)(int)dataRow[property.Name]); + objectToStore = new IdentityOrValue(id: (long)dataRow[property.Name]); break; } - if (objectToStore != null) + if (objectToStore != null) { property.SetValue(EndpointType, objectToStore); } @@ -211,7 +223,46 @@ namespace gaseous_server.Classes.Metadata switch (subObjectTypeName) { - case "platformversion": + case "agerating": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "alternativename": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "artworks": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "game": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "externalgame": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "franchise": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "gameengine": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "gamemode": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "gamevideo": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "genre": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "involvedcompany": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "multiplayermode": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "platform": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "platformversion": objectToStore = new IdentitiesOrValues(ids: fromJsonObject); break; case "platformwebsite": @@ -223,6 +274,21 @@ namespace gaseous_server.Classes.Metadata case "platformversionreleasedate": objectToStore = new IdentitiesOrValues(ids: fromJsonObject); break; + case "playerperspective": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "releasedate": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "screenshot": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "theme": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "website": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; } if (objectToStore != null) diff --git a/gaseous-server/Controllers/GamesController.cs b/gaseous-server/Controllers/GamesController.cs new file mode 100644 index 0000000..f80810e --- /dev/null +++ b/gaseous-server/Controllers/GamesController.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using gaseous_tools; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace gaseous_server.Controllers +{ + [Route("api/v1/[controller]")] + [ApiController] + public class GamesController : ControllerBase + { + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public List Game(string name = "", string platform = "") + { + string whereClause = ""; + string havingClause = ""; + Dictionary whereParams = new Dictionary(); + + List whereClauses = new List(); + List havingClauses = new List(); + + string tempVal = ""; + + if (name.Length > 0) + { + tempVal = "`name` LIKE @name"; + whereParams.Add("@name", "%" + name + "%"); + havingClauses.Add(tempVal); + } + + if (platform.Length > 0) + { + tempVal = "games_roms.platformid IN ("; + string[] platformClauseItems = platform.Split(","); + for (int i = 0; i < platformClauseItems.Length; i++) + { + if (i > 0) + { + tempVal += ", "; + } + string platformLabel = "@platform" + i; + tempVal += platformLabel; + whereParams.Add(platformLabel, platformClauseItems[i]); + } + tempVal += ")"; + whereClauses.Add(tempVal); + } + + // build where clause + if (whereClauses.Count > 0) + { + whereClause = "WHERE "; + for (int i = 0; i < whereClauses.Count; i++) + { + if (i > 0) + { + whereClause += ", "; + } + whereClause += whereClauses[i]; + } + } + + // build having clause + if (havingClauses.Count > 0) + { + havingClause = "HAVING "; + for (int i = 0; i < havingClauses.Count; i++) + { + if (i > 0) + { + havingClause += ", "; + } + havingClause += havingClauses[i]; + } + } + + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "SELECT DISTINCT games_roms.gameid AS ROMGameId, game.ageratings, game.aggregatedrating, game.aggregatedratingcount, game.alternativenames, game.artworks, game.bundles, game.category, game.collection, game.cover, game.dlcs, game.expansions, game.externalgames, game.firstreleasedate, game.`follows`, game.franchise, game.franchises, game.gameengines, game.gamemodes, game.genres, game.hypes, game.involvedcompanies, game.keywords, game.multiplayermodes, (CASE WHEN games_roms.gameid = 0 THEN games_roms.`name` ELSE game.`name` END) AS `name`, game.parentgame, game.platforms, game.playerperspectives, game.rating, game.ratingcount, game.releasedates, game.screenshots, game.similargames, game.slug, game.standaloneexpansions, game.`status`, game.storyline, game.summary, game.tags, game.themes, game.totalrating, game.totalratingcount, game.versionparent, game.versiontitle, game.videos, game.websites FROM gaseous.games_roms LEFT JOIN game ON game.id = games_roms.gameid " + whereClause + " " + havingClause + " ORDER BY `name`"; + + List RetVal = new List(); + + DataTable dbResponse = db.ExecuteCMD(sql, whereParams); + foreach (DataRow dr in dbResponse.Rows) + { + if ((long)dr["ROMGameId"] == 0) + { + // unknown game + } + else + { + // known game + RetVal.Add(Classes.Metadata.Games.GetGame((long)dr["ROMGameId"], false, false)); + } + } + + return RetVal; + } + } +} From 76673c42b9281a41d6aa401dc7e5ec28c2ca1861 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Mon, 12 Jun 2023 10:09:16 +1000 Subject: [PATCH 22/71] =?UTF-8?q?feat:=20Started=20Game=20controller=20-?= =?UTF-8?q?=20needs=20more=20error=20handling=20and=20handling=20for=20404?= =?UTF-8?q?=E2=80=99s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gaseous-server/Classes/Metadata/Storage.cs | 2 +- .../Controllers/FilterController.cs | 50 +++ gaseous-server/Controllers/GamesController.cs | 325 +++++++++++++++++- gaseous-server/Program.cs | 3 + gaseous-tools/Database/MySQL/gaseous-1000.sql | 4 +- 5 files changed, 375 insertions(+), 9 deletions(-) create mode 100644 gaseous-server/Controllers/FilterController.cs diff --git a/gaseous-server/Classes/Metadata/Storage.cs b/gaseous-server/Classes/Metadata/Storage.cs index 15eb9f1..792b2ec 100644 --- a/gaseous-server/Classes/Metadata/Storage.cs +++ b/gaseous-server/Classes/Metadata/Storage.cs @@ -229,7 +229,7 @@ namespace gaseous_server.Classes.Metadata case "alternativename": objectToStore = new IdentitiesOrValues(ids: fromJsonObject); break; - case "artworks": + case "artwork": objectToStore = new IdentitiesOrValues(ids: fromJsonObject); break; case "game": diff --git a/gaseous-server/Controllers/FilterController.cs b/gaseous-server/Controllers/FilterController.cs new file mode 100644 index 0000000..8247192 --- /dev/null +++ b/gaseous-server/Controllers/FilterController.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using gaseous_tools; +using IGDB.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace gaseous_server.Controllers +{ + [Route("api/v1/[controller]")] + [ApiController] + public class FilterController : ControllerBase + { + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public Dictionary Filter() + { + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + + Dictionary FilterSet = new Dictionary(); + + // platforms + List platforms = new List(); + string sql = "SELECT platform.id, platform.abbreviation, platform.alternativename, platform.`name`, platform.platformlogo, (SELECT COUNT(games_roms.id) AS RomCount FROM games_roms WHERE games_roms.platformid = platform.id) AS RomCount FROM platform HAVING RomCount > 0 ORDER BY `name`"; + DataTable dbResponse = db.ExecuteCMD(sql); + + foreach (DataRow dr in dbResponse.Rows) + { + platforms.Add(Classes.Metadata.Platforms.GetPlatform((long)dr["id"])); + } + FilterSet.Add("platforms", platforms); + + // genres + List genres = new List(); + sql = "SELECT DISTINCT t1.id, t1.`name` FROM genre AS t1 JOIN (SELECT * FROM game WHERE (SELECT COUNT(id) FROM games_roms WHERE gameid = game.id) > 0) AS t2 ON JSON_CONTAINS(t2.genres, CAST(t1.id AS char), '$') ORDER BY t1.`name`"; + dbResponse = db.ExecuteCMD(sql); + + foreach (DataRow dr in dbResponse.Rows) + { + genres.Add(Classes.Metadata.Genres.GetGenres((long)dr["id"])); + } + FilterSet.Add("genres", genres); + + return FilterSet; + } + } +} \ No newline at end of file diff --git a/gaseous-server/Controllers/GamesController.cs b/gaseous-server/Controllers/GamesController.cs index f80810e..04ee22a 100644 --- a/gaseous-server/Controllers/GamesController.cs +++ b/gaseous-server/Controllers/GamesController.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Data; using System.Linq; using System.Threading.Tasks; +using gaseous_server.Classes.Metadata; using gaseous_tools; +using IGDB.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -14,8 +16,8 @@ namespace gaseous_server.Controllers public class GamesController : ControllerBase { [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public List Game(string name = "", string platform = "") + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + public ActionResult Game(string name = "", string platform = "", string genre = "", bool sortdescending = false) { string whereClause = ""; string havingClause = ""; @@ -51,6 +53,24 @@ namespace gaseous_server.Controllers whereClauses.Add(tempVal); } + if (genre.Length > 0) + { + tempVal = "("; + string[] genreClauseItems = genre.Split(","); + for (int i = 0; i < genreClauseItems.Length; i++) + { + if (i > 0) + { + tempVal += " AND "; + } + string genreLabel = "@genre" + i; + tempVal += "JSON_CONTAINS(game.genres, " + genreLabel + ", '$')"; + whereParams.Add(genreLabel, genreClauseItems[i]); + } + tempVal += ")"; + whereClauses.Add(tempVal); + } + // build where clause if (whereClauses.Count > 0) { @@ -59,7 +79,7 @@ namespace gaseous_server.Controllers { if (i > 0) { - whereClause += ", "; + whereClause += " AND "; } whereClause += whereClauses[i]; } @@ -73,14 +93,21 @@ namespace gaseous_server.Controllers { if (i > 0) { - havingClause += ", "; + havingClause += " AND "; } havingClause += havingClauses[i]; } } + // order by clause + string orderByClause = "ORDER BY `name` ASC"; + if (sortdescending == true) + { + orderByClause = "ORDER BY `name` DESC"; + } + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT DISTINCT games_roms.gameid AS ROMGameId, game.ageratings, game.aggregatedrating, game.aggregatedratingcount, game.alternativenames, game.artworks, game.bundles, game.category, game.collection, game.cover, game.dlcs, game.expansions, game.externalgames, game.firstreleasedate, game.`follows`, game.franchise, game.franchises, game.gameengines, game.gamemodes, game.genres, game.hypes, game.involvedcompanies, game.keywords, game.multiplayermodes, (CASE WHEN games_roms.gameid = 0 THEN games_roms.`name` ELSE game.`name` END) AS `name`, game.parentgame, game.platforms, game.playerperspectives, game.rating, game.ratingcount, game.releasedates, game.screenshots, game.similargames, game.slug, game.standaloneexpansions, game.`status`, game.storyline, game.summary, game.tags, game.themes, game.totalrating, game.totalratingcount, game.versionparent, game.versiontitle, game.videos, game.websites FROM gaseous.games_roms LEFT JOIN game ON game.id = games_roms.gameid " + whereClause + " " + havingClause + " ORDER BY `name`"; + string sql = "SELECT DISTINCT games_roms.gameid AS ROMGameId, game.ageratings, game.aggregatedrating, game.aggregatedratingcount, game.alternativenames, game.artworks, game.bundles, game.category, game.collection, game.cover, game.dlcs, game.expansions, game.externalgames, game.firstreleasedate, game.`follows`, game.franchise, game.franchises, game.gameengines, game.gamemodes, game.genres, game.hypes, game.involvedcompanies, game.keywords, game.multiplayermodes, (CASE WHEN games_roms.gameid = 0 THEN games_roms.`name` ELSE game.`name` END) AS `name`, game.parentgame, game.platforms, game.playerperspectives, game.rating, game.ratingcount, game.releasedates, game.screenshots, game.similargames, game.slug, game.standaloneexpansions, game.`status`, game.storyline, game.summary, game.tags, game.themes, game.totalrating, game.totalratingcount, game.versionparent, game.versiontitle, game.videos, game.websites FROM gaseous.games_roms LEFT JOIN game ON game.id = games_roms.gameid " + whereClause + " " + havingClause + " " + orderByClause; List RetVal = new List(); @@ -98,7 +125,293 @@ namespace gaseous_server.Controllers } } - return RetVal; + return Ok(RetVal); + } + + [HttpGet] + [Route("{GameId}")] + [ProducesResponseType(typeof(Game), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult Game(long GameId) + { + try + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + if (gameObject != null) + { + return Ok(gameObject); + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + + [HttpGet] + [Route("{GameId}/artwork")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + public ActionResult GameArtwork(long GameId) + { + Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + List artworks = new List(); + if (gameObject.Artworks != null) + { + foreach (long ArtworkId in gameObject.Artworks.Ids) + { + Artwork GameArtwork = Artworks.GetArtwork(ArtworkId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); + artworks.Add(GameArtwork); + } + } + + return Ok(artworks); + } + + [HttpGet] + [Route("{GameId}/artwork/{ArtworkId}")] + [ProducesResponseType(typeof(Artwork), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameArtwork(long GameId, long ArtworkId) + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + try + { + IGDB.Models.Artwork artworkObject = Artworks.GetArtwork(ArtworkId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); + if (artworkObject != null) + { + return Ok(artworkObject); + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + + } + + [HttpGet] + [Route("{GameId}/artwork/{ArtworkId}/image")] + [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameCoverImage(long GameId, long ArtworkId) + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + try + { + IGDB.Models.Artwork artworkObject = Artworks.GetArtwork(ArtworkId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); + if (artworkObject != null) { + string coverFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), "Artwork", artworkObject.ImageId + ".png"); + if (System.IO.File.Exists(coverFilePath)) + { + string filename = artworkObject.ImageId + ".png"; + string filepath = coverFilePath; + byte[] filedata = System.IO.File.ReadAllBytes(filepath); + string contentType = "image/png"; + + var cd = new System.Net.Mime.ContentDisposition + { + FileName = filename, + Inline = true, + }; + + Response.Headers.Add("Content-Disposition", cd.ToString()); + + return File(filedata, contentType); + } + else + { + return NotFound(); + } + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + + [HttpGet] + [Route("{GameId}/cover")] + [ProducesResponseType(typeof(Cover), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameCover(long GameId) + { + try + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + if (gameObject != null) + { + IGDB.Models.Cover coverObject = Covers.GetCover(gameObject.Cover.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); + if (coverObject != null) + { + return Ok(coverObject); + } + else + { + return NotFound(); + } + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + + [HttpGet] + [Route("{GameId}/cover/image")] + [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameCoverImage(long GameId) + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + string coverFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), "Cover.png"); + if (System.IO.File.Exists(coverFilePath)) { + string filename = "Cover.png"; + string filepath = coverFilePath; + byte[] filedata = System.IO.File.ReadAllBytes(filepath); + string contentType = "image/png"; + + var cd = new System.Net.Mime.ContentDisposition + { + FileName = filename, + Inline = true, + }; + + Response.Headers.Add("Content-Disposition", cd.ToString()); + + return File(filedata, contentType); + } + else + { + return NotFound(); + } + } + + [HttpGet] + [Route("{GameId}/screenshots")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + public ActionResult GameScreenshot(long GameId) + { + Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + List screenshots = new List(); + if (gameObject.Screenshots != null) + { + foreach (long ScreenshotId in gameObject.Screenshots.Ids) + { + Screenshot GameScreenshot = Screenshots.GetScreenshot(ScreenshotId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); + screenshots.Add(GameScreenshot); + } + } + + return Ok(screenshots); + } + + [HttpGet] + [Route("{GameId}/screenshots/{ScreenshotId}")] + [ProducesResponseType(typeof(Screenshot), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameScreenshot(long GameId, long ScreenshotId) + { + try + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + if (gameObject != null) { + IGDB.Models.Screenshot screenshotObject = Screenshots.GetScreenshot(ScreenshotId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); + if (screenshotObject != null) + { + return Ok(screenshotObject); + } + else + { + return NotFound(); + } + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + + [HttpGet] + [Route("{GameId}/screenshots/{ScreenshotId}/image")] + [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameScreenshotImage(long GameId, long ScreenshotId) + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + IGDB.Models.Screenshot screenshotObject = Screenshots.GetScreenshot(ScreenshotId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); + + string coverFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), "Screenshots", screenshotObject.ImageId + ".png"); + if (System.IO.File.Exists(coverFilePath)) + { + string filename = screenshotObject.ImageId + ".png"; + string filepath = coverFilePath; + byte[] filedata = System.IO.File.ReadAllBytes(filepath); + string contentType = "image/png"; + + var cd = new System.Net.Mime.ContentDisposition + { + FileName = filename, + Inline = true, + }; + + Response.Headers.Add("Content-Disposition", cd.ToString()); + + return File(filedata, contentType); + } + else + { + return NotFound(); + } + } + + [HttpGet] + [Route("{GameId}/videos")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + public ActionResult GameVideo(long GameId) + { + Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + List videos = new List(); + if (gameObject.Videos != null) + { + foreach (long VideoId in gameObject.Videos.Ids) + { + GameVideo gameVideo = GamesVideos.GetGame_Videos(VideoId); + videos.Add(gameVideo); + } + } + + return Ok(videos); } } } diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index c3ee292..00eea39 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -28,6 +28,9 @@ builder.Services.AddControllers().AddJsonOptions(x => { // serialize enums as strings in api responses (e.g. Role) x.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + + // suppress nulls + x.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle diff --git a/gaseous-tools/Database/MySQL/gaseous-1000.sql b/gaseous-tools/Database/MySQL/gaseous-1000.sql index 5e2b19f..457cd2d 100644 --- a/gaseous-tools/Database/MySQL/gaseous-1000.sql +++ b/gaseous-tools/Database/MySQL/gaseous-1000.sql @@ -361,8 +361,8 @@ CREATE TABLE `platform` ( `createdat` datetime DEFAULT NULL, `generation` int DEFAULT NULL, `name` varchar(45) DEFAULT NULL, - `platformfamily` int DEFAULT NULL, - `platformlogo` int DEFAULT NULL, + `platformfamily` bigint DEFAULT NULL, + `platformlogo` bigint DEFAULT NULL, `slug` varchar(45) DEFAULT NULL, `summary` longtext, `updatedat` datetime DEFAULT NULL, From 800eea4f45f3d041a11b32fe5978506a44c65847 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Thu, 15 Jun 2023 23:39:21 +1000 Subject: [PATCH 23/71] feat: added age ratings and icons to the game controller --- gaseous-server/Assets/Ratings/ACB/ACB_G.png | Bin 0 -> 3441 bytes gaseous-server/Assets/Ratings/ACB/ACB_M.png | Bin 0 -> 3827 bytes .../Assets/Ratings/ACB/ACB_MA15.png | Bin 0 -> 5263 bytes gaseous-server/Assets/Ratings/ACB/ACB_PG.png | Bin 0 -> 3195 bytes gaseous-server/Assets/Ratings/ACB/ACB_R18.png | Bin 0 -> 4738 bytes gaseous-server/Assets/Ratings/ACB/ACB_RC.png | Bin 0 -> 2930 bytes gaseous-server/Assets/Ratings/ESRB/AO.svg | 1 + gaseous-server/Assets/Ratings/ESRB/E.svg | 1 + gaseous-server/Assets/Ratings/ESRB/E10.svg | 1 + gaseous-server/Assets/Ratings/ESRB/M.svg | 1 + .../Assets/Ratings/ESRB/RP-LM17-English.svg | 1 + gaseous-server/Assets/Ratings/ESRB/RP.svg | 1 + gaseous-server/Assets/Ratings/ESRB/T.svg | 1 + .../Assets/Ratings/PEGI/Eighteen.jpg | Bin 0 -> 50956 bytes .../PEGI_Parental_Guidance_Recommended.png | Bin 0 -> 16222 bytes gaseous-server/Assets/Ratings/PEGI/Seven.jpg | Bin 0 -> 57204 bytes .../Assets/Ratings/PEGI/Sixteen.jpg | Bin 0 -> 61446 bytes gaseous-server/Assets/Ratings/PEGI/Three.jpg | Bin 0 -> 64687 bytes gaseous-server/Assets/Ratings/PEGI/Twelve.jpg | Bin 0 -> 67077 bytes gaseous-server/Classes/Metadata/AgeRating.cs | 32 ++ gaseous-server/Classes/Metadata/Storage.cs | 6 + gaseous-server/Controllers/GamesController.cs | 345 +++++++++++++----- gaseous-server/gaseous-server.csproj | 48 +++ 23 files changed, 339 insertions(+), 99 deletions(-) create mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_G.png create mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_M.png create mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_MA15.png create mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_PG.png create mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_R18.png create mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_RC.png create mode 100644 gaseous-server/Assets/Ratings/ESRB/AO.svg create mode 100644 gaseous-server/Assets/Ratings/ESRB/E.svg create mode 100644 gaseous-server/Assets/Ratings/ESRB/E10.svg create mode 100644 gaseous-server/Assets/Ratings/ESRB/M.svg create mode 100644 gaseous-server/Assets/Ratings/ESRB/RP-LM17-English.svg create mode 100644 gaseous-server/Assets/Ratings/ESRB/RP.svg create mode 100644 gaseous-server/Assets/Ratings/ESRB/T.svg create mode 100644 gaseous-server/Assets/Ratings/PEGI/Eighteen.jpg create mode 100644 gaseous-server/Assets/Ratings/PEGI/PEGI_Parental_Guidance_Recommended.png create mode 100644 gaseous-server/Assets/Ratings/PEGI/Seven.jpg create mode 100644 gaseous-server/Assets/Ratings/PEGI/Sixteen.jpg create mode 100644 gaseous-server/Assets/Ratings/PEGI/Three.jpg create mode 100644 gaseous-server/Assets/Ratings/PEGI/Twelve.jpg diff --git a/gaseous-server/Assets/Ratings/ACB/ACB_G.png b/gaseous-server/Assets/Ratings/ACB/ACB_G.png new file mode 100644 index 0000000000000000000000000000000000000000..93b5e5fa9bfeb7620b4bd5ab70b72ee086ec2bdf GIT binary patch literal 3441 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 C=w~+o literal 0 HcmV?d00001 diff --git a/gaseous-server/Assets/Ratings/ACB/ACB_M.png b/gaseous-server/Assets/Ratings/ACB/ACB_M.png new file mode 100644 index 0000000000000000000000000000000000000000..7bdaa1d075fde6b97bc285c3f497cd9843fbe425 GIT binary patch literal 3827 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 N>1i8jRcSg!{|^%jBS!!L literal 0 HcmV?d00001 diff --git a/gaseous-server/Assets/Ratings/ACB/ACB_PG.png b/gaseous-server/Assets/Ratings/ACB/ACB_PG.png new file mode 100644 index 0000000000000000000000000000000000000000..c505b3fc7cd26643bca7927ec94e49bcccffa6b7 GIT binary patch literal 3195 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 literal 0 HcmV?d00001 diff --git a/gaseous-server/Assets/Ratings/ACB/ACB_R18.png b/gaseous-server/Assets/Ratings/ACB/ACB_R18.png new file mode 100644 index 0000000000000000000000000000000000000000..62edcae4f2eddde46607e97839bec8f55bc200d2 GIT binary patch literal 4738 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 R{7)r-hKi2z7e$-!{{bc!E(8Do literal 0 HcmV?d00001 diff --git a/gaseous-server/Assets/Ratings/ACB/ACB_RC.png b/gaseous-server/Assets/Ratings/ACB/ACB_RC.png new file mode 100644 index 0000000000000000000000000000000000000000..1b0f511bbb516e90abe7cba680e33b7448e2d14b GIT binary patch literal 2930 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 LzhQ=j`k?;-DQ%5j literal 0 HcmV?d00001 diff --git a/gaseous-server/Assets/Ratings/ESRB/AO.svg b/gaseous-server/Assets/Ratings/ESRB/AO.svg new file mode 100644 index 0000000..c88be19 --- /dev/null +++ b/gaseous-server/Assets/Ratings/ESRB/AO.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gaseous-server/Assets/Ratings/ESRB/E.svg b/gaseous-server/Assets/Ratings/ESRB/E.svg new file mode 100644 index 0000000..e6f7695 --- /dev/null +++ b/gaseous-server/Assets/Ratings/ESRB/E.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gaseous-server/Assets/Ratings/ESRB/E10.svg b/gaseous-server/Assets/Ratings/ESRB/E10.svg new file mode 100644 index 0000000..664135f --- /dev/null +++ b/gaseous-server/Assets/Ratings/ESRB/E10.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gaseous-server/Assets/Ratings/ESRB/M.svg b/gaseous-server/Assets/Ratings/ESRB/M.svg new file mode 100644 index 0000000..3ae12f7 --- /dev/null +++ b/gaseous-server/Assets/Ratings/ESRB/M.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gaseous-server/Assets/Ratings/ESRB/RP-LM17-English.svg b/gaseous-server/Assets/Ratings/ESRB/RP-LM17-English.svg new file mode 100644 index 0000000..39deca5 --- /dev/null +++ b/gaseous-server/Assets/Ratings/ESRB/RP-LM17-English.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gaseous-server/Assets/Ratings/ESRB/RP.svg b/gaseous-server/Assets/Ratings/ESRB/RP.svg new file mode 100644 index 0000000..1cd0cc7 --- /dev/null +++ b/gaseous-server/Assets/Ratings/ESRB/RP.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gaseous-server/Assets/Ratings/ESRB/T.svg b/gaseous-server/Assets/Ratings/ESRB/T.svg new file mode 100644 index 0000000..31039e6 --- /dev/null +++ b/gaseous-server/Assets/Ratings/ESRB/T.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gaseous-server/Assets/Ratings/PEGI/Eighteen.jpg b/gaseous-server/Assets/Ratings/PEGI/Eighteen.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c4fc8f24aeb87ce7703eb34ce53acabd24ca10e8 GIT binary patch literal 50956 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 Rzg~Km6qOUH7Sa#+zW{mkeJub0 literal 0 HcmV?d00001 diff --git a/gaseous-server/Assets/Ratings/PEGI/Seven.jpg b/gaseous-server/Assets/Ratings/PEGI/Seven.jpg new file mode 100644 index 0000000000000000000000000000000000000000..264cac2765f25ad708b9e5a2aedf01ee62438095 GIT binary patch literal 57204 zcmeEv2|QHa`~MwlN+e60EtHaNY=dHwEhJ@Mvy8E18T%SjX;%qJQKSu(6taePMUg#A zi0o_D%$S+~nX#t&w0ypw&-e9v{lB;7IQOjY^PJ~A=Q+>4_Z-rDQVX;~Q%yq+f>FVs z3*a9_`nb|c)z8rmg0!_EAqaw&K=iPc5H;|@z&{AK7NVv2A;<={ZqC0SwrM5}6-Wbz zfD3H`KM$A$#m@%(F~=Ptnwh-!!QV!Rqs8Zf&9@0Av9L{mJ~P;1@>FwWvv5xr2kiuZ0d=PTk`~gH90p=Z z{SYIieU{4F5R5bcF;VUy+k@nsqz32+#JFV15{4y=3=E7c%a$%ejoN4(ACZ-i^OiWB{WQU1uIt$CM6hL|mu`GeELLt;JK8T71 zM$G~veE`j7AU%g60ZLjJ$atY!=OFwTf#5s@Qa!{-O{tRw zLPD=_>0|acy>~~AKPoh<9=n@5>f`KMOVzXxHJ^HC4Tp_yhy+z=1ql?4#ZUE-pqEB$dC}BgMQUK* zVDFHi2`6F`4^9JfBf2S_1YL>$QrsHt(ehQC*ksrkme`MNct(OWL`Ea#^6#<;FmS(S9Y2)3FY`QR-O}l&alxjG=je{J zJ&b<1hIz{ON04K zxLI&t@j3}=#gd>M_okvm#>z%4%i4)k7dMRXNDD-0(g?gnIlsP@)l(wuWG?&e;+E@# zLzPJP=1$P_0y%wR-Pz5b0fnk*>qL;pZHi=fm9j*!j4*B7WwpXtuKB*a{lM0#2@>=v zVnMbHU892Dkf|!`IAUc_IMD0^jeth{Coe)*@sq02V4>1Q*j=|mhMLmD8vV1 zhc)KCTC{L+hM{UL;<*mw0cda7OMwmMTi-J$Zk>DC6J#(uFhS< zzQ^omT_2?Izb?E#=r@$0Cz_ZhjJ_5j?mbkWb<28s`wo$Z5D{o&{Qe9 zQ~T<6T#Z5sk^wNuJ7rTYx>UVn=?}M2@gvCVJY)NJiMPF85w_otG%PQ?QOM^AzhuoI&|fX(=#chzA@mA~B@vv|Y( zm3*yoB6oQXHVsaFsXmtLdT3v1Op))yQh)q7Gfvn@k%wE`nmA{7e`5KEDZPzUG=75S zwXqf&U~uYQL6#X2tS9x^X1F>0eey6C4E0syp-vw26pz>2Xj0YhHb;vUieGMoI%Ih> zB+OjN#f83&yS?aG^tmS`uP~EE`}=F6pSJb97JbSO*t*;VK4xJ@uu8{hdtxgT&P@4& z*@PqFCGO3aYo|z1z|+AIQ!qzhjdF6_h9{Z*fY1-?wh%j?4f?b5y?%s-z|G(4o9f?f zjK+Plysc{rWbPA+u!J!mt;nwa57qFZL+<#CV>Ke!qhZx=CZdAE0SjJo@6*2@oW6DR z;@cq(o~dz;;m+W+_?m8R_V^dgeY#WIYemFZ!9)~aY<@mM!}@+QnUD1nC&9dV7b$dj zrbBeMvMcHknSo3tL4(~J=6s@;(nt`g=4dyU)}`gNN}T?j=%FFYH| ziQ&=sVXNpj2j1Vf+0}qQi$AsROkwt5c9Qe@Q<~G>nQ%k4=W=f)DooXBRx&rS8AIIIyMLu@N(jlPZ<*gCM8!N_1sO(8)fzB3O9?@OhDr}InQIajU3_C(?qrgQhZ zy{N{U*1L3dr*Vm?H8=V;>fXO4>B;3bZi0*iXD8gRSL=-i>>BY%rOiHdVZ{H;Qn3N=w6yDlV?ibi^#n z)5!-fk{{QB?aWBp(4pWQHd;f18p43!-Oy``y_H35#1b{UJFj|KrsX9v8WJlp)gP-1 zD)Whn^@&y9iWf+b)@0$E(Q}f-yC1`n3G#jN_V&mCiRyZ(fZdV!pE**ZSsn)o) z=N<~)m;5%f=wTTlp*Jw9G+|8n8qZxW`oz&1%(asK=RHlG%Kgt>Oym?hk)B*Z8=E`w zrILK~p5f|vx>JB&#zTT4v=)*q|M)&SOpSSf<865#qrLZXyCMnPB@Jdm&5DVmzCjB3 zTFuhvG0SA{raNP(Ww!_s_3OgNxvz=@rmSJ(Dn;K7ykZ1Yzb8hPIHAHyvz{tY*UNw( z)zv3nkg6A25g-Ei)!sboc36gelY;K%g!Ytcmvz!}&fFf|au3rx_eft)d;Inbr zgU`8@=}c(428NC2-qknwd`3B4Yt}0tB_PYO^4XWKZV6U4xr*vv&iU9Wu4ZIN^EetI ze|D8&w?F@?Z4KJ(XEG#fM8_3_5Uwc$n2y`W`Nj`x*Bq2a-T>_4Q-&)X3rM}Iup{&{ zmmL$&4#L&VE}8VW6!x4NBhRLZhj?>BY@sR%qD%LnuWkswt2cHspX0VgSQQhK8mm}d zMM0J>CS@Gyd$+_fuxBE@8;2dr+oHd9b4kw?{(GZ7?*cAN`J$(g`29!0?VJ-u^-u1f^&M{{B-<5?0D`=+q}t#Pj5uTMfZf6b*J>2 zOa$4aS3K>x`XMqwWUS%b8yq6QC9oj+Zu7O2(XbQ?xoicWzM=pH)kzZzD}EytP4(%W@<%Ok=}V1=e+m2A9SHdL)r~_J21nMQj7bo# zwJR;P%VJrtn0artH(hh2oPO)*yIRIWmmEh25Ux)9@vPjn#w8Phz3BvzK#^AXu;|NI zi9*L+^0;4G=$)XUula~}tsivC%ugOp!@McZ6IM8k}y>+h3aHomp*%Hh55tyMR5CPGC1MdvzyY^6nV-c~|_py6$!< z$&I0RT&n78E?MPlIwLHMr0@`bN2mv#-R-bJ-yg)Zpm)@mhnr zb@|L+6hcoGoV|k4E-l7eb1Cd*Nk`RG@7D<0 z?6_zX)M`QsCVe!1C^$i zh3F+MZJe!9PSf!lDDf*S&4!>r&x4n|mPe;H&Wu5 zC@#f89)zY#Tu!+q*UL>wx#sJ8>JiF41H}anjFJmjtH{?HdetPQ{c{~k*0)*%!g3Q`~vPF$PL(zzI#!3k)yvGMfy8& z6k3prrgQ}5j%>}FzZ0k2!KUd!Lq<(*3EBEZp#{wnN(h-wAdG7J^lygd)Alk-UsFn6 z3sOPUTELm61^E_&Ie?!a@GOI@A$tf7i9iVOvw@t!JqmInCn2X>bS(%Y=O&l(^|c_u zT*M4a^1Z9KvorWZZY-S*#@p4_Q^#gM3f#@)(?xl~!5sy>J}^%@V_faQJ>}Ykw)0we zzrzdTGJQV{L)YG6CZ5tTI!|Xu6x!3!d6y^14*RMeT1p%UUI~8mlp76|1!dFQdtkiX zz6zqlcsSZSx}sgZT|gd2RXuX)I+QSQzuel}3!{#9MSEC#p>06`r0?&Bo=LoPIx#tf z9HZf4&kO$i)9t7B_Hfo$HP#1J%@CJN{I5f_J?+t7h0s|$d+A%-e-*}rLW6v0KQ9f> zo%-6lY%mySa)C=`gTG45;DGT6P;z#(pQRM@bmMo<27{dRwrD$RZ)Y#yW$;0Jczq|i z;cW2N$(PyKD`T879t()eGF|=-^|=U;1=@{qCDWS03*)Ba?d6GH$T&-#0V91A%4mb} z0u1#{_%hJN4vUjh#?>ZrMGzzSQ6YyY{%M&H-^Y#|I7 zYG{ZT+5O!2BrqI7q1N(aJ~ZHU|t~~ zucNI3pOLh-xVD=z+QD&WfCpMHV7ERhzzHR1%ZF6t-R39n=i=sq_Oj;nb8&X{l=oBM zqZBR={N!LUK3+-^FDC^)ihZ8fSbGnzGR6bVD=jJ^f)W>p^U6qy!X@RTB;|y8!Gj2K zG4g*25ph|0DQS60ao(AW57g#iYbUR(qBc_(xKrSpX_T+8uc)u2D8|EH3@#@pCnhc- zCLtjLQiyo^yLwssiMV?5&z7Kq_C$F&x_LQbTzSbQS_9eSrN9S}Qkr0knk&@}j4Dc* zwkR?5%(D|fE;zYW)6I2pn@jv%x-Lklt^Lx>(zz#rC_Q?a8filh+30#Mau&S_RA|3Vh^e zaU$Z9B5=6=?6bHn;&6F!@tMRJTSq(pUrMU%fd(bcCKZ z94@OQqar5@mykrrC`;~;Q=QGLf$ldrY&!ZN(QK>=z?%CZtt zvNB3i(h@4FpmJFiWjQrjNfi|-DM^I11Q25LEs@vrvi9)ucH85Dv2%0=)ZXI(9)VKu z0p6A0Y3*s~=;>(VjMf2Dl^Z!ufltla*%SSpB>EoKuAXkz9%xsT|Ke1;_kb?;RN#a2 z@y@ZAJY~+(#@K>tB^QQkC@at5@zhn{!KCLQ;3p{o!az9) zaqt6a5#)Pm5D$KmQj!u|5h^=m5o#)GlB%jo%5oAC2x*la(r_6m2}w06H6~k_03qyX0CV0cpuD$O~LbUO-qWxD<#by8tb4r$Z1Fzbx6612}<8 zNh2o>{0IqhI0AG6a6wl|!R0`iWS5**PL`ZkPL`ZTPL`ZTPL7gB4o>ltOOvCNCMQR( zQI1lk92p4^RWib$JB0xY2+If~gk^>0gaK*baACNFFkDg?E+q`Q7bJosgaI!|fV6-^ z0D}PDkp^r8xBxH=;DfDjB?%>IIc2yK7_<^Ql;r^JRS|&xQaj)h;;L{t^4twXsF$Oc zGkQi5P;&N~@i{w^X9am@YgaJ+_=(IYoP3Hi%iV2a)jTjRyp$PO-tl|F?xzB4IF5cjaQ_(+>7m`{ ziAB*67Kh7JP9?AW0!t0pN0SC*8J zl?5}>G+bW?G?0lB;$Ka0lJXL=vlHBOGEa=1moJdkO7=jI{&13;PK$B?tb_)-uh`$g zFY!0YX*4W|9MI`;LT`=PG- z7ew1LtB?QaeYDw;{VT)p?~rkUBK|`$Wjj47%tBfy8|SL#{YFa8%fh)%A&bjjsqm+A z@#ot1f3J%u^Xv3nBer-N0{3JcTie=?tQ_k(2B3ie2g;hfuL7Smcps5+k?Bkhb5P)u z0d@%>)Dei;FmH;Ax(5Taa!(42&y={w87y;lgDK1NJCmKFvcI+K0u5kB6)*u-E{yND z;XBCo@dd$)8bofuSJllx(DSwSLOH0D?TFt6F+B$^G(3Qe>hcuBBN@2@pUSkAjCZ;u zUP@7vMaN7k@^W8+Pn@g=QL<7LT5CXw@3fh`s#KTvq+psZ-N?bwYkCc%F7HBFfhcR6 zXw8)GMJalZu8y**p59Ct3Q!GHH`gna63OdUun74k+@C_sZ-Ob$8Ww~u7-Qc6q|kk) z?tdE~STW7E^RFP1DaNZy)>)>B@%PENpyKH=X9@XD#P5)jf^(X`|8r31DD{ z=f98h8w&l5H^0g7+ax6ml79i{B1-=@%pK0&e=yR&A?OK1ml< z{Ldm)fJ^ggD*z(!nkdML^J)R_?<0mwi2|dvoSf`z)>%RNL*mLUYUB(Xkl))UUlI#d zyl;f5vI2OypN!T-K~@?-{sY8|s-L6rVqCM#AS*p9LZ=19Z?P=}t}KPZ|6#bYi`p>@ zSMG1al_tX_v&DauybJUoO0Ul1{f19|pT3gP{{TBk&d_n%JghE1i}&xb19`;FjkiBw z-WgE9&Nn?%&yKbqF~g$p-=nPrd93{{W`N86KsqeKHA~#T$qayV|0dwYTskKm{u#iC zS$_EA;f7c=-2Y*~K@Oa@XvGxNoJfHlpA#reG_!#iU4CFsRJWrfOd^`_f zI^rjY=f>|}0|r{WHkpM@6ex=EpDaJ-Rq#cKf5aAx=g1iv&x^>PK%Qd@pe6qqCoi54 ze##b$wdvpFWQw!^Ogv*3l>ASM3z>QaFEBmvUznKhYK+kj1U$X{9$WcI3SOf6~nCGin#LYj#`~%jQPfmus=-Ht9^b3A7 z&*UhP3m*8*rl-7yH1|Fd*|Q+gG`a;(1u3s5g4dqDF~R?-abjjn ze*6zWn8n15)Q_#t%{Mj=dKVyc)t@s|?e_?h|_ptW&L|Hq}KJTSW9Mdm~{OPPfUc7%j_|F8PyE7pWM(gcXjE{Yk0hchMJE%YLm? z@cqJHX-8m@`X{AIAbwKnqGj2yVHJn}q|`;v6n?2xgv?J0mG}b!0HXahRTnLpehDd9 zegAc#UoQ}TsZ98E_y4&pS(KCf{@S8<)qbR?g756eI_umAeu}dn;{EUM|0wW33jB`( z|D(YFDDXcD{C`A&`7Hv_uHf4!U$6lHsg=u0c=VFcKonLdAe!qsgXCUfWWt|yyTBw$^MxO?3*xNGrf&P zrO3`byu7m!zxbC0<>mpln}F>B=_PH!$Ns>71o)kOz1+y*1f9EnnqV&Fk39?O6D8L-_c1o_>{l9Ou0{c? zthWa*_yin<<`r4Y#DAP|0jvc$guxCEX!57#XkJ6WWsa`)pu1gd9VwewIJ$oI$@qd2 z{xI8u63E_Zz6KuF8$^iBQWRQI#s*RMzkz62mqFC#aUcdZ*KfNR48cAp5M;#DInDRL z58^5AR}NJexTW%Rr0kCaDeLOp_m~yXbr>-@q^9JMIZ^VZP8X} zJJ>Tt6Vie7ArojH*uKaC?4IHQ`9cSvL(nlO6gme*K`~G~bOTC;?m=l#7W4>u0=CeYY&nb-wg$!n+W^}FwkVf{DZ*4?yI{I76POju9_9-3 zh8=)~z(QdWuozeZEE)CymIHeNdkrgx)xkc(x?#gG92GScBNZDJ7u7~8F)CRqB`Pf{ zeJTqodn$LT0IDNYXQ?hyU8A~7l}Yu4s)VYVs)eeTY7Fd`zKoiKdOfuWwJfzVwGOo@ zwH>twbujfQ>I>A@sP9qdQWsHIQnyg|QBTs)(Xi6+(umM(rP)biK!c)jrwOJxO%p?t zM3YJLoTifIBh3&Ek#;#PC+!wmd0GuxV_JJ!U)p1|7ie$MKBRp?TTRkQCniV#C6HxB^Q?5UQ)26VoB!`Ji`ix4Gi)OIt;c9{tRIZHyCmm${5-i z@Jm^i3NBSxs=svq(nCuxF1@$(#nQ&5Bg>X8<69=TZ0|D1We1mCTy}rit7RXTO){=z z6k=3nG-vc?Jjxf@~UW``JR- zQrJq_hS*oKOR*cW`?1He=d-tPP;+eL(BN?92;)fOsNuk^;##G&3cc#&s(Y&{R!yv4 zw|e_(^y*WqQ&(57#;xI9qq@dvP57FuH6PZ}tQA_jcdhT*xV10V_N`-Ew{;zA-KlkH z>*_hFIE6TMIRiPbbH3pm^V|j8-8`&3iad@y=XnZw zdU)A+m3W4c^$F`s*W>vF`3(3D^QH2A;9trw%Wubjp8qNTkN}T> zw!i^_I|B6^7&gdmaM%#Np=iUzM!}6n8;@_y+SnzyT2MnUK=6)W4@(G1pd?}?sw9_7s!3uc zvn4-EZIMDt#Yw%FW|h{KJ}&)KnjnLa@sPPE(~S^7SRpPWYGv7E_sE`>eI-XLw?hsq z_egG1UPj(aK23gbtH@TTt+%&!Z4=yv-gaYKn*yJLwL*eIiz2V0mEu*!7UX)QHS!v= zbvyrd+wHfucPedG+OKq1X<&!=4v!rVcT6bDD+eh*R-sl=S2?9ps=89uKs83SNsUj< zUhR(Bkh-*bp!(yTv^%wShVQJ^;L<>8Bx?+5A~b_EpKCE{>1oAiweAwy<*_SQn@U?# zJ5sxG_lDiByR&p49SxmGoez5i_qgxL+e^21&)(R*9lCJc1G+_eta_GuxAn&Lcj$-d zHyCU(@HTjA$ZTkCm~1#6n?SS(I73xwQE) z^BN053qOn3`_}Gr-j{F5Y-w$oZbfTlWOdtW%38Rom`(|xG{d1aako3;Cb)?+j22xEV+jhzfjqVEutZ2R;TV1tkQ-f^CAIV)?O0u;W~2gNZV2Mqj!!m9rHL=dwlEh_!BfI98Z*h!*L+?ZPz3=xtuzFCFrj_&2^=YA>G^DHRpIQh4>~wfyUxV%g%X5}A_BH_~q& zzLkFauvDfrvkXy|{Z8&(Zuz$If(m5ClS-A!msJ{7CDpsD%WDj3-q)Jfw!F7}-&N;a z_qpDu9^Y`Vk+$*FhZP?#G;uavYu?Hl*R2=dcdVbK|MI}5fsDcJgKvh6huS}Td?pQ_9$7PzI4U*z zbZpO9)40nxe&W=ZHD8h@WhaYpMz~JA?-c#iMZ#u6E>VlvNOA$2FwP(7u^s3F(+7G? zAD8hI59<0nt%|`sX4+6g5575*P!tblD1s3Puf%^q?jqqM~E)hJnLASZG($ zt*2+@gOKv!u6a@}E%;GS=J4l;YbyGl zs%i*z)FuN%qqS*ID_VzbL%br7ps%Mtt8CjWYd7L8BqzU9LlY=#_70BweSH1=1CAa$ ze&Xb*^HCR~FJ6kdar0JUQu6JLhnZQ~Il0eYyexY4y11&kruKbZ{ipVh&aUpB(XsJ~ zFOxXXeDV<_G}N>-G&FQ{v@QTL3pj#=7ETB8u`2DQm#}7YU$Wy6e>l5tf(?fx!vldQ z<;osIt3Do<0w9A6kVrKzGF}6;w;|%lQ3~LZ&1=u!$RM9Z z5;c7o$tHCpJK4GONahbC*=g+Hcj97F_N(_@U*t4Q9Q{vTO3ry**F7mDuW7nJ;8e`* z+~WEk+}CG@RP!X~#@^pra$lUwtU8-{P!0cq=LPnVRpF_R;_*RKtPBy2CddiF>@?%k z(ufA@2kflDV=4ht0jzpycj$Aj$H<88dY_sTeF;+#Ynxf#s&P$2>Pv)xM%~J7$I|3e z_s1CCI>oS5^xoCjw_J37BhJe?9F|c1ieqJC?q@};E79_ZAefz z*kC+e{7)ABUz4Wg zVkAiA!yzI#I^vz>5c8Mf`0iB%R;)Le9FZMXSpUKt67(2(h6Jr74q%Ilh&mIN{^OPd zWr)z)fqldbXUl3#Z(=R>mHh;^=1aW*335Nc!}DTj)UUA4x2wE0m}~8W@~mp5(L$5d zDYIv_raZ@D?r0hvbJU!=Q%ic6fNe*Bqp4tBVpF%V`1U}aM%ctv5|p^AjAx9N1onA?x zZ<@q6;0R?E&cUmWwm7E84Hi|m`aK*{8k$moR~i+yqCP6Dey@~O)=CJPJbb=V#lG6K z_V`j%e`?8Xd{0;h&&bwC5`>e7dj>-SJw4k4uDf5kS=f!OJ5=|@vSr<&{T}D-c{&@V zzQj1+yzTaIOZ&Bei^%=BzE`H+9a!t2Yo8h{4o_TC=*-T{{2($FXh*2q7ALH)R=qRy z{PPoDuUOKbUVS&*X;bf9L!Z;M#?YXu@Yc)gYhE}$cTdy{OrN}G;ALNO;mgN7`Dj+A zx|gopcb^XgP2o!u_78cjn{aae@cMEwrT=cbmYf)a6-~XC$OEYFX6c)}I&u{D@t;ynf$#snWZX4*OiNmh9!9m*y-> zJZWe}>(r^U%jgc>Bzl;2-Dl4P-!iXH@bJ!C4J+C<-_P^WY&%Ot#=UJ%y8q>k!(C&c z8;6n7jxqMeH*6D+sW0`y9}Y_Wv^C5lry1S8M06K}hxzMG7Qt-p*LM!yy3KH{d(TeV zORApb9CbDgXZmDL)hin&o`Gm0iK&W*nh#aL#PJIn}i5Z{X_;6u=r*1Umjzd z_>-xL-eEYP5Z<-GlFI+D4Hxn#O~y{{#lER0+VFgN-~mn((yHek9vR$9f`qc}7mi-# z8M7-(z(``V8&*xdBSCy#YDA;an2D@1Oov=%w*zs+y&n@AYoC_sxASzhR{Z-s-!>*2 z)BUTDiiXB*Ix#z=1X*|A!ry3b96WdCpZ%ketx`%G!rwtn_5<9+` z1jPh#xmBB`ez7!??;k9)Pedzkxj z8GqF?)hf2h{So-O>o5I*usE^`J zt|mCOrVAw|9o7)JzJF=AhAgzd*fjW4B(~A3T;DO2kiU+nTVwAe_Zh^nu1Z)^!H%M} z!tO=Gg4<4iD;~T<^24@RvH83NWi|(oIFDD1JDra zDZ$%TBq;YG3F=TSoNUi;uO&Y1B#QG)oah3_`^gs(wqt8V3rQ9K7y0GR;773++|R!> z6et?Fx{O?^^EYK0?)Pmr+bxn9UTS2}_F#E*lL<^G`}!M~H*8Xuc$iubI^2r5C!*Q* z+UjParC)yOdxfWU`RNUrW?HWj>wF4tT`mp3o&LqA*Z8!@E0Zm(=%;Jh``eU1w0VZO z9d|j)=Ju}W=0ixx;X0e(>XAhA0DP;jRlj8iPcv3{xGpHtpyo<))%z<)LmpQcb#ykJ zuhZl-(%z%59s8oHzxV)+Y>q&0ZodeV=CvIWPyAjc(*!mLy?dX9wD#w(?=>@f(IJxV z6?ka_`n1`3`$8*z_s!?CSMB|HP01iYW|d)LV@tG6S_8Yex%y@s2dQ_Ln3g;3`}k=a zZV7QY2@+jfcnZlISd{50XOR%~*uGQ`R^ivjzS}NKEKA1F-lMv`m%s6IyIXedwqRGr zeaJOCkDcBB$r~U0>eTjg27|~ZIx)|jRgrT0D(ZW-bV+GtZ#yil;mUt>@2(93J`N=r zr3X|`hkP85v=GWVGv(?d zCY1f#uM}BjJUX^aBt_)1xq!oq8g-jJ>cUrVuHF-I>tTPl3_j;c&Rdraxw87FH&;P# zHC`|jnSR86FeO1Fe9cqCO*o(m+y{EdX?jAdJU2Mu4-A4mJY%7$q{{z`9R7{ol2Nd5 zFpq&VbgVr|&}wGt)K$?QNAA8 z7lXvnrv`U7AlvE0CcpZ$JlcQ$9exh_2L-3mz9E%zUM_e-0l#?J^@O!5aII+8q z1Z{$2ADzu7mP9LK%dWlW!7WE&dy5LOcT+RszwFKo!*2{D9_nq*^(mWF3MG8<9vnB% zT}KS!0Gir6r*$M~p8}qM4!S{to_c3s>!h%w%=-wHnb=}R%R&BBY+C!f=8>*UdSZxi zGw$i#mpveIf16R_Cdw_k$pDU0MEqaUu#jD;&j;?)4f@;HnCC0?k z5jk|lg=buwhd^Ch=+z%T9I^`ks<(MD-6p;vTRuL~8Q6JN7dENyBSGkrgu>2pXJ!K3 zgTk@4=E54IdYvgjGj&3wIxy3W4gP;YVnfTaGpV(K>)gf$YTq{~yY!nTKC|9JK!M!rK{N_d!gp=O-}%Q_9zcG~^hd0G^V1|KPQiPh%gQ@sa+dp`s= z=T7XK7|8#Az|Dz;D%?GA>h7!#=d?hFtNw=uaV?OMs~#Z;Dej=+Xy)|giA2pj`>9dD z96VfdtHy!Iyv6ge*C92iqsR77S4;_**u3lbl3^YnWzNH<7OBc1^`mzg+WJ11+iq!l zfLrY`DSy6=pbf_NB>%*%=E5L+$s0I-&*7(=MD)QC%cnJXr*2(Pn)F^X!qz0yD@(tG ziugot>UGx*d{OWT+^?kqCjE!^Pd4?$^PYU*alfR&ex5m^Nso<8GKC2!4NzY*0y)Q*(JuNS`Ca^N5%#JwG+v^$|U`OJ$q zBk?ak5i9pa?0fbB{(V;eW-$C3@7Mkx?USxGlPtMvZ@Y+PWs@qBN*Nx0I633+wpX3u zZn7^qczf3{UN%*4PZ^M$hz;gU@3&MKAa1<0%JqVjV#IF6IR5c35h;p6s|M186hHWj zcz!&mSkuq*X6zoKKxmA0>>FDCFu>Gup-wcoKqtCT*a?mSOk&2p_{VH2-T$>HWu6;nLfNE2@;HTqpkmh;geP8q3vlCqn;F(a_ure{A&xm-~3@l+&3WoWK zOPd40^P=lnK>yOh;A+bU$0B{e>BXt|ViFXaid~$8K!Rv7`Pk8NP+HxFEP@lTV{11M zh=qe&h_3m>GURLK$=(N+I0lzgVkUlKioF1=xhvH%Knc6hJeCTc7a=ecI1=_C;eP)~w-F@@MCW`mXlDFAp}!-WKy;wJt*^qj%62d+-S>ItvYGxynh?a?`u5?7G7 z?VQVL+O3@X1Xr0v#PILcIcw7W!ZzdeZg6aQMJUVI%G9(tJpRc5bIyT>CF?E_mbpkg zirCFj^Fik*qYnRaE$*8&2B;+7!>vc$yNVo}e6@PNh)mGfA~s|=4Ft&dNt($U4}ysF%k)!8f) zjJy0;Os+C2$5n0%*V(3u`=b#(nk#~XIa#vaKkIzAELO1A*9DSQN$n__Dn?=qst1?h zmi~fTGb@gMP5S)TOb$JOw=WRO@c9kzxAr)sank53 zVdF0or+CWlcukbM9#{BA1$25g9Pf=i0PI=mX`x~l;=ekg{O+{KyP)$&a=`UX8v~z^1G7X-m!h>W5KlmKNvYUNcsES4-9y?5e32QQeg)H7tEs zbT~(lfIYA=Hz#JeqjjKt%v`gtFUaJi9rBXE7*je2IAZL1ZBocXP<#4X*%54!ldpX@- z-fU9$U}s@bvhzV%(F@?y_+RyM?l*aV5MmdIgP0`_95;g^pf$aA!CCdm55PQub{^9= zY%sHAICi;|J^o`j!)3y1-A9Gce>oQrE4W>g5>& z9ci3P`iGW-f8BivciL`M67jxa#{Cl~v~@&#i$3)x2$^hn**}aA*|6-;s78`|#Cqhx zrca==CIfNh1BrZn{oF}rZf8wsg&2Ltvd{5^pWR5`59YIw9>f;eHzNGe?)Dk;iRa@RL^`RJ9ciB2~OCul^*WOtg9CvKuaA8nB z{#m{Mx~Pg>Z{4LUQxgv#-t-!Oz;QdCc{|Z1CcLg8TLt zQOLj)5;Remm$lAA!KOjjrN6s9`AH6{+jRGBY{fy3P5g28u`e3y#ogBm5$*G)@>jz^Myt(52Cr^@^iLy0FFP9MWPuB!=~d_Yyq< z?qmH!-z9!Std#wEk)BsN>j|Mb&yVf&TvvqDQ+e^QVQtQumKQ~-_#K>HdpEo|RQZ@X z?10jQSkSh&1I+T6gfUgm9CHb~!W#nU6Go-sM|q-Q&yvm(F-iG(G1-o5&VTivD_$B4yzw%H&z$zvIA=wFz^;DX`ck2ZttHt#EA^~)-G3eR zIku@@BoH>3Uhtxi`U2tlCxwh#mrS1sx_92|ioHPJ!I&GYp9rf;Nq^Sja>2b)9)@)C z#YQJIG^UqFe`WQfC#2?-mh_4MqkRR2R*l-hl`)DfCL_L8>PvCt85PuMgz3myF}xl# zct+>uTUo*x`gZDZghNcFOLjOjbCq%IQ|b=e3i>x^Bbv2G$9?rec072%;{u<;)r2%N z1)TWZd4~Kt#4mNszyA0*KJ(tvpz@sYj=GyQ`+YeAnGYwjoY`$(@@ezup)Rv^g%MYc zjYs;Lt?JRuSsx-%hYJ+?8^uLrdty{i>vb9y?S8f<#v#G-{VMeoeL-J?*)vBBI5g;xORK6XHL!SX;<19f;X+3Z z4A}N;t_s6=7ktIvf`#%~@P=k?U)r^@XI<)I8yf6Z<;T3WnDR+xjb)XlTWX6*O^6+N z^|CoX_=^w5ZXy&NBr&kcaQyLP`;;$r|5QLR%xr1$TXM9O!}Y+ z-xLn>qa{fss9ww6qpk9paqEf7&Q?wO_W^VxUR7GMuvU|>_U*{00k_2t;JR49S!Dk_ z;Fr|!e|<%D*LM=r|HPyaPwy>3XU7j*%&eSG)e0k~=zWPaEH?wl-SA&Gy zJISn*hh15t2OS;@Pfu%@ru#rb_nud1NLHCzYmTw13bP6~|@I@ak zj<>sC$29Ps2va}z+D&6JDoWPopnq-BV9bWvoF%#@lttLgiAcZ@yDs>oOQx@Wqt`6)H4N z(1FLDH`iUhA7Xkq^SN0vjr&>~wn@FDMBnI_jf49Nff1HscDTB=(Wo5r)vWi8PXFD2 zU$YYZ>#Lqj#xKXgGCD7wE1)7Oa#tOrfosd9uo}xjI?MIZw>(EV_f$W>PPnOFH5|w? z;Q<~gd*I=(h9oL9Qkq((h}Rr9>Jf*!=wC5!8{5>k*U!u#rgDQ8Hh(4dvAUX0s*$CBtNi}8rl*RJZ`i(b^ELll9u`rM@w?av`Z2}#xjCmwYdVH~Zm_78 z7M1IB2uiJ{eluohb?Qy?682uBM+KS(N{s}gZwpkTJ=k-EB6bRtS?G!AKR=hmAfXw2 z5PMf)$kRPP*0WJ8YfFg9z()Ud1gX=;IRD5`^2Y@{`5&G63>O^lyfx^6OZ*DkcRTHy z6u;Js|Ij10Z`quz;e5JXT73KFr4t4QH0f}erB}@(G+F}R)1EM3N7dl!n$cD-YwH@c zT!faq{LmoFxms1ofKG6g)^--nfF*l%tX+|b1%1OWt7N!M+xt(8&_PQDhSN zvh4IVH?RC+gPQeJjp0tA&5iB@#9n-KeX|<^stWgsjneBHeqO%5#cykJVmzaJH(j^j z(X4%i@kg37PBC``)Zm-hA1j5m~v>9E^VSm8IJZSsq zd)=yzuIqO^I3>cl;6A(|Zi>gd`%wKIRfy7{PbpW9qH;dCpj>&VcONQsKbk!9@$pc% z#pz(_{u}$W`NE2?~tfYeEO)IT)AtB z=LS&;)oq9yaY%kH#WAyZU|~S}pIuQEnA2~~GrpJeF5Q8Ojr3=?go&%Z@o=cO=XTb| zz69^EKt0EThV_yve?A3$KgBN@eg4%Il9VOmP?6*?@?QCcp|)G4cJ?xY+h6v-$rycW zWz@TGy=BT#5`mQ6{^~vDg9*JkG zoUFVKLHtHZxKlZKroEu~wu~LS@f|c`R65m0EgLS&ScO>!Ilu0F{jvsGzbEKp zVY8n5P*6epl?U2u+kBaVot8g(95P%j6raI#xbgjHT2|PXo2h+z#Ng!AW~l(^H=a;@ zCB__sNRT&JZ@&q}j+6xU7S6sP*<(tAuAqolk-e)d|FK7T-+e<}kpz|GF>T4aqi{YP zd_P}1X~O4DI8{GYN_UFaZ=1rps$iwICUe4@@pA8h;Qrv=I;ZqvrBVNlSxWdluiwQ- zp{lbrKUv!#GopA}QjPWwCg$%{SSY3_lrpd!+44FBvFBs>(F&37$Rr&l0nG-x3*iN~ zRH>iI;%J?SJKLb(sa4g{4ObrQ-qBS2jN7h`wL@{$B+nJ`WgVSwPh-2wro5Pk#|-0! zc&=K0MbmF;`(eN@>CFG?LMFqq{m!Xciw6IfW`3J}f`#ZFju9Fgj-{n2tz52%OglEA z7qsKKaY!82V|k%LZFcM(=EGZM&*OR#OvGSQ))MI_Y|?9nPx)GEbg>10P$5AFy`A>q z=-<_TBL;4}5K%cC(GSOW3w^rDY^k)5Hge~IxVEzGCYWIyf}yvi)2fN`m;5F*xKM!fg!8)~?k`cL-6zq)$K zE^L2thQ<5rR7p4DUUSG=({3BM8J1O_ZIdPMmMPm|4ZUD!eQHHmj%xJrSC1O9D|YgF?5MT%k^(xhZx6Pl`c&YXfulj+u;Sjrvf}34Nm?Viq_Ck6 z37HNj&f=n9MP1^Ow1}N>IhTE(`|zV+9iiZfXv@8W%by!qzH3N{t^03r z8vnoczB{Ptc3T%e3*twyfQ=Fr5$Vzu3{epf5Rek85Tt_?1p-Kmihv?TKtMo(bO^hdJlmhBrze}-?#Vdv*(=e+&TN6duHF6JNw@^zs$UKy=$%KdDd%s=;;(v zJ!sX@xnXPhh}_U=v;4fg0gGC6Uy(*4%TB+3y0pA+K*kC;qK*52B`Gt51X2?G&ppKd z+H3lUvSk0%*aV9Xt-KOGZ}RxJ{Br2E<&uQhGIXzB!pBBpVE`@JXEK1M(XE$G2;em2E`tM zpCx7UPyURV18olsJfocWY51S-@4s;b{GU#0sqv9qFN<}mNNwQ8m5EnmYgVW zyWKW!W_5ga=X~vdt1QBB=#$*IxvAq5+zqJyVVA7 zRa6fMhaYhb?XHkEa4@(lVV-b7IHM8Q!yA+nRrU3?-l?-;FAb{b{yd1;?@GK(Bgz2_ zGi02=#L_}Rnfe$6T{wn0sbsnDi~|b+G-A;5I$>*;p92uc1RSsc)aMO=XafUO z;OrjGkVReqn=bb|U?FCq5#~Pqnuuu)+Aj*@riLn_3;D2GNn92a32M~e(yZ%si^IU%>?JpqG z(TL_Z0L8liAe`;9mSOi16cSas0ia8NbR#f3=K+Cvbi>~}zm{8%d10f>T1Dym`K{RI z3FWN&<1n!2$sDlFx2>l0JSR2e??3NE$$s+z&6AM9D?5_-K9!jrm@HB363)XtGWH#} z*s5q&?Okimo5#S1D7A$vE|IIJ{ z!O-xZ8i)Khhw>+5_NRt_aIpV$^y5z*{hw&~2M7C4b*=wjuk6rpyfcXdSL%BV;^S&L zRI_Kb*0Mjg)zDkH^8#F3BaIDGGmI)>f|8p@^cMg{A-|dY?Z#W@I**h@!5xm&dr$f_ zklTXL{x~<1LZ|n7nBSmjx;Cq&e#Zq~ZB+U2MVYV*EI-RlGt(}LWSK;6vmZK^+;MXr z85XnmEEtF;r}Y`{4_F3p6Nvrzfa8c_jVdfv$A=3@10hC{vCK6bWChR4nAh6@geQOg z1LfCn06-CWGiH6jQX7=F?+gf4s{4N_-aTNsImEyR?0*I&VksXHR{&G(vj+n>m(%;q zDuK}hmRb36$R^AgphyBDl;R3z)DJ|T+yRTC)d7nm>mqu85!8X*=Wj>?NS%t<|In=l zdLbY!$?^v6L_vB1p-mGF2$f%Gkfjb-G(7A9u8IyIbkQezFwa>tAQ&4>R{KtM&iC`MrasGy|N=00a;vOsBO25y7Y;Wb}&55I$a0*??#*dbv*jFFN`aI zq63{ni|7vY8_R?pinU!`n85qB;)*HW3a>$T{`u5)k51_zK};bvoj{`yQd>;UdMW^Q?opGT04kMG_ZK8^q-Zal>`i+*3_4tm-r>Q&kj#kxq8L4~ zZaMKTtHpy4@AyPgs><_bq}8~2Z>_>WcAF}cU)Kdxr`Dm=k`Ylcet0vVU)yE zcmLK^bhFbK>H$^>{L+G4-3rED7=NC zlL;}pvSaU>o_MXqgo(bU2AJPrb(~i#IyWr2pfmQfs!&W07uFCZ{wj(E{0NKnUjDZb z|Brqm4o3d6a0BJkye-E?$6iL`);EU98avv6SwLv`ET z)tsVln`!8e1Im^3-mt7S#43e)@}}^|tE$G~JtFnTb-2^N zt;jsnzTseS`pz8%1&Y!3ndp+Ygbt5re%!WdENDas;k?X5f zWEs!!mIY&&7l_%I5oeIo(}DE*10KZMQJ~YQ0U(tA`HK$01(oHM?}Q ztmt6gmvgmB9u+Dod%@&YKEsH>D<*y80XcMEmojjvT)j>;d? zG~tfe(i3-$K5k3GyPj0j;EbF;Omk+;O#xcnWm-lk^?;?uC0N?ft+=y(iI|Kz+uA;N z&W*@7ZQ|}o4mlNzH%iTXP#|k+{z*_RLuR=A@NXUE>b;wV#ZPlsn&nQzC&FnMtJ!ZFkE(W_tU>P|GlrcS z=H|iH4cJ==XX$O{(F75)Qi;mG@?Vu==G`A|Fs=p2SA)DsL4>k83dg5@x1I*~!^&z^ z4tQJ)j|wrjW$Ag9T*a8JEvmg;U+=0q;>wE3TJC)-;mktv{0z2>^g4BnzqHZFdvtNF zsLv;*1mkKamfPy+gWSygl5+DYN+EX_cY%F$sK4s4(LI+hc}4yy()wIe8mAHt2Na4O z<6+Gls@khwoTkUHRqVMFdfgi)YCXOEEbFVQmt-*V$i5|+GeYvmq)xqSSJj8bb{HEy zDBzY>Co7+15hm0TOl+$mvx?Z>0~T+$I*CqDo45~$CpY$m_M55w&Y_#&j>~>6AB5Zm z3WI26@`fQEN1x;&ugw`nhLDC=u{>aZ6xWCJi}d-q7`uhK@CBH$`*haEW22L!Iv7Rw z+lF9wgjlz`0eYIZObXYb;}lS z^tkn6D-=t#pSeb+ar#Qv`!qg%L7Wuz=f~EPX%W7i#Ha=f<0!D)gLj8aL>HkAqs=06 ztYEYL7OPRF*C)^-bUHwA9-%j*$VH}fZs^5*Y^cDxY&F0QKeoE@we|EEQW73t;A`7% zcki(K?)Fa9KC&q$@j=%#PeM^19{R?=Nw~h)AEO*V$tNyp3$lJ0@OmMEhWlf>xE4111D(Dr-j(&bkR08LA=5d z|4!C=R2{XK@s(-MXq%ufBLbaQdMGYL&Z?q^!PH(GFPzkwV}zpkbCS-N@b*(@Om3cu z{@LcLmjG6*@kdLi=N1-E?(V`(Et{$o-FOZU<+!PjP6wC>f1a!z{X*~HFggjX(g7jm z^P{i94L6I5&Kn6}RDgUz3qrC#0n1503}`fL8OW8-?a-k;DGRu+V38~>aBr|1!QZZ# zGBttk)z&yRHZ_6CRgaQBa|;!5ZT2_ND46QqQgoh$%SeZ@q9jZEI<%+J$)wTrRHuj2 zPgP(ADR#;y(w|X|Da#Jm;L(D)RtyUXii@Y-$`Ary!9}>b@TO90L5iyJ@rBgp;jC@HBLVaihGcSbTudOZ-2s# z-_Es3WSe`D?_`fEj<{v(`&O(#WvyrJtDb67XZnI0qhIA3yw3!q>^QG(oK>Pg6yK%i zdF@Vbj_;=n47Uxc38^{Qdd}`i&h>8IR{8C9;@Ol!A&K+t2KRIFkkTZ)(}PGSPX7W} z@Y?<#*dXj+^e>*tXd7%RNoCI}hw04g`^0#(SWiFNQgiMSk6y(W4<@(QkoM34OD}rC=%;tDb|~>H!qQjr!i76B z3dD(6cDjIT1foH(hFU0=Gu#nVj$Pv)V7P$Qu057) zX}6L2R(IpgZO_Z2l8vh$%>krfZFa;Q@x87NUXAWekHHUVYw#$@Vj6Epp-tqTCj>@s zthtep7hF0QO3j3H(Aa0*^KsBi7F!xv0W=#?aCx+y&?!;v+l*a|VpRSb3M`-ZP4Cg^ z{3@gDbU8{_12SP@Qm3NnR=3vj^ub5R0v{p%;avga=^1@!V0+vqmjA5qa z;@0A*?C@`o7L;ulRUm?uu}>Vc0e)+j8?y;lHgu0&Oi1F4kI|#I0|_QAwnv=j zEw8KovS!pxIx@MN$c-N1bYGgyF3fDWQ$z7F;}s5jwV+JS@GA5p75&~D=fPTfUCj8h z4qweigYHhz@27^2l+U|&-FDKbbGdA3_sg3dm*5p_3ZUD8^8_tlFYh z^67*nf%ft4L@vJL^&W~?|GfF&b|g%S*7X=jh|%=p@3vx zsP@kmOIrYvwOERaZ&D*4&i++VDU^79M8vLVR-_!S8XqMm`USNGPmsSqpXg96C`dQrB>X7c$AGW=J#B2y_+L#eXv8VZy^NRlfX4VVyJ+PYQk zhI<>mNQeDdiC_a$wjEjAVdqn>Bx4g_?c*S6YL{a;n-lW9Q&-?eQC5%wMAw{?-#CwP z6*hNm9FKoT7G4V5Y`z=UxqJJ(K-${HlrFjakW;n3xk_013FX(X&Eke%FnosJUx*Gf zefL4%Vf|EPQHXK4adPQ7RKp*Il233mZGxi6%YGDL&A|8geUp)!96Jf`^ZZjin%*&I z40o4=rSt_R!vl!^z(f=2DSbBcT9G-r=1So%5${faHZ;sWsk&Km<(guFr z4O-e^k<~E-!_Cx~>U)irzKoYX!Y~F46X@e5Koe}dfvbFBxr%)TZlAo8dq0A`O{1ia zA9HqKeBW8k^w;7S%b{_)quBMQ&HVS1m5e`fzZPMw`@}fbBu0F+YMo!KBTQnC&N-A%wj$^w zH;S9Xd*A%kJ~ zRea1KP=0F}#EelYv;(TCBBFqTMhYqmPs}0KU!ko%sxQU74`32b(qNwi@a8Ex~QQVhd%>;X z(Cxo^8;FCkzve(bQEjJX8BR2}Z8UbSATcpLzrMcMchvfO$x@-4HO5M^5N@;Zpd(Ss zwQbV8PBY;|y_)RNOCL1qhE5dlyZ17v!2<0BLa!H1(cQ)TdPQnLJtY{DM~SQ~9{^SF0zJlyy>BW)mJcZsZr^=@&yA76wOQF+=3?0`_6zz6Gly?%RP zJ!|~fZG_j8`OkjQtF%+qzR|laa>!)!6B}W(({u^^gWn!FU1kyS62`)3u$k?7ut)@_ z&?I520GnhPa_#c$WU6DPCql>vY1lT*<`qkRi}-{Fhic|=1@`8_Nui{$cO@KNvT{-OJu#@m(9wq4`l-r0q>AH&2$ zCBKIYp1E%9__?FGqnBi<^7?I7aW-W3zOs4VGKAdD1duj~>T41*^i*F`V@^0fhn&Hs zcKyJzwKc8DNzK=6Iu|vtdnIQ;;&!q*swmI90{cVKKK1BF5Tr+^xMEXGe0m2Z+b0+5 zq=0UD+}K6b8T-BGA7vy{lL#MZ=z=6Fu>EcZGxLY$lHWPN3=ulN+hH zkOBZSZS3lUE806s5BL?V=i7(4xx+rBjw*&|Hfpfpa0e{xJ2%;1XRrxX{T4JjRA&O_ zg!RAAbw+$TCQIXEyuqDc;gnjG1d6sqBYPXc*(ux^G-b_^fOY4ApbqgL1~X18zGof` zg6HpTv^aT!N3)ay@2(Cw0Q(W)TVy7%NDbXa@QB{k9BZ)us+Y|#?!vRA>Ll3D$ zuVEv-E2>9Mst)ohXgZ{rooMIQSIW7C?M|~ z9s(F{AesIdviEx+9N_7U5$YP>A!70o$^g832XUCG3KRjAoTTz@Q|6M~;he(WL~us6 z|NJ@2*K0j6i;es}v@KDG_OKuVhwB#7zpJBjjo_O{cQJ8VOXhQ~&e;4~R%+d{Yg-x) zqiP=Fy^sUs^X+}S%pJB^j}tE0xi{-G)Qlmg)jgjI#G>yA{03{&{gwAiA;_P$9WbWl zPpDvWXZHa4amAVVdf9qPOxrPE8?)rpIat71Aol8MnTp?~R1ErID?N5!?2eH$fpiuu z*cG^MnKlIaGQ|=50=~?g2p$-HjokffjfUc1Oh3fbbuilj%Uji12y@RefHZ9>3?s%Y z120nu{DC}Ur6Jpj!21|8OO&#)ytxJ^5t8Ng^%XTn4|Wr%6BM0JEd+G~a5bD?kc7Xc zM~s>DzY5ukM$nQWw;@~YE0BGHPac-D7;fo1x)=p#m9>R3U0mGskTRahJBxZJ^WNR0Am~^rn^j)WdsnjryfI(y}gbj zLOmhRHKZr+Dlg7IG2stJu6kT7FgEN#s>3nb@k%syzlm1ng!&)YJj#j zTpb%+uB*UX4D$ZI(7vP6+|BP`3=2{A479f^yK@@trd;a_OSlv3P=cqqk4B`8 z^h7KrCZGMrOUizo{f4$nzJM+%>M}95E0;GMvy+jB@{EjB8OV*u!xOx~YEbC?KJ9Ms zeX?x*X>IAN^f7J!U$+ieE?=D>C}ZQ%>br_I4=1$JBVZyvJy%{B+(IQu{AgG$V?KiK zbKK|oUE(%1BCYIm#y2hY;9JtUrr5q?h~QPZ1e2>TqN{)siJ=m{(nRdi0RE<*SR(e+ zpoLum0p^!)OY1tvLoRHSja#HRB z(I@ZIx;ANffFMTxomLWvnUaI{n!Oz7oea-b_-Ugr$Nk8=?|0r(Uv9ZL&WkA(K~Ks@ zUbjR+b!^lc*7N4M)mB<41oBCI!#PHADXeD>y-+{zCOsMpz6cMS1PnkECfMeEE{K&T}vyd2Ivzt0LN z1UH8%vm?SY4QJ5HJd79>r>32HOn~ED|GiD-ua*9I{#T!f#ThmJ$E0?p%8V_YSHJ&u3~wwm#oR z-`@+<3I_q3d(Voo&i3r)*Gir<(StN8lbWk|zbPF!kJ>}_eZ7$jCqnZ}vSU~6OPa&O z9=SXb#w$P!%%l0a`>791I*T(Nri%vix<3r%mt34mNJTU%(a)KFZ1R;qDj?yb=%lZ9 zwdo?Nf=7pQI!)wILQYN`>JjS0!}jLFhc^|COvsM3b3jBVnME^eP!cR|6!CR@>RJbE z`H$^zji;^+u_^t#$hq-az6K47tYS_5#W>NZq_VYCwyUhdoKFSSscWIJ_HLr(kh*6N za_&lu{5)V$mi$C*VAMn69oxCydG4mGb(7c2w)quH$#lx0v3dc??oMQB3~OV3VY!HJ z{7l&PLk@?Ztp+v^fiO4WOjF7OHoxFmjXHN3^Lryo{vhBM1nLcoZRQUM4FiS3%z z=k@hZtm*HI*2<6%9QqA+6$5)~6cxrdBB58&I+EIS^f+&Yn4Z1!*=X~N@(Hn`&OhkI z+R$h7s3)rgs&@L$?;U{&tsBT49R09Opc{_f#^&3P__2rV?6Og0wX}Jfh3Yfjuq=q_ zG1>#Gc*(yBGhH#14{K>YsvTAkcl~4Su2OBXb2Jzx%n;MM{TR}HZvLmKtk$!#z}~e_ zTT8EvkZ*kNH~9Uw1;@A_v#7%(P?Fly`js5unFSx1PqKI8 zYg|ucyYaC$kb6YpLkzdIF^|Z+lr;mg zeLPSYK>xeZKqc2)lRqM75>*4cAMeql$IzhzyT|WV!v=hniZ(w1I1Ea^P60R)JrpsY z#}sWR19>03#sfMDKoCsk8)N zl8bdlM3&+x;+C>r>5U({t(Q{8DwIjeHQQS~O;^RZ6a0qRy{lgR0tK)B5Pca~hey!% z7|V@jBhM;M(n*+KBD0cFFyJ@+jf z-aeGro1Q!#uovYMC4RHY69Ev?>$ z6vCjx)91S5X%b zzO`oG-C*yp-VGI>Gy95MtK;T3>WAi#@D^E=Eeb6oaE;1QO^NQOCQlFT>P`|Sx;Q%P zwmHUvbg&}jW<73)ZB{+?1$xE<`dp3XUcSwyE99Atoax|e3k@1E<7vD+phgTHA~eiT z0QtdMWxZ$J^~%DPzQHqu)OF8Z)%UH99^RXgY3eZf*z;fcIc3=*B61rGCY8)ZUHrA| zDxbCTx#WXKUILF+dj}|a{AGyP7y@5GqtGr_=ZW65AKa$=q-v*XaZCF_T1o_;u+0MH zn4_QmfCspB>{bw6ed1y6Dmqyx=6Z$xS0I7cfBeDs0MthV_$g$eHh_k!@5}_U70eq&R3-b>oIj(htycw)fMMJJC-#1mNJ^cWgKWgqW!9{ z-b|Z^Vs}!X*Nx?2@bXq$CEsTihz{BTZ) z7h{oc0s4n>NZ@8#cZ08kZs7Fl^aV!W>Qrq?PN=9SF65b=i|54Ww0lGC?7xG9)k`!V zU%7J3%lc?I7Vdt2g}%qNW6!h(7i_D9%i=s{qVvm~jq8>NsElW0yc%8VerX)1TI&n+3SKzhM#26OovydSyPP+SN{`;;Yg2*YXN_A=RZx^Fkg}Xa};~--S)z4^bN@ zT(8#?#CqO*Ai7c=jnB=>z|ldrwoj31Yby$2KfX6eVFKnDO_^c$M!vPn|jwo*y zY?X6EiN3w+mtzuz*Q=Xgi4fq2A-R}5LX5HJym#DC_=!Q@c*#pAOPGD$(~{un^H;Bn zawxEhg-meWls>!rBw72xn1NjAuD?f<@_4p0DMd* zf1=5fS?7aSnE<1imFWzHD|SexckD%vHSg;Fz}-5*v6wiG({LTwd<=paksJtqk>|F8 zhre{$TEjbSms*{Q>6{Fl!(3_ZC`OS8dP#^^} zP6T5sgJl^))ZB^jLQIwVULo-|@>)cBw+`mlm^E~_O<6gi~CD8R8ohb#|$F)LQl#P+N!)fbb&Lr8l&g^qwuqU+~*qi_8jr zvRQrSiE48JK_u8L@D(i&#}hP9wnYF1*GmS_O|w3Ds}<(jMseEWcjAg&Uh@Fr=s2&3 z3vrl!)8Wa2`Caj(`U+l9@Osm|h(pm~`!vRnV6Q!o76xk*ku(iyV@eVO1v+tlDy4rJ zS`?j6%F@`D%&YL8zW#Ro7iV(;lglh34{>$EStuWPST@1z6~!DlhKAsPS<=p3TK^U> zFZ>zXvR@Gc>UaVTFzNL`0JbueM;Y689hG@i_7RHpov=iFEzG~oEpFJZBQCADW-rl? zg)gajy!E~d25y`u;A8;w^Y=a18dSTLRYoV!Test#iB{DU7KkE%d>^i30|_TaXN0q{ zTFyfn{UqyK_l}xS{3UAd-yT}D%P-uT&eeS1h=xCk;QS&x2%7ERVW)*`u?k^jfS1jb z1NP6@1t^3cikjX9O>v?L>1`|34eG{)N4eftC>ZaA@b1Yjt))V*41I01c<@;CD8<6w zI5%U!lwdLDwP#G321hk`BHA!OA|G!N?!P(JKViE~XvL_kb&ooFO+aR;DO&7MR>I=b z#iVAPBXZkMhxnXcUZ+^hMi-r%>ED6VaE!0dN9eHmJhvQ(g?%~ta~E8J@y%AT-AXSx zQJP0xk3Xubt=6s3(j0|_UptGrIFfslInzBKqgPjW1#=1+nbf66eMH((rkRw=f-P&5 zVXnI>?NdMHZx)ZdUunp!=Mt)w=J~jkSaIqDek2*(+#n(i-BNBh)KiPijwrccCDQVt zEQV0`se`+urBabql60SfJay_am2%u_5 zZPJOp1%Mc$z8q7tw@I7g9_GeLq9GRV15dajTh}L?9rKf(_la_-j)JD3is-TVR{jH z=+2Iht6-h`oyQ-MJHFLYjDLH{`9a`6JRY~Q&p67oCx~McNB4V39#OnQ{A$to&ZzKG zCESZS3(sA&byJvpR5)+aVSWc$!J|GR>q~SOhP}J8&K~b?m=uIFhDxxAv6&pebh?F1+y_~goP*71&qM{&zAfR-J zihvYBrAdwS8bVD7Y2PGNQFkr-zW3hWw?|3NnVB>5pJ$#nB{Q5)oSONox4KwaK#;aJ zv=)M($dc;C=${o*Ng$ComWeO#tDuhVH?Od~=8HK`Oq5&?Ig? z;O`;dqLKRuzSU#imFDKaEBQH}4DWspc&Qc85Swkg`mjvKWGl97^DP?3^Xk&+RWmRFIN zSCN(n*+UbrLXaT$TS`Vo`rc4oIUESOH(|JZKJk0}Blmy`yi0(Of2a-uW9!N1Hd;@< zlVj^Sfo~*_ixWojo_BL7zwO|gODEnp=M#qvP3P6kL_-_m<8(kmyeG)~Pi{I+1#}vk zK6&!wDU+v9nKFImw5ii(&J~_MU3l*N*|X=)o;`o&blx@eHTW?8lW)efX)}ao{2?Ut z#~dLcp*h?Sp*e%E%>0oII4_}@lc9UiDSp1?(1e+M{4@DDRiKhnIB)n+0Oce;-UR@2 zFMhy16DJ7>PM$J#8sGRxK4=2}aO58l-$XwC2@@ww5)_y`iGR8*h@8nkaoH^CNt^bX z&R%}}gp9zPYxko!i>;We`^rpK&f(+-4Z)SZdYv_0Xq^1K2QjBsZSm7L-}*WjuORZ# zpcc~|dpf<)v94#E`1OZpp5J&=RNwpPMMiN$pP_}**_$D8nI(->MNK11=X3s{@mZx! zw3*O^2>|Ux9wtG7N%A}h%cN&b1PG2VpFK(D#I-qG1g~^Ibjq$Ut8qB#tD!d+jg#wI zDZm9FxJv%@M*t#N#C)rP0v=O40^$1z#77};%Ax7}yfSA(DCo^VY@g+$zQg|16yky6 zzKEz^CtJIc_6qVRO7(tXkH|d^R7GGobD+JC`b5~9Q2iDp*1V*-xjiWqSs_a~v4G>3 zEtk!|aFjnPx4Zw*#aY++RvbI*&Nk~sFk(579S3TaBQ_#8aiC-rmBJ1_G<64YKqHU? zq5PN@CP|+;kfKNs2fCFi$ZqFA6~uunNcyGsa7O_vnFBp8-6ZyArC0{C_na~t7Vpnt zL<;vDcjZ81l6?H)W&E0z?5m9&sGyz$EiiF>#kNDLaUf|oVOALjiii6anvAVc7gnN4`KrwmQo zN3A-*fsPwn789S?wa#~$;iX5;4=2yLSw78Esd_QZrjoSxDI7{ZpnM3qyrxKF$v)-8 z`SHaGN&H6{ijONyLn|q}xGJdU6~G zy5mb|gquxR^pzjVn2TExmpJc*i%jSfIZ#a?+`GToNNo&XL`V{@-8sCvVnC@X7tBIH=%ltl!3g4#MnBwuRbw!}2FbF*bjcc<0d4*|V>jdmlDal|J`GN+n0P zC4*&1l+927Gks>Q%=_Gj`#diRvnIifii{hQu~)8`>`Lt`%2X0a4|p&95&P%32nO0@*MQ+OTwtY(X6(BsZeqz+9jtfs*Hi{46n zpSDR_F*jcx4z9+LnF%=QPUrp^jzup$$9E{ zaORnt)|NfUMXlXS%2?;tzTiMgr1Q-1o9`~}3suF@@9o+dz2L&j9UHdq^C0w~dXa3V zugPr=bRVYG`&%;?BWpPj=fhY=pr}W9suIq?1Jd^JUbucX2RduZfj(G23SK9MaK4WY zCoPJjd85`gGzBTCi~FrVA?>ZzvV^sHuVc;S`UWv*qs)2I>w`MS?gAcsq2j~Jj-?knrHEm<_CVV?}# zFGbadvuALiC&vgtXmOSt2;;{-ed`&l=ELaXKo7ewtnjT{(iA)pztL_{huDFjBOGXD zP$p=XzIjQHK9t3Vpnc{6{iQ?!L|zvjQyqQKc;B{6wpR2PHLp@Df1t@!y7v>jTTdum81BE8qIzG_c`PQe3pCT%$jbT~_zPq$a z)uqw2rA}4lxLY{8A@QwgmeLNr^EXpeQX}QbZyzLA1lt_7Uph-^>p`~Qp~`mK z13DhO>OwRIvnY()iUvZ7pixXZu`E4UE^9H?s-2MU||n*59d$qRR6b-T6`R8w*_ zpK_pVR|Fq7&ogH};AG0ze+=-xPpS8rC+#?|_)wmF`Fra8n|C*zN;Z%;y4~-jj#hPO z5I-q3^N)qcm#*1aWZZ?*&A9xUzX+4@<{*L|iQe@Tt_51G;1X)X!-kp}RA_+sQFf0w zE$gFMo1gMi+|Gl1LVs*3`olxtHEl*t&P4Qu3`yTv*NK^zOEn02Gu~eCesCIYPLa6* z=PTBt*2We~rDfb~4P+Lk_QCpWOX}iqBVId2S}LqDyVI4Y4+J%Q!IbX^s_l@V7XB}3 zAL-Ab+GWSR8RbXdO<5f1B4{8@gHF`FIu4W}%YL|ST3kA_1l5tnzEq&o1jy zfmJ1LDQ@m(DLr1kHf{OpOG*`64727j(G?s>>-e3XFcn`{aat(t2J*azW=G8FElPQ* z+X{pqMdx&h~mMK7g~(0q@S@0jy1ILM9E0L>+R$o zLzK>9bGS)^T~kuO(q%HMY$gYCcXP>Yn?Yett`mv1u4rTX*5I)hY zQ_N*Z)|B`C_;VBMik2epy9J#B;;q*OvksJ;d|KF*uP|4CLyL48r451OKrM4Iw%s43 zUR|8^aqsEn9(5uc2p8OwEQv$dX0;Zn)d*Ywg%d zPaQ^WWB6bbsdar6mYrEKUyx~)1PjkSc~}P3Yxa$%@#|M6WN?aqv1#&wCH^Z#cH?Ii z8EBf6HrBzMxz$XgC6YARfp<%5SWh@mPkD3c)KcY0Au9>ob_x6XnU9y@S+Ws@Hy0Va zF}n83>b+8bDKKs&9OxCj*}zoI*oNBAu4MqPKSxeyi07 zx&rVI?clzjRZFKmU55In!-z_BYe2l(jOq@(!y#dCg@7~-}B1?kM z1o{;#ZkHacc~PSFvU}PE#PKz$8&G&w26A^n73#QVPj{Go^K$_vWX}w55o4zjyNKu`UmX1g9RpOGxsn_2d8YTl3Tz@mFsD1bCyM2Dbf=`Mh({quJ zUM9XfcT@Qj1s#rCLM*l@7^p33AsI>$`ZheSx~;Y3IN=yksJ3ZiaA@+ploE1fc~x32 z)-s96q}~vErQn_tUoHa|g6@nR3J}_V>_hVGKKBQUjqqLPQC9J-ovkD*l4Te*tZ#|V zBjQIkwnt5FI&jze(XH_Ac%j>Eaf=W1ND3Vn@V?g`&>0wy3%3p!Xts2&A)7k=uPU2C(v)Wiz0_gSm9!?t} z;EndRNnXw+9&T%%sMs0pjZA9xhc#oUB{|LoVa!(p?(va#?;=~H^x9Gnw5Ew@Ggc;$ z-!g>v%u6k~d{H>{W^{+63Gjd><^W&WfE_?GeDr$5B;$_5>&OdGExN`^>4dZLA7&mtl=ledMCZ#Oozz%Yly*HPtH(UByMq!P`Cr-9W z}FU;1E;ZJgl1)47QyM!0I4JmVlMkhqlt3B)>}8XJfP%>$X$mkh)a zj~7qByq1z+;AwH{+JOi!it){lv|BqM{p z+91A2mi#XFyE=u;J_7afHTFUh<2V?CSlLY2XFs5*H_%tit~fIwdFgaJv&jgZ-krWW z({X+WO`uL8E}1FV6CRlNro~gIHGH5$Xqt+hg}{O1-7WVijtYAARu3q<>(Jpz$xCxs zQ8czcTlj-2j~b1AUFxbq9=cj9_JD5I`_EF{?XTWt1>|zBPaJa~U|BoqMV#?vFeXtt!FR1&2n*dp>aSTEvkM=( z@A%HMWTJU5Y#CQY$hRpcJ+~=;p<+~G68^&TSOJD^5fxtTy+|rzS))nep-UF?E?cl7 zV^aJ%kV3*7U%evFy=`~p98(x+(Vy17YQ0zGcVD8f;%eT2qMNOIntuf8!P8!!r+4KO z)ZE%j-Ii>n8z0gL{&TTbvP|H!%9k6R<3d!3O(OGscQ~soMQVpDwvu}-7P+0dyX|I! zsg0;^W_9KV%kZTf$lwZ)indF^Id7ZQ1ly1Y+ln0ps(g1FD5c8DzwWwiRR%{aE-9w2 ztt{P_cZEFwIu1Jz=eq2&xfJbSm2(t0{YG`f_QFbje&tH(_ zhkly)Qs5-*Zh=!KW4~|ELkvDB{$tYW?AG=cBfJ|sw`fgt$@4PrEV*JQ;d`ZSH1&}7 z-ZTO$u3*ot#43@;y&c5D?eUL#10AfgU%rd>H!syIiwh~fYu9G`Brhi%9o~m%?_1A- zGz|=oe>AA}FG=#zQuMsi-WwCJ;D(Q*ZBpR=vh}t_`xlqmRFNHaxMN@7)vtO5n;0G- zeC!h5BboQY+tgmt8g!b45FfgoqMm3CUcFbqbEbE>=b;a=bQ1QX z@sz8274@Fx&HkP)ZMTK9Q86mjGhZ@r3v1Vrw$629U&B6bEX5e=$Ko7RoVO2zS#HYA z4-KC^ju%o!W3(%O9=q)mp%Hfw25WB`(R}cK@q|f`psb0RjVaoCFx^UCxqME!h(MmkSL@FVIVx@2-$P9f~MM!qy|sCY&P5A?S>Fd zH*>VI!CTq!vIOx{c&RnCjdu=GV*@vf2@>FAw8pc>)L6$JZ-uqPfp`&aN)3Cw-Ebv9 zCCwa09&{~n+9MAd4t5%&4|e#`hh1hixRIFMmUyR;hpo0YTSguL9Y?d?jJCEMBE%q- zAYIMP8UPKL+JPWlb5T)qtfSdR>}T#H@6vRz`zma+&DZHRJDBSm*x|Q{>DqAHcI4V@ zV=nrwa9x}Yo)^By#b%?~%i;7>oiJ!T)?te&-jrJd9>_hGdvL>C!M*3E1>aoQ&@PO{ zU^e55)dw?2-zruQZ8I3Y#{s?3n41Id`UCBNwKqD70gVed)XL6sn0!Lq2wi|_GZv4> z+S*|4EQem1J{-e^FeZG)aJa6O<Y!SVt}|aM_`U%_?st3t9Wrj zLxqAb)Su8Gzs=xsJ1TZWQK_4%4JA|~b z05LY;6U>2ui*G38{_TK~5E)+he(*V*hCA)X2XQ%nD1?`1KX`_?lEwMv#ox@0{$>>C zcj9=on9R+4SPmy_1l0t8Lv&lpz2nW?4bpKT_mN*4d<>%JK0E}$8xI!l6c~uj@*;t`5I76FVT#@4YqMS**I1oGw z{0Z_Nw6=^XOVH8*>uCQ)kO0=f%F=2?TW;0k=B~pF19{9ab;M&eF?JXSQ#{5T1VH+( z_QT44>R@7S2scK{)>0IF{io~C@91Fjg~{L%|H}|Pe; zE6ZU@2@h6&`*1L*xu7}5!qkzcNT)bq9Pr;r zZa5tLW%6lemKs*xwd2xy3La|@3^EQgR#Q#20V?gQ(CKD8 z75G*7G|W_eRw?;s8pY|S64Xgu3R9;d+0xhMWD5|I|DJ`$4D5tny6fA>~ zM@q>eWu+u!WL4y(RODnthpy$IGzW7F72Pe{hROm@YRiWz7iP zIU0!>TDt+rCC#nXV0CTnM-qRNu458vYyWjpTicOrad=(KXx+YsL>*#>S zfaGGsMd9#Uoj`T|sG^QU%xt9j{WfZn{U4`5q)VywB9 zh3gL`)o{Ro9EX!iDN9I8OUNkdODU_!N~_4pt&@@-$(5HB=tSU94olQ%uSY{qPIP2Z z5f6p7wu+V=4sU9Q#%OI(TMjBAX=P=uA|Hs=Ip#NfFWdquR=j-wi10Gb_Zi+Ls!daQwV03`V;q_b8mTxn) z!C}6UMBl;G4rg!bfU!fnj!(6F4`2YC+H&dTqNCN_g|Re6yK3#x*VtkOVgaAXZ&VX=BX+XxXvhS{fQ7wHefupgSYF z8fY`6HC)XF*b;Arx4{gl!A&;!p*tHZt};@wF|`B2=OQs=ZkMYMb#GOXTped)1yb|w z246&u$qq0DXqYV+2P-Fx`8Efvtte0bs91f2aW*R{C@3i@Ye*@|NGT{uODjuDDQ}aO z1-CL>q??xyy$e`mglV`;XAWBUN8cUvy?%v7kf4%-wb^ck0}9M_h(vmre) zG`9z)==6eD<`cXE2E?& zH~g+M=;(1((O9JcOs9;}u+kW;6%K2GcLqGZ$r4ENu;S1f6(~`lI)<}E9|f$00hS;6 zJNRXO1HYn*yyE{fejv!dOn)U6d8z+t{Bpm6Us**)`hOa~{IBDel~R%aAIGoo8~Dc> z{r`;o{_|>Y#6JFp()bYz!PU{mIev^Bg*f0h&RTi{H}2@A2h|o z`gp8G8Yx=zE88?`fkv8wYovapz`wOdo0b3G?Ef!ln7^&ZkNLfWoG{E}-*xi;LNZ60 z?7K_;hg)|X!|{ByL4O4~-jxB*T<=!f)P?Jo>RGvAfL8%JM=H+11(lKn4k);|`EtXK zsx4OlJ$v97C<0f0FwD`;3K;)ASTHV( zX$*#nu+CqHZ{|*`j0qlBCvF|SC~*jdp0g<)eN>Y>823#agRa!r-aB|vQ-wEr#|5vp ze9PcaljvYhqP(nl9{*4(Zl76gxfIu(=Do`E#Z3V*zEN#%hfh-l$HO$3yU|fA{9p${ zQ^l6&z;h=|hVsYrvfiVsqp?*_Zzv4yY6^U_(T3sW$n8=BYJL^&$|L4i!MrJ|F`;9W z##aD&bRR1FzYS1YYNVcj1(8cJQC+UdppH=J zp9ZSw07jpG9_LpS`a9nI>V=;ssRj^<{{ql)l>TX$n{6C_LDIh>&j>xgiWz3o-vv6P zn}439;|l)IB2|-?7u8k+L=ZJrQ0S9x3|huA=R zC|G8!r}~vL)licHm;qX2H6?if`4L`N_H^9A1DV+XFpjmX~we^$#%v;N0H?Jf2Jcj=;aky^6#9 z@XO++I8NOE!+?R(<(iBU1@S8;W4r{7Kpr&#Uy9uKh&-D9FP&SixzJXV<%u0v3CsS1 z+>JL5Bba4}I2p`@e4!@3qlQP5|25>R;rG@nSzh=I=uOEk5QM=-&c7o?rjNfPrf{+(iPnzt%+>?@9hW4DqA*sI`$fE?B0i45<0FPuh3t?ktuUmRehx5zsjUWVGf>D}z`vl~#+N@r;qiue z81Q!(Vnk2>H4pPN1^6&lQ{V#r6@5G&zzE{;5yOZThL~u$%Qr;g(H8j|$XuoXXaQud zs*w8yH&cF`Js5#J9@+@xaW4!(9&N0@hRow*z#Bu5e*y6*amMlSD1}C18F=->BUkdjz{}%Vc7)91o&90V zUm3$8(4(C6*Q~))ay-yH)=>Nf%%eq(27)!mU@7_crUa#baY}G-keJuu2gLnH-HGwaW0(_0$v0Th zAx8K?t;W?5JT~AB!~Y5w3=UC$H7qzD)-c@f2-{D<fpqXOn(J;Jgi~3-_gK7 z2$yFwxS9cUNPmeL#y9$iUKjx|*yzJ>$NQW=#|*%O04DA?;EwkdM&OPk`#%GBgu0;D z@n50tsNMY)QR7jUH-Pzz%k1j-f>j_IcDGutZ|v)SE)N*-TxgkjIYJF;Qkk^ zgQkP2D-LaHGd$nRGaQ2pMSj&Pa69i`s#Z=^vB3^mRtkU4e1ApZmfllhLn^9uw3MEh&1j_Wf05K_?l{_9M?>>&J5p3;NO|LeMB z+?(9>Yvb0cea}$^KUU-VtRugYQy=~{+rQucQs7?-{7ZpXWV-SeO<7N4c{AZFJw7*U&OsP8@#XQ-aCLw zfHb|_^p?E)3Ece&=9mrMFXrCk!QK<#8`8kqyYhBp5as@$Uv%&%v3jmJJjNC$s%3}9 zI@n`}_um-A$9)T$IsUh^ywHE}gWC>++4bz@(U)&t-e}lpL1;$@r=cgl39=CMch94F z+?fwSI*-7h>DCFd}8!K)AQ;x&2-C%b$fY|+PU-KMCC zUTt7#v@quN$J%c5Q+U7AnBds-!n!p|7Cnw@l~uND?EnO~JZg2!$=Su#?abM8=Pz8m ze&eS9t=j<)ABBWI4tw(aMO=JBV$z$8%&fQBIYq@KrJu^m>l+%Inp;|X`>3>j`T(dt z_soci{F5e5oG2hL$reDK3C@g|BrO0fpS@|Xpp5C9@n2+Al^&n=I`~rQ2t>)rWai{#Y;kkGXFph?br_b;J_pDiX{o!-&;So0m zPmfrwX=I@^a)89>=@Hxa9d$W>>v6){PtE}n$ z^45Vb4|yn}JQ;}ca`s}r?1POy!zsxm2M#ok&3}R|asK7S`zPL*E3QJb^6Yk<6%z&# z9$QEq6w?lDBYS_n-TrE2Vj#`u{-+3+J9VP^?`QA0R_`+K9&Z(66IdWkK_w^%(@?=+ z|JxZxJ-KWTut(#EK(Gla>zU_EQl|y067GBmmwAIt)NSs9xHd$GGYjlC+;_N;9oI+c zKSm5P>8aIZU#=!_Ao3~7_*dLnXjVQ43PG{M%i)sh)Nd5SU{5C{`>^NuFr>jb3}nA3 zI08;2^@7b!i`{hwDoTkpPuQ+-!3D&CtQ*XM;)IFtt3Vpq!twPr4rH-`$ck~K^!d;> zuy1jofha2ubij?qls73r(!0bs&^D|N2co{BQTk=QI#B((sP-&pkjd^;b~L*QE(E)C z8jw+K*u!AE`*3;#2U1@FH@|D)K!<50dmPMC6=HnDFa|-V29BfK&sd!BzBn!Z6J$r7wmNlvy7f|pr@(s z?1fQH{>*-WJyumS9S#Aj63brg{UH4_-&%mXtx5Q0MUV-T2P{aGXqRE&oMnNo16+Y-{ zIpEKNSv?$R8MX>eVTZC0G7*nkRajZ6Yz8}u1Lfjc(*mP7@9!ZRjKFaw4IfZ#PFOab z{fc&(=s{v?A{*>J;YJgKdgI;YWS@5(U%#aD0C_l~Uy%9|8M{p?m)qh3*Ydv|!{IcrJ|s<$V$6IIntt2SSK2Em{U%x^^1 z7AP|LX}%tHGz@j!)%HXcaN?0#e^PWbNx~~6i}kUpZ*IF+tNW=u|Kv>4Gj}ZeGiV2j z-iBa$h(7*Yt}ykmqNYKeX~nHdlCddm6_ZUh^ehn&rRspNPo8H}wIR9_95;|aRbpf_W@6e=vh7S_JQk7am25o_bSkI}oX{{(ZPI2*A0kkL{qbBv9n22io1i zj{r{+@=1+L;@7NZ_Z%ndT&{4AQGl5bi{Yi84q)rN(Cy9r9Oy*il|d@ zpYT2Sk`3G^$ARVprxWf{zY>+`!*X1oN~rGyp89NJDWFq^cJ|PyR8r`9$Q@N~=a8AN}oXCYg|UAXWdONX@?ev-OvX=S&1 zUp6hJ22Ox;4cnp|!O1vYFU!HXXu@gg1{m2zt70SFH-txTKnR}*4bL;cuHE%W5)s0{x}Qu0S;)LKGr3 zt0PzyO~Ur5h6|oxD}NgTwc!2EP9M5s33ERszny$Af2WoCGi9KIJ_+5*tZD6;Ypk6b z>k#O(>{bLD^eE<6^t-93W_7x$+9K$PYB)zTnpiDD*~?B@`iX(fSp*k*;t}0?KqV9i zvO#aEENhEbB}0p)(dbzRS1Q48?9N1iPE(i3GxfFYUIi6*Pu(grzQ`h%E8BLR3$!e5 zlzIOAR#Jqzx=(u>>N1%X&)(-%_z6Lgr--a~FUw;R5*)k=k~Z~j5H5r-_b#VKU86;6 z#8fM=(ofS4S*L=7cuWB4&H+W1<=(2pe&xeHNn*Tmya4yBp}K$xN%1k^6#nxp*u1+n z|8Bcbb8fxV0pU3^H$FX?lw&@_0JC)M%>aQ2xr+~C;!k;s`xGQ1Dom{C=~bx}?q$le zj~UxNJ)^Y(63h$W3p%;DEI&|uRar<)-iB92nJ53e^F9bdXml?TMzx<}1^|kpfF8PU z2oCvZrQn>I6A3P;0W=xao>Xv8ae&D%1^dr$12)nboUjNwgyx9)Qf61b4r^*$6ELFg zw6QNl!BGWqh+aIFArALAQmC8Q+He8lG&sp5931rGOoU%*(N2L*xOyQwH)4SHJGtX` ztJNCmX&$C^q?V>Kq8uq+ZB_21B-%Fv3QGi!b=Z~~9*xVad}p`z^Ax#+#Re9;L*BqS zk5*1PLKXd#hz(E<=(nv*jXh*yLib#c*!#h*CS{+^v4TX(<|@Hi_JZUrjmUMig4w%@ z4j+>Y&DY2v-+1!IHgv$`Or?DPO;o#D-T}=jJ&Ir|+KyV?O6>0Ibuyt|d%qAEE*+co zMkal!=jjtj*_+fPQDuU3c@CuNH5;5d_7sRwadjr#Im;8M}9-BS7%&SqzoYk2|2cWJ{s={_&Uc8?Y(UJQ)q}R$-eUAxvSitC_a??AWBh% z!VJn&)*H1MDu3<&?G_$u&H)#V=avy^1ZZe$lK##NUI9U&sRS2xb? z+hx6}w->Q(hRYR+$9gOdR5HQ6E`MiUWqpNLHL1hLhHRpGvGhc@ddg}yHT9lpkFCG( ziC?Ut^~@;U_t*4kwrzoZJpHO~uL+AgE0%sX_%XvHttGcQQ1H^T#A*7H*5Pnt-qJ?o zF||ug6gSB$gf(QyHXV$TF*Sks^)w6SAO7q>TxNVCZ$YK-fbeY)7{#|&uQ;Jk=jGwR zPtkhjil^n51SZ_uLy>fOBv~Hv7#z508NcR`+1gSLbUprwsF!BiLk5PnoJ#P^G3mjJ z-=dLFzyY&o>&J%p^$eU%Xq!n7C*|X-vkxV0dO_}ec^Gv&svioC(zKj$Hsi=qvo|%z zZ!x#2+?hE;K;~w^$(wxG_KzReAAj1;_l)L$;Bc|&bK|Gpxp5MY?tO+Jug#(KzUewR zSDd{aWpT9Fg%yNyH<)TwcxbHj;%8FWH(K-GpWIF>d0SD|^6+vxbDNt^{JV{DF|0_h zb7YxbbNKnL&`Qhv>OyyHhrG{;{KfaJJgmCrvS51t8a4I(=gQunvS_TvG-RtDHmP1( z*?CWibsKSaU9lYv*(|5sFM!R?1|W zb%%h27KwGYdaEXLQ<^HO@k|rzIb5b`yfT~U(mJVvo^^CO+_1)lPB-U3=ek>;#m}oO zAn$r%SY7$)vbV6Z4vUV*z_KCuy4$?_hiOobzZ%ZM#Ve&aHeE>nT7NG*&ZB z-bXPMJ6;j*^SGiw=(d_)ujyABt>g}Z20UTrTi^((I;T-MP&@Fknk9hY5jn^h>lGLO zvr-h?)R|SAVQJN|Lo&TOfg)^7E+-@gUktvMDtrA+RE2I=W<_v3eGlrfPyAwjn+nE5 zRz)Q1ElI)4vuVaXVeI;|sFr15gcNi>y05iqVB5BfR7W|kPqcdpY(Da;a>#K%&|Rr( z=;@^obhh@hsO}L9zV1`%5@4w+$5USj*B2d5CBkg=^`*2-r-O}kj5|8UHEZ`zf8nV} zm$#_N_9>3|^v z#G00)VMY5^9F5z9WM`4A60!bHc6-XGRj2%&GGfb)?4Pt)u%buZyzk_JpgPy&!)ol$ zjqbG$I*8o29$_DYKV5qG1@C`l%J7Fal-5ONP&`waipO<5&(=H$xkYA`X>2EC*!RRd zoBA|jYM#6Dq_BO>VlCLHgq_t`GgoBw)63)v=6>f#QH6&ux=@>ZtTHsql?E(=(==(q z9`)<`DiunKF->+Zh_WW?bUO0|mApH=Ds@w`!vn83>x@ci{_WIhGN z*sVG7`P5rggohjrS+U!K8h<<9PPeh@;iA`TC%;V;JRw;4zK_5Rd*?2$)J`Z7GRZ(4 zRX=x+R_XaZ@Qa3f4$NE<7=vik0D}s3sWiaRN3uB3CM@f7QWJ?StiwEpqKK7pO8yx} z-edbTFeyG{d#%&i>bwq*ifKO=NVxks>F^QS3;!1388MZhwgm^-Feb5}ODH;!!0xYB?YnV=c35@5Ouza^S^pK>)B|A+y zy2F#1?pPx9h3pJl@B3npRnp}vs!qi_&70Mkee*Hmjf1+M&ZSp{GmUQK6rJ1_wkB-5 z0m9doPFumyBA4`llkpazR@(@iG|gh~B@ax;5#-Apufo zM#V)=vm^&>@_B9B?x=u5@a+xXvZM6zoim1R0q3mW>L{wSPtZ}GoU%j<4*H> zO;5+Z-HSD}ggVn#?$)%xwC>2Xin%mj!aMwNpJL~odwj>hAj3{r$EAmMhPo3=i@$mR zJ6R;4oe>dvbY1v~i6`N0ZZk}-^+jAFhoY(-L}arP_zzvJ{p=Cm(v8UN4oPzCo!jp= zkYcghH+Yd>mVWUbbE~ze>?dKz!ot-*M*2QuZ;DTwX0nfDQ<|J#mEhCWuhM)aHRDR( z`O3IL19cB;s@I`yRR2dK%HYS9e(7(?O1OBxVpq>&hW{~agg=_Rs&c_}_1et$=+Hyy z3CC@B?%aEBe?BUio?^z*0StEqkL)mQ_laBD(oIw}VVuZWPP30qfo98Br8&U0s%||B>#E^iMrE6j_LpsrCPST}kJa{3MQWY14oY|HI<6`V*sQF4aeqp$N&A}E zzA`zJ&|X?lpiWirry2f9SrvyO(|p4cI8b;)QkFFd@A*1S*B9qm?S|9 zlfIrRVN`F0=;v0p8>6}orr1Utxc>0efO}g(C9<5<+O!J0w9#(Lp(f9J@qP2`5hnc} zbR`b7agM6n>!=hHMNd2Co!}!y6>%J>$^JOCj@_1k_j$>D$P-DfF$F z=_)}IgtCN=HyIzFyLe@UJXs(%_w<}(!qf|s48!;F`#*5Oa3IsY6mOCj$=2_BYI-?n zE?ZQs$Lph-kH`sv^pnEIQ8d@asA}f(%G88Aw#FRjG1?^dtmY>{gBA+^z}uh^9rDBe z{qK!7hhJy!BXreLo!LwJ39LedrzsoEgwl0cZ#mGThloDw2xZ1QINS0%+;o`(`Gs$$ zeejO<|E4{%-2)=R@p=S9MP28;yYwe&c(r*o z)$4Etv9hf#Dx^i8#kz9iY)?Xmswf!OYdc!y|3aFv&dT;F;rzS|+sl&3DVwKF!p3Fo zeI$DQq|15{0=)fTSnf^LM>!0|HRQ%P1%=m@o_|h-!<$Maowai)&ee*Ca{6ic8ALny z6EkVQ`)vH7uUO%O99bt&ZDsN89jQcvYi~PJSd;E$n^7H?tlC(2Y@Z3aE#}6J>q-gc zsKU2!)_{A44^EvHl<$7rEqMmD2bYM2)6L}(g)Omq*}*~v#CYODr=c>YmMrf*9!K# zN5!K&ge^QXs^7Rq%u+r1lBSh=FxY<7B87ppBM zy6P3}{kmc+>0Qswst`+@m$+p#MwYo=AttSv<2AK(y1XR=QKNi;RjWxt-^h|?%uen= z^d3t-+w1uN2oE)ynRLtZLe=hqj-~m*XPOl_&~py-%}>p}SJ8Hdyf2^qF24xzM* zNtasHJ3aH{aygI$k}^GQK)tit9ZT;2qztnSpSIr9H{TDxIYmFU%XM~ESp27;?JGM#e1*7K=dIuSiQY5k;AV|mQbDQde}xabT}9GGQj%aijKikpP|=uV60x2bXPDDS{O|~ja|Nz&Ebwtt z*3G@{TIaOL<$w>zso%C68>zC$ z2CJ$-J$GDN?@qp+`5M%pwJkCFZ98?gQa2XK@7{Jkd^SCyt{UfAnAWwFb$X zRK7TTSYYXd*0+({49}?^v$|s$;ZF0bkhE&bWd+(d)EqOMD_ZX@*DF}atXfR!KkoG> z7{1^AihI;I;E6IsN)nKwo=*h>iZ~J0)lk;+zUlDGrVd0WnLx2*R=Kkmo7_V+=skmb z7iYj!O|W3c%Y6Y@CevXCM!@EPl`%nJ)Rmu*0H?>nsRS^Z_;nFr{SCd{lC>!D68o(l zyBw^x5%Q|xKtg3~D*W!l%eJ7Guc_X7udt~J>w;g~tzLUfy6w;oD{K5FqXS!BhXnNW zXS-?GX=kK1AVqsJvpP`X?M%|Mh3&K_J4)^-InL}6%7$|fWVSrB6P?Cz69|Ql3Zbx} z4eu|sTYXUcYMgQZbq4)xTf~yDjI?o7clIlZZH)G+d~~dt!p(IPW7zxbR9{A6I~5jT zNn7CLEYktD18tF4$ojZqpu%DjU%=%T+UBeG1fGn1m$L{QhkEa-x`rTAEc`yL#QPaz zgo}o?*^?D)v@tcC7P&!a)eFheGM9v1syHLRh^SmcM5yBZC+A2D*x-EKtRnY<=e9R8 zyt8Ie^B05hU+YxgGmv@^bUz%FEb?enK z*VU+!>_WRFF*^Ge!E-yiBloF0GTG3wQ_BoKRvpDMZTo}WbG7t%eFd?0r%$~RlXXBi z?N;9<&#@H2&!-78b0UFWc;lK1(*m;HeNsPgAaGE%4r?{&6_RHr{o&xxe7ZOo_~Pr} z{{A*27*=9A@{iQ{2PNkp9*lo_$qNj)l*u`wSYh1+#s^C^8nFX(`Of!|Lz&$L9LUPg zF&}mzwlv}VnMH`M;^rd8TX5>{h?_J#QU2XQ!31K%l`H$4ERR}vg^DHF-?}gQte7b_ z)94UP7wpes6r@GL&W7b8zN|MXVs%ybO&H>asLzB5B}QI>Hs!i4>TI4*g-(y7mDhk` ziR@Q8+cD}~fe&NNQy?D4w1MGEX%J3Z!lHs16H$6P+jTum7LMqrE@Iz3TgncoW2?J~ zFYQ_v7R;r3bFIftx?((A8JJSG^oyPiml##7;yHxZYgf0tlR1+4dD5|WrhHiDLD(cT zC592@S*M_5nx%9l-7-0iMea3TtXar1VLf-8?w&|KLrI(wyM*YxPyO_6nRRb`ZuQc8 z@u-@7Dw4@q0*o-hMvCg*A zz?8->XT5?J-y`yg5o>Ap?uAb2h8S<#SMjm^3*E|MTreRIA7|HEukosn3vMaiBRl|D zJM|nrokGd>$jDVK&8@4r`-x>yp+>yvb$_ZQ|Jzb>bCv}ofs$<46h2^@C+{HjOj55d*T29{O%Z1i({f0My0i+l=-tn~t>-MLRoC#I3uD-cR>!Ac+2 z{XW9dbhz+YIhbWX2xk>+sjFV;DEGA-eRrN|zdc+97`JB`7^dy2W&q><49$V=#j`sP z;V5G!s{i2u+W&m3q`?g>bM`WwAK>c>lATp4*uKp>Rtq!Uph?#1wb%4MIih-Rrf{Ii zZ4=m?aBr7MVT%Y;JeWk0bvr%cZjWg1()6Y**7XvwBIL!YVlXx4aE>IzX2AVdJ%DCX zy@{fpEAeoj!GU(CTXP`o<6xbW+H4<}8$}=9GfIgf?gg>;E+*TwPrs~mF(x+aPJw60 z?1j&xY~`=tP&RpP^9)tun_k)U7Ufjnrk1r1e%5`av}4;-Nju`3w%u|A-g0!uB{)Q~ zs!uM)m*E!q`QZe?<8$ZEo$yfK&HJ*gV0UT9lZP_qH0Q&6c3Ph=U77eKR1wjAV_8KF zf^|Z@sSVlDOLORQG{JeLi@fzTVa4dt`<7IBHYiK$^q|-I}4tmJa&TS;IY6>d3v?_V;q2xPp!WGWkb|ly%vs?;enBCR&*cOzu78 zOnFnyXe)QMjt%4&J8=2@wN)lIonLIv%EdB^BI&AMXqO zLu0bTZp(k3cRx-0tjY3Q1w4!wX=K1|gk_Wjl!FOVvjgbJpl}jnhuRTAB3RC)NQgmn z;vLl*vi5vEg{+V45j5|xOQ$;rl?Q4FD`1sxWRUl*30rn#?q+h%gyP~nmj@3XMz6wV zrLAYRCTiYxrBHNI90;q)6M9_l+Gs^W2+f0ZNYFU9y^#^)(DXcTcOB7=q=R`*DJ&$5$V zg{O3`_GZ81qtXJFY*zciC(~Bnkmm^ zk>z{JcD{Je>q_s^E)$Y)Qj{no)uQ5x?~JIRHGADscCXuczdx-~uc={uw{SA^UAAL1 zvM+E-!Rf4XWi3>5H@*Bz8x}ov#_D%OoXxGiRpXOeN6g_9340VC5?1SnJQop9$E-A- zxTsrrY5%K&J7@CJs&JCzoP!n5&pU_Aow)F%&?dnX6Zf1d!FM-Fx2M#^fQ9MG@H~x& zsApKsFFI_FKWZF*Z^(2+pZynES@!dl)gk@FQeEJ(;0~mQqApogGraEzQ;UKa_i4!Z z0aUWj%8Ol!S?pHDQ;{AFvJ#Aof2Nwk2Y#Gz5zhUPL%X52C3S4W^Q@cb@d^d%K0k$u#86^Y23PIeYmiBWcC#jZX$ z=-@%utCJu)1btrTE!Uh==UQb)WksPk+;w8OT_wtBiIe5!QLzP&Xwf=bzb=}KW$T6p zldK454;z(RkMD<`@*IzL&{x_a%Z z`(bB8e4qG+LnHWTb_E zu)pzpqnjh8{TKPk`EwS_muLun3zCMS)!9)zOBn%ReU<{lmILi2(VSA-g4lGp@O>jG zPMERfR4|URI_bjK7JV4;S5+7ZxL?pXsYol@-Eay0La_9aWQCwuG5sS8t-_q0$2&x4wgamtr^%lt z@U)}&8gLl0#F*6y`EJXCI{m^ZBL)6oll{XBNdF7g?7!AzD~TJNCSb8X$-YUex0I<} zQIn)(UFsGMme9Jz&+<^S-2_JcD6`V&m6w%?pU6E9{7iTAN6j2)$E^fJ&*^wQ!Dt4l z1GcB0S&W1;3K-}TqF29j=|+`dX8CcK$|wpd zV*wN-8OK4!0ukvVM5Rdy5s(_fZAVdviWEhOii*-g5dwh_H40KA(n+I(q|wqya{FC+ z*35h6o%4Nb-gBPwuJwHD{KL{f?sApAfBUz8dtbjViQjMh-yX+z{}U%Eqon77{pzLL zFh<1pKaI3D-pYOc{CpF`H|=oJ zwLF(w>(b^E@WZO)!mF{diQe$5y8`C~B^0>T6pJgEFT8R6)<2O3is@~}D zRl5FBh4alnY@i%ikZ=x5ds8;GQmN0lQr|aTH=BFn+0w@^5rShu__ZT;_Zu^$o};JP zDE4BO9|h&pM^*%VqEW|$QJKDyopViM<9#OC2TursSPmTQ${YB%4 zTp(*UL=0G~x_JBW{;`p2M&U~qo8UiNAz9VyRq&07`l~+uUyk$nXRQ@ejGbf!H&|}HN$W|56;ocnJU)~;=SkY))z`bBQy!VhD>SJrgwaFBMI{HPUZ^qr zEa*(tI@kN^((s`~Md#i136Ck1xO+y{;r6YKpOcO(M6I(&>|`XHco`N%%4(Dx&UI2A zo7H7~yJdL%i$+UFm$SyEcI{0sUeu|+dxDr`@aD(VW$htl$;f=o-8h6NP10(e;@uMy z_5rJ+m&#XKBO{<-Tp@|QEAx`Ke_(Nmt;3-myZhbUD)y?k?lukxtH{!_Gb7Fmv(8e6 zipZ8qXPWk@A@$&EkjTLk6=P#nnK6fbMy(JW)tgsG6-(cpPSyPIagy&HLJ~B&;v%Ho zJo(vm&zatr%ba{*si~12d^q)e{i(Av&WTPiSaJx$l$1;=C!UL0<~vF&gMOBv9!5vB zBnJ&Q!srn@Ub+mCJQpSptN8X;pfIC-f39sQOHgQDf<;jz+#ssyy*A8=SRSgA!}1MZ zA$jGa^H~~^1xHy+du!W3QLA*(*(g4*XF&3ai4!OBHcSx|a(cwRkNZWN#{&M3R`)+y zANha5UG49Ce*`yqsleObOIKF?58~Kpk%gxnkWrQ@MP2Mqfoc|wLRQfsPVIDIEPi0K z2r{1~&4pW2vXjI{vx(H-jP~zVJO?#`s~SxYm1Hrl9@`w+9o#ZmkOR^V)arr&Mn7}S zTsxIP(HD(kSp!|HJ(8ryr0mTcoe@GxHwK6pEE}0s+qygqa3hFYcLU8i=s5$b;;^|q0oefa3SRXC$LA5l{PML}knEpD=9DXM(a zbyUFA-?CNbB^OPLEnd0C4r5iO(%Y#<0=ca!fap>z&Q^P``kl1Cq*VHZ` zJsC^JGvaf;6~N_v4|8ZB>XJHKQOjsM zNmf7XjY#6f{2EYSwLvx+av7?1`>Il?k#!{(F@9g)zb*KJIArXBTdI59J9R6ww^;SX!K^nSy4te7}@IY{b6zAYzOP?S1xrb>R3CUgvnCNCvv7M$`Dd!=lAN7t?l7{Oz21%X=;t8>WsucYZlRIF8TMv{%s!ik(#!h zKdQQ(pSx6E>+LH%L7WGruH3WcS1*+Dc5~S~F`bb92`^N>gR+H@`=zU)lzhIj`Va-l z+cEccbdUms)aPext^4VY-x4?-H=(U-;_gUcc>`(M$rEoqANZm4!eN#KBX%Pa=D=dB z?Qo@P5aJg(!Yx9}T5ySI(gL3(BKwSV+RkBT(`$M|+gghf^!&=?#`hAK%*EDBSB3`MrE*BdckVg`qmUDnW@O=)z)s z@0R>4 zQh8-`10WUE)%5{kyMtR=3P71y>Y-}qfTmM*YnC~xP8UH%-+;<60S|!6FaRp2l;1ah z(Qu#r3RFHHHoy&wZ&j)@zGxWz??5HD9q(Nl)%0SQ-?STn@)Pg`ZRekT=;|;d&lp+# z8&WaWLjt7oU@uv`t_-}wNwhj&`zunZY6D2+yoEG0%}^*ut9%pwUy;iDxGOE&2E-xU z`npZ`Hu6}7_wO9ls$s22FU)>QkrSRF=nt6$%DyT*Q)E4x+)DR?nR`HsvkBTmo4^UxakB z-A?S+9!11iT)gbUzCr%56f>M$zD>qOR!;uO0kp;{^xE!ve(tEGW%xdO-gxJpXX%|6 zi%!;s?8V(Vq-(t8V4!>B>X_Fs!@yLuLN?fYSSN<|iW_6szIdEqlkM71^|U#t@F5zz zJpFjiTsr+=XvjXG*Fv)5QyB8V1Ee>xyxIoD*Xbw+t{>VhGy>|X9KJ&I+g0~tP9@jh87(uvWL!qfCoZC_61rBH@F z)S;q34ERVE?H{xL!%!bgY9u{oc|cYbB`cL%RvK-%m#6AY*E`UWs;RzcT7p$Zmkx6> zBY9*xThA);0qM?F)DTQCE-AHoCSsl3-SRsAlcplJ!+)K-LrQ)gIXAP}@y^S<4=#3c zWiEM&S=U8`%q+UPC=`0Ng*G*B9>UgFNq@opB>9$?`~NW~mU6Sf<lm<8Ffqp=?&H`eyz zc?P+!GD!=Q8m_&me3q7*di?p_l(6GzPcxWuB6L7?p*b>6QvV>YxzljQ!pf!+({vPO zBv)Q(vf3ctCdSviuUK?2q)}nx!rY7LjIHry;qWv@8v=gmQ>6xN!^+s5cK(T z?n|-yoRi50A#BiyD{tJ|+1?Z4arQpyXH9uf^25ZSJe+J{BI&lFO5gAC515a-F3}!4IPuS#@X9lxET3txd*|NIPE1-Xck3}<;KB(-T zT?(gu(O_TtqOp4gHH~(=yJcFmvWd}Of;tBEK7cF{jRge4L|fsd-|zNUvEaWPo%Yq} z7bq2BQQx`thZBr49xP%n&1Ea?ay0Hu#2CM2U0Pj3>ae+=x58Fs0x@{bHGSOA1>EK; zylq8XBx*2icL6rzQ>bFQ7AWOmlHKsKn?v+$dT(VNu0IS_Ynw(kg!fBLfxmo8@YeZL zWdHyDfdhJaq3|h}^UStOPGxLcmH#>SQSNd4@tmlkw>OIh;fUowuXa?7Ji-qmc-b5@ zpIEv7R{pP6rlx-MDLF(@U%WI$cBjc=hS6+xQIz`iFGHtN^gWcY1vRO^0y??CG1SB6IBJ*kE2vc9 z@j*363p3g&T+`pkP;Gid;?%c+JHnTMa-tUR+1cFT2%M0J*&*9qj?qQ^7eBbSho}JgX=xw}rOUC+C z=82vW*oi)@FOKN8SjWyGHi9CoOT+e8#9>kUxPP)V|KSHuil@@)#SeA5B4YmL|2#8h zbaQU8a_92#g1D3oE%pU=i36dh7{6kfb4j(k2}1;NvDEw4b6RIXv#iX+Y>+d$XZeTo zt4WUN^|5!rR(Q>)byp-ik_r)Kh>xO6u9Ja*D}bac*nz?HZTw_^fg>Ax@Yzsk!0 z%P~*CVaLbfLD>iWwn@-a2sEew3}PjLmaYsWG?xb&yJRi7xqRG-x?stxnEPhl18MJm z{9!3BoLC$`8*Wj2qFEwEkW0|;NKZ@)!FKgh$Nfj*#%&!&E0DZkwxV>gNt<00*a~ica0|yY5T}-%bMdzHDSqpkwEAevuC%A z9H9L6l3x+?SXmUWUjM?!?o*Bx#wi;r^8&8F#V%R|0*}NYh6$PN6zg_+GH|fN<@Kg7 z8vpp4Y@V`JEZ(3rEg{6uzK!=ey-)Z7^3b zWhB^zC|a!{v~TCoXw_kQ%vfhN2kWY^*4N)tvvbwWAMT&9-G_4%&R(T{i$a?I`z81An+w|_G2 zm3Kxio5tl>GM-18zhgV$drypTYCHDdhV?IGzj$uym{ypF3UUi~0;w@KSv%@p0*~vNB+QFi`g#uIAdoz6 zvs_K@e?L1HcDB+4MYBynap)62%#)+6LDf@s)OM(65yV3O#FVd5GhqEvwE#eN(6#Z# zjHW6#QU|`2L54r5m9P%e8#Tsh!bpE6N!z;Uec>`4c_dR-3WoqrP22P%V(Mi#YF7I+ zU1ERRM^KyZqR|KDo`#Q**Ua<|If!=>au1$Te= zAhilX5W&=z{gqPtp?Yj}6Kd)nF}EV`>8$#puP#V3=4aED&vB_?ORhwS*XZ|aqG zAH{^_xJ-k_rZijJh*oA@@pv;ePjM%s|Aro$b?CV}5Dpu`(}LRvY#iO(bIXVQY$c&# z0mh98d7t%+2MZ;hW9f*MMcuuYqp~kJy3aIR&-I6=<1eXbhT;eiD_K|k?mPj zH1#0U5vZ6gq4cBnN?}gLn(}~(sibbi=YwA~P}5@(IWKeat{Nu>Jifn2Q)oALVV@yC z!e=d1O$^_)^TuNb#v`;N?Ir&kDmbzyxW#Cpoor8Bn$@yjHO*pH=N@1ERb=TLvV19Q z)Z+{D%dqCPvQT!~-v|eDrgG3WK~Z6weeykq0Cj3)Q-@c*lTQ{uP8V6I7Yr6SYj0<5 zG9P()xA*l{do%i#1H7ruXST>bWVo_JsaOB>2KSr?>v57fY7iD51Mfd3Fx z{I$RM%vklHr5~r#+kOYQx!#(>lJpDkuK1>43(ryiFB*5^CnGcu<`kD!BL>x5=%2d4 zMrO9~=(Bnv3`~|9f6?$nokLAL#qp$`QLSi+M;i2*2!yJmIu4^k`;WOd-#hl`!G)&s zVTURUeFsz<>@}AZ%*H2)w1CZvG#L8!#m+~=UT!N4{Qz6_K;MvW%n#y;^7zJi#x*s8 ziF9hNiU6_ji_ipoOcEo(_T#>pEJzUXyGGzo#O-e%{8fkWPi8Xz+Q9#LDg9O_@?V(E zzrGy*$qJ}{iFf`d3yS_F-np&aBS5UxQzV;jAnei02@6)YCMipk*qNBE?iX7I;>U^W zY+W`@e;;>6-xtAk=!_;j;Y#f??`;8XfN)Cd0x2NpHc_m%71z*u#89*w&S1W5KT2tV z09#*dg!}~>8x%k=T{-uQMt)Q&LjLO>3_OgFQU{L7)MsPb;QMOO@XL?S6R_hzpj!Pv z0hq*WVlim^2)@tb>kI1Oe`o_JQ|Se;>v5`Vwr@H|ZKljor!{xcdny#*S22603_&L{ zgg0pU{Y8UE@3iV6s(x4lO2)!yRd5;Ty!~brJsTuLE4Ko`QyMx&hNr@ZY~f@icy>N$ zoC*E$$v~ItOD&|CE)hxU@wKm+-{3GtJv91(s9r-Le$nXqq5&|8o7fLEaVw0jZjg#+ zJG$synh#N*$sClRJcI@}*EP_U^~~Fd+1*OS7Y#D3hKspPQ2*LtNSP|+N%F{0p^hcg zhM__|!5q9glb8z0Qb_AjGoez{7Y&SBsrIG;jQw_B!xs&i35(8m1EK66 z4^{T(AqH){@!LVE(V3}|JgJDW6l1ve&&(CvcC#NBk`DGmmwIm zVT*xk5IoF48Hr02qq)#3cnXR^0W+)%0}|=`FB)5vOmB#dHxgmSf%Oi;urUW`ct8-h z6ehtAFzJbDVfCz-i>N`>&3B18}Ir;?! zr7tFU_~4I9rfdU5oBpo*rxzb|UJ??Jyf&L!(>ih$yR_?c+GCBbG zV4TI0L5<{=Y``Ug*BjFmv{Z1_`@8X7xk?jkJ2vDsKZ#WliIlMJ&9!xPq{xt6jYHdY zZ`yKO)x)b48+;MU9H8Q{jRJV*E_3=HLhZ}h3RD+wmCSEw>}GNsTMtelw28J}Ic(=# zTK@XOnwsN8E%)k8vaO#NtS@2-)U(-LNVNmMi)u((2k(x;yf`Y5J`^inMMemZ06TN7yt6EM@>tQT1Sog*) zAB5oN-pON(Y%fDhm@K>dzLW6KSKnX#v&&a8AZR&?CNBc}m(g}$lgAEKnn66CQ6 zZT}ZhND(U`9NQFvF)c62Ho25t@Sz`+7?ol})->yPmM4SGJ2J1&wv)CBDDI7--awf? zK(rNYq!8GJ3pq})Evl(geOe|K~>M&|7|vO_OC%nx;`6SvyBK6Y=d!5OFzq3~B_ ze_f_Y;IYSgRcJY4iaDyiKX#zTT85#mV+>0pHI;NU)p%-G#sydP1Y0j; zr0CRhRO#2-qbIW!hR{yaG`A%+6XUofg{dfhu~~ZWbV!nrT5jaoSJJ~NNdF*gdcTpS zSB1J}RegHr_kj(Y^S0AtZqgB<;oiB)v{n}db|PpP_lEF!Us1f%xsic!c4y$CTu$k2 zaz4Xf+Vc65?y>2*y^j^XF^s!O|}Vqv*Khyw#$NSzBn#cW>Xp@B-GPn8#Vm| zY;9ab2&eweQ-+X6p-h_H6G;2(hfRg=@VK)ww|JE5oEVjA zO}g-zNwoH@wDK=0?Ih`yMZPy>w|{bU)N>x$xuWbjQKwwWS(jhtcX)%Df%#1^N#h@? zn2H5oG|bRM>wpKsB-!MhJi}6H?Vb`ANqMPTKdL*1t!;kwb}m0mVr6?;B&ZoIe0Tc0 zw7j&$ldd`C=y=)aFCq4naf7!lSRuV6sw{q>+x?IyBb93v zeio-iu{mg4E?eOJqqa9zNNr$p6S(8nu`-vaNxd?_-EcJ(j zqFH)Vof?5NYr>uJUco8l2{BCT(@7HNGHLE&jJ|&cR!G zYO-pygw$X}E;kLGfmsaDO=x`Ug^%sZ{Y3%Ao}q{D=Jj+}*MOMOCL$?4n`?%zF2`)! zRJps6#VtOrkce_wgSJzXiqvVu85~V9nU`Jane&P|^1?gnIc8}^d8BbV4p?W+J((b2 ziiuiaP(;WSiO4CPEr~nG)c&PChxkFP&nP*o~m>Wl_CYb`X{5jcI4S% z4Lm|l$+&$C(UDqq{*$w(>IoUOo!S9%S?-0=F6t)UHh57SS-y_8y=w6=`iKab-!vM) zH<6YfdQz&B#nLOhpH^E!4#8RqVDh6qmVMFd4vNo-Alr)in8}6B+ z_t%|_dm_N3sF!7qSAc3-umwB!uxt8y6yc=WHHCvsA(n^sm4_?Rj{fw{F1BjfjAm~< zrDFwTx$Opwb9$IGl~99p`V{rd=-m-7B5K@fu)x`;Zx#cUig}*PO#;*=Opwv2cVCM?wX@te? zajn|$wM0dDaNri?N$3i#xN|lSCwIlz)tAB8xHwVb@!gvy3G~S!VQl@A+2JT#M{$#K zcsy_Ss)gkg>4G`%Ts4uBo^YJ+%^ONrGv$z~1eFGP;0V#2;c1;vk>D!p6T{vW)verL zh5S{R+uQdmuH4$X=;fvh8UTgdcTkgqB{i=i;6Ti(oqAD<#<*1Z=L^@4xk|ela*tg7 zc=;Ij&g~lXNF=OeJxQ*Ej}id`Q6!ar&@6~}m8066FdX+qL#GJQxGc}IGe^qq(T>De>IwAw*{!sM?pJ0$W@g2TNbZUU7=F@TFr+g;p7EAS?&Nf2i>H5#5D}NM8`XLs#cZPDk(ht=Cws0O0b+YX^nsE4KK4dN54Z2W9i6a zkv|-`no@&2B}lrpb8Vt8wm3_moiFb%4t(lzU{OU@9nU?M`c~_MU)eRy-Vj>HYH-u% zmY$Xj#WTMsnzdT%uTgl92p5W$XnYKs!&7u{o(PVVi_IhMmSi=wmMLrTm+-8-Bw8J zJorC}11-Be0APH(2O9BKiN9ztOdkWH=!p=4uK?YXUCAEPW4XZYV@CU@ae=CKq1PRG zC@W!5_H&$%U<=(YSY1_8p7pi?ik+f)yJJda27?V9zEb#v@7YwV8(J>gGhU-ELt4v+ z+V0pLEZEC5DC^)#?S*xa6hRad8PDHv3jCK-|}NRDHL_A?JDB5*#=R=?867xoa=n+e&!3`lq6{*=Vqe z#*AT*Ka?#NH{(=4W17If2@6DD>xM3RwMP_w%`p@xYoY-sXv6^-*=x4_rm)nSAVhhL zo2q_TkhUbD7JbiY#YCI)Cuf;U!^Ofhg4vHw4Ci7K8|MvnCY+UhxArZN!CBxCs3-J0 zh>54f5$kT$+MQP7Csug%ycsTwU3NACgBBLtE;c#qlxejJBbDRCJ?!o!%^~8(;o=iR znwf$$-VnxLIICUO`JTiqT}M>lRF`J0hOdsk_MY@8U3Wu%V`|66`*ZV@v{Vu28D-L4 zgP9zKgi8g8^1R26}o0e2Q)!|7+B$DJ9Qy z+=6t}7kMVcpqLNSGV`f;jm<$nTK80#Lnn@be72N_>8Ef`y*sXVujB+vmCfp#!zhR- zjhfC^INpN~BN~|ly!WP;%Far>L|n2%m&eYij{`-i?$)#-Dt+>Kb(TKq+9S4eCT^*%-VTR;>BxlcmI!^~ z@^cU|hn74S1d`v}@RzQCi-R-dr?@K+t7|+|0gAc$zBc+sXqmq|E{=IV)V*xNih*#i z2E#h2^2e%&5dd6MeBn5Rj<;AmLQ!Q>thcVw71Fp?<-`{jcW=*eK&u&_TiRG0dIsv; z(RU>^+9cJL>hm*9(`G zv-#>vfQUcQ6=GdSqfe)Nt(QrIcI(`BR`XTu@fg|4Ra+hGRC8%zEQGJ`WZC{-YtQYg zllZkZ_2+|av-N3rf)T`|ynZwnc(w%9rb|I~(|HOXXe*Sir?b2g+ff}QbKG|`@!rp0 zG?qM3_;<8BcicUBOKY>9GN7-xxo$p4=3`<{{KHZPxF9BKC{3@ASI--Y`DBIO7^-v} zK;nEN%lXe@Q=cW13(adWCnK~uX0Dj2(2dM)W$w+)+pv4c7J@uxHbQiTI5K}?S}6f% zg*wCTcsP9x>O78?a=52wyeFX986~S5SYZ$I==e}?U8S*S-mm39s5_DrMg9enpD`0J z3T2?}9;VskoWL@Wz6~)*IVMSWd@q&!bnLanM^db&!w%n^q&O1cjvm0}pg6mCU4vw_+XlxV!BxJ2l>(S5F#mhUwG^T)Nko z4jd4(H|H%+j<%Spk@sK75PB+l_S8HV+!UFAw>kIB_{+%7KE?cMFl2|SZO`JWEtClS z8noBgSvbym=V8_BV(5c-2-#idxXoyIBg}7k)YN=npzz)%80`=dKie~Nwe;4qkfR?% z4$Le5Aq9V&tJ>}`+(q4jnJChh97zaO?UU1Pro1`WMf0x0XYV;|zyDahW4AIXa@)t} zdEd>tE*=Ran*uy!S-b>T8=(w=Eh1 zsV^U0y`wLjdK>WU0in!n>lXdZr7@`SpRdrnU~^Kz44@Sk?jYAxs5-39n zA!$L+fo2pj@kj?Cm35%-NGDjOC09?P%V!}yYGl?YmVS8B2P`-Dousy2&!f0JZT1Pe zU(Y|P{Ly50L}>{ZB-a2*nNrGWhX;r7%t2j>oOGQwf(8cxp*v?~QzQ^{ zVB&+Ifq%fKNy;&cC}1vL{X?2v@J+jn<4o7V{m3yL)z7GfkgS+54w&VEl-XrG)MFk8 zg2Q*55|r%1H}{4F?yUxlK@$`1KQ8A3C0l_C1gA2`PX=w08Zab(o=55P!A7+g4bRL) z@qal8k$4!}wbhu32@iYT0Mye}$5JOyvjBFD0<%~Aa*cG=>!<(d-4u!W5HOI)b9vD; zGmC-kWI+&05*tBBzQn-GniP;cntWw&ez>P9sC;jy(&L-q^%YlW54Oo%PC`{sOi(zK z^YyalfEVsR{_+}8aZge=L6_@WFbYfI$ytE#21emjtJiv|GdyLb6hgnJeU6S0mBq_K zXkA}EezU>fSYinTpC}Ok&VsiPN`)G=7=}YKo{ix7hhrn-n=eb;w<<1uh8c2FZxgP0 z^)O{!L4L*1YS7UC=Xgq2%jhVoac}Jaj3p?i27_H$0o0bW@gKw)s@=l zRzayERZifwUoEtxcL!&`PPAgt(2f=N4mC7ABlz0+#vgc2Ugz%a06ROoIs>ES?%^)9 zJ2KYUgq{ z#8~L1Z#U@Zf|nO_bXb_?^r{~}Iv3h5{e(Boo%Pl-_Ik5D^H4&(Xptc#bO4-G`W5OR+P<)U01(yilT^xeq;_$)=W9?*Syo)hKW1O43|I%V@&1~u-a@5U;AIr>5 zjaXI;`H%0^Xw8sw@a|;jpnEZZ8aimuqR3Xggcly98^JrXfbXul{ONBQ0Z+J| zhn>5ZTZbSMZlXOR)7FBFHpCYuQ##Y>%)A{D#PXm<_21Z zJYaZB4ThKB4_TPc%eeX8e-&ali}(H9f^(;>#EOL>H8)|GZp28q>y$@G$X)~d)0*8; zfBP)AY&1=5IQA*-W|Jg-H4&TCXOZNq3k#uVNIzH)c9mWP3G(>DfG%pOSZ-^oYJwhq z(UAMjpx_zLY*Cj1W?aILCoCh5E>jWVK-=jmiV`<4x<7Inrt|ujN-CN?l7Tv$pi`o!m@pn#pMk4jlYz9lcBEmO#k;me8v)va3dNM1GrQ)~w7%y+pQ?bL3hi!}?9^>3 zVtXvxoPpvjsTmdoqN{rK7FC>jhj~})Z+IXyO-2Poc*hK631`TwR8^qJ9I8WK7k3i$ zP>osjZ)dn~o5cA#7(t+YO9S+T-e)yURT?BjJkXR?hOS^Wt~hwTL-x~$%4a#14rbL_ z2WR-4?-6n|J%3! zDr#i1@ohP%d`+_X<@*;ea;iXVc=9t(ewaMHs6eqHxyN(xd^g^3Fj6z>lUz=PDFsLHg9wxX7&bLVhLG1P@Q-KDtjVaxXKDiLuj#kg;Rei;F z@jyqbb6FQXvSEsOI?ja=scn^KA}e2pA6dsOQ^uB_J2pVCtU|4;Ml$6#8*P`M8lg+k z5ECDFf03jx3w4kFI<9W2TVb0aufHc?4KNPvZ@g;Q6ABrqousWcW<2{{FZZ%hDK6+PKj9n&A z?^0N+8v0y9(PHQiuNNVzUFsN#M4m(72HOA62=o>8^i!4n8 zBeWYi723rNsi>`4U?-#E5%|;Et%U{8d#wtu-UxFL=9||x_^lF;UJEk2%SYF-dq$9+ zLAmxINHCGsH6ccVcn>dhDSEN*JF7NkNOPBNp4dm$@Hc+ImpJ z^=3v)wzs`Sb3cVMX&3f&QFG{cJ_q{J`oDanA4tQZ9uTM3%Kh8B;}hk)DP%S|mNNGa zTQ}}N7b(}$FnW#G;cypo?=5#LvugQTyh?rJyb~>J%`Sg;a8}u>Ara;M{N%}#+$Vv# zFLV8J@8^g%-p^L$<`(AW0T$oImIgi0oYobX-aM)@=VAhE3s5Yuh~Ice3` zSvaANZ^E}~fUUb!zlUnEK*rHa!Hd|b_TJ}kTJ>q*f2Iy!G`=h85vcw4Z_j9QApkek z=j)3D*>GYOsBjJ@HEEV1Hw_~HF0N7O@@`}_-|l>fwe2*_wRBv**&JuzDVP;izB3mw zBGd@)?W_paGJWN!sjLh-1ISTF=xBtfd>25tVF1b2DR`jY1!g~x%-QrJWue0`*^4sT zan1)odgkoZdwwRok-RmB>CT-Q( zfywPZZrEO%KX@zU`JC|BkAV9-9LH2hrpTkRlgFf}2K%-T;_9o~S%sI*2peK8w=7`v z>~46pvM}Wgc8Nt#|Kx(=GpAOhk|x9QH3-ADv`-H#Wj0j>c#|S39>T_d8s~5-s9)J$ zKfg@tGFfC#L&sov#*)*#w#R?v#-Q)fa;NlZ3_~zkHl>&I%)-FOQ4Z1X z!4KbQ4Tl1Unq*rZwi|nuq0(MMjQzB`jT~_hWeB6AWQ{eWNad&0L<}v@4 z(cBklDa(>CBoWLxtms(NQkDlQq|G|iNG~Zn`{@rUAgh!22|flj1Pw7i=p-pgii@iC zifqSrATNkbG3sGC>U3D`4{;fcO7@Nk7MAU++^)BP@^wb9F&>}-Tm81TF1-VoB&{|V zJ2YG4*H)w_{!v17+*=sJpkx|y+iFVA2X)t7jxI{1J;q)G=$5hC&`ctvq93m>2gkh@O);=eAR+a?aoYOOzg#`x|A zq-LR13p(N8S5k2y5;vg0DqXg}1Lv8io>U=XF*~zpoi9BdR3B@?&*5tIg%iwtt2$-D zWKQwK!z&aS1OkriZ6xNBLe28C3N1>_7&zQqnR3$4f}_jMh~+8v*F(3##Za`5E;Rh3 z#e=5xqA7Vet9zSL-cPeV5!upND_Rq|_}oKUsVu$snCK>QKXwGgHdnEf>EI30PJt7G z(Oh|-MD#rFiNY5w&|M&%R9$mKsxxaVn62JugzF5+ekF}>M!ha_A_ZJ~3NJ1GQ4a;u zv>Qie;=bmXLwTg;#iJ-+mBVY*Ixc4@oW9DuT*n8?)^SUIHNM8}*=tFMpC!e=^&uyr zdpRM|L&yE^jP_%oqM}psjmpA0S?4sJl?S@Z5Kf-f- zWFqeY)LHH4g#uLww@w^`_L6w1bYtzvuD9~veP?CpFP?Kay|=b@T$qj!}|6(K~0&|;}?#XM35#dw(n1DU|Ez0ne4jcu3Ame zmHEtd_^?s}Vu2mJP4s?nz|Y{Y{p&_aGQQlr%Q3j1Zl6aY=X>~iZ-TdPxO}|9Ji7p7 zxmSZDd$_yYV#~tHj892K#@vSEp5k+33@qiJQ83b=z^=9TOhzzZk&#(nNYT_;B)?N z-z~T~8Wx%36WKYIUE3-`whOd+dUVIF=?7g8UwF#IHP_!_-@CupS|8YJcNIc3jVK+- zm(|cdX#!yQ2RRjjd>$BG@-T6V!@GwSPnO0G25hqM?jPiFLaQci!y~u=;4M?x%J;B6 zBEohD_Pbv@{<(8+vU#cAZQl)c`{ohj*H_2NTwgu_%zbPei_AiJUz86{Li4MPE*6@K z_3bPIhC`j6&IzPiQ`BIe2F-1LUau^?4Ky^J^dVloXj*ZJrA(p0Hj&!4#Z@K|W?RfE zV<&A69=u-b?crh+f~`jC35t}skO4QY@uA%@MjWc_CmleU{3mJ$=wI6Y%d_`e?eTw) JX6;MQ{{wbuDk1;? literal 0 HcmV?d00001 diff --git a/gaseous-server/Assets/Ratings/PEGI/Three.jpg b/gaseous-server/Assets/Ratings/PEGI/Three.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2d6c734fbc092705acc128d0f5326ea72adeeda0 GIT binary patch literal 64687 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

?knrj3&=*`dP=l} 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 literal 0 HcmV?d00001 diff --git a/gaseous-server/Classes/Metadata/AgeRating.cs b/gaseous-server/Classes/Metadata/AgeRating.cs index 7057fa6..47bd6df 100644 --- a/gaseous-server/Classes/Metadata/AgeRating.cs +++ b/gaseous-server/Classes/Metadata/AgeRating.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using gaseous_tools; using IGDB; using IGDB.Models; @@ -115,6 +116,37 @@ namespace gaseous_server.Classes.Metadata return result; } + + public static GameAgeRating GetConsolidatedAgeRating(long RatingId) + { + GameAgeRating gameAgeRating = new GameAgeRating(); + + AgeRating ageRating = GetAgeRatings(RatingId); + gameAgeRating.Id = (long)ageRating.Id; + gameAgeRating.RatingBoard = (AgeRatingCategory)ageRating.Category; + gameAgeRating.RatingTitle = (AgeRatingTitle)ageRating.Rating; + + List descriptions = new List(); + if (ageRating.ContentDescriptions != null) + { + foreach (long ContentId in ageRating.ContentDescriptions.Ids) + { + AgeRatingContentDescription ageRatingContentDescription = AgeRatingContentDescriptions.GetAgeRatingContentDescriptions(ContentId); + descriptions.Add(ageRatingContentDescription.Description); + } + } + gameAgeRating.Descriptions = descriptions.ToArray(); + + return gameAgeRating; + } + + public class GameAgeRating + { + public long Id { get; set; } + public AgeRatingCategory RatingBoard { get; set; } + public AgeRatingTitle RatingTitle { get; set; } + public string[] Descriptions { get; set; } + } } } diff --git a/gaseous-server/Classes/Metadata/Storage.cs b/gaseous-server/Classes/Metadata/Storage.cs index 792b2ec..bb26ccd 100644 --- a/gaseous-server/Classes/Metadata/Storage.cs +++ b/gaseous-server/Classes/Metadata/Storage.cs @@ -231,6 +231,9 @@ namespace gaseous_server.Classes.Metadata break; case "artwork": objectToStore = new IdentitiesOrValues(ids: fromJsonObject); + break; + case "ageratingcontentdescription": + objectToStore = new IdentitiesOrValues(ids: fromJsonObject); break; case "game": objectToStore = new IdentitiesOrValues(ids: fromJsonObject); @@ -313,6 +316,9 @@ namespace gaseous_server.Classes.Metadata case "[igdb.models.ageratingcategory": property.SetValue(EndpointType, (AgeRatingCategory)dataRow[property.Name]); break; + case "[igdb.models.ageratingcontentdescriptioncategory": + property.SetValue(EndpointType, (AgeRatingContentDescriptionCategory)dataRow[property.Name]); + break; case "[igdb.models.ageratingtitle": property.SetValue(EndpointType, (AgeRatingTitle)dataRow[property.Name]); break; diff --git a/gaseous-server/Controllers/GamesController.cs b/gaseous-server/Controllers/GamesController.cs index 04ee22a..3c62d3c 100644 --- a/gaseous-server/Controllers/GamesController.cs +++ b/gaseous-server/Controllers/GamesController.cs @@ -1,14 +1,18 @@ using System; using System.Collections.Generic; using System.Data; +using System.IO; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using gaseous_server.Classes.Metadata; using gaseous_tools; using IGDB.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; - +using Microsoft.CodeAnalysis.Scripting; +using static gaseous_server.Classes.Metadata.AgeRatings; + namespace gaseous_server.Controllers { [Route("api/v1/[controller]")] @@ -154,39 +158,23 @@ namespace gaseous_server.Controllers } [HttpGet] - [Route("{GameId}/artwork")] - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] - public ActionResult GameArtwork(long GameId) - { - Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); - - List artworks = new List(); - if (gameObject.Artworks != null) - { - foreach (long ArtworkId in gameObject.Artworks.Ids) - { - Artwork GameArtwork = Artworks.GetArtwork(ArtworkId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); - artworks.Add(GameArtwork); - } - } - - return Ok(artworks); - } - - [HttpGet] - [Route("{GameId}/artwork/{ArtworkId}")] - [ProducesResponseType(typeof(Artwork), StatusCodes.Status200OK)] + [Route("{GameId}/agerating")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GameArtwork(long GameId, long ArtworkId) + public ActionResult GameAgeClassification(long GameId) { - IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); - try { - IGDB.Models.Artwork artworkObject = Artworks.GetArtwork(ArtworkId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); - if (artworkObject != null) + Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + if (gameObject.AgeRatings != null) { - return Ok(artworkObject); + List ageRatings = new List(); + foreach (long ageRatingId in gameObject.AgeRatings.Ids) + { + ageRatings.Add(AgeRatings.GetConsolidatedAgeRating(ageRatingId)); + } + return Ok(ageRatings); } else { @@ -197,28 +185,50 @@ namespace gaseous_server.Controllers { return NotFound(); } - } [HttpGet] - [Route("{GameId}/artwork/{ArtworkId}/image")] - [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [Route("{GameId}/agerating/{RatingId}/image")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GameCoverImage(long GameId, long ArtworkId) + public ActionResult GameAgeClassification(long GameId, long RatingId) { - IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); - try { - IGDB.Models.Artwork artworkObject = Artworks.GetArtwork(ArtworkId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); - if (artworkObject != null) { - string coverFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), "Artwork", artworkObject.ImageId + ".png"); - if (System.IO.File.Exists(coverFilePath)) + GameAgeRating gameAgeRating = GetConsolidatedAgeRating(RatingId); + + string fileExtension = ""; + string fileType = ""; + switch (gameAgeRating.RatingBoard) + { + case AgeRatingCategory.ESRB: + fileExtension = "svg"; + fileType = "image/svg+xml"; + break; + case AgeRatingCategory.PEGI: + fileExtension = "jpg"; + fileType = "image/jpg"; + break; + case AgeRatingCategory.ACB: + fileExtension = "png"; + fileType = "image/png"; + break; + } + + string resourceName = "gaseous_server.Assets.Ratings." + gameAgeRating.RatingBoard.ToString() + "." + gameAgeRating.RatingTitle.ToString() + "." + fileExtension; + + var assembly = Assembly.GetExecutingAssembly(); + string[] resources = assembly.GetManifestResourceNames(); + if (resources.Contains(resourceName)) + { + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) + using (StreamReader reader = new StreamReader(stream)) { - string filename = artworkObject.ImageId + ".png"; - string filepath = coverFilePath; - byte[] filedata = System.IO.File.ReadAllBytes(filepath); - string contentType = "image/png"; + byte[] filedata = new byte[stream.Length]; + stream.Read(filedata, 0, filedata.Length); + + string filename = gameAgeRating.RatingBoard.ToString() + "-" + gameAgeRating.RatingTitle.ToString() + "." + fileExtension; + string contentType = fileType; var cd = new System.Net.Mime.ContentDisposition { @@ -230,12 +240,119 @@ namespace gaseous_server.Controllers return File(filedata, contentType); } + } + return NotFound(); + } + catch + { + return NotFound(); + } + } + + [HttpGet] + [Route("{GameId}/artwork")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameArtwork(long GameId) + { + try + { + Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + List artworks = new List(); + if (gameObject.Artworks != null) + { + foreach (long ArtworkId in gameObject.Artworks.Ids) + { + Artwork GameArtwork = Artworks.GetArtwork(ArtworkId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); + artworks.Add(GameArtwork); + } + } + + return Ok(artworks); + } + catch + { + return NotFound(); + } + } + + [HttpGet] + [Route("{GameId}/artwork/{ArtworkId}")] + [ProducesResponseType(typeof(Artwork), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameArtwork(long GameId, long ArtworkId) + { + try + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + try + { + IGDB.Models.Artwork artworkObject = Artworks.GetArtwork(ArtworkId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); + if (artworkObject != null) + { + return Ok(artworkObject); + } else { return NotFound(); } } - else + catch + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + + [HttpGet] + [Route("{GameId}/artwork/{ArtworkId}/image")] + [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameCoverImage(long GameId, long ArtworkId) + { + try + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + try + { + IGDB.Models.Artwork artworkObject = Artworks.GetArtwork(ArtworkId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); + if (artworkObject != null) { + string coverFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), "Artwork", artworkObject.ImageId + ".png"); + if (System.IO.File.Exists(coverFilePath)) + { + string filename = artworkObject.ImageId + ".png"; + string filepath = coverFilePath; + byte[] filedata = System.IO.File.ReadAllBytes(filepath); + string contentType = "image/png"; + + var cd = new System.Net.Mime.ContentDisposition + { + FileName = filename, + Inline = true, + }; + + Response.Headers.Add("Content-Disposition", cd.ToString()); + + return File(filedata, contentType); + } + else + { + return NotFound(); + } + } + else + { + return NotFound(); + } + } + catch { return NotFound(); } @@ -284,26 +401,33 @@ namespace gaseous_server.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GameCoverImage(long GameId) { - IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + try + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); - string coverFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), "Cover.png"); - if (System.IO.File.Exists(coverFilePath)) { - string filename = "Cover.png"; - string filepath = coverFilePath; - byte[] filedata = System.IO.File.ReadAllBytes(filepath); - string contentType = "image/png"; + string coverFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), "Cover.png"); + if (System.IO.File.Exists(coverFilePath)) { + string filename = "Cover.png"; + string filepath = coverFilePath; + byte[] filedata = System.IO.File.ReadAllBytes(filepath); + string contentType = "image/png"; - var cd = new System.Net.Mime.ContentDisposition + var cd = new System.Net.Mime.ContentDisposition + { + FileName = filename, + Inline = true, + }; + + Response.Headers.Add("Content-Disposition", cd.ToString()); + + return File(filedata, contentType); + } + else { - FileName = filename, - Inline = true, - }; - - Response.Headers.Add("Content-Disposition", cd.ToString()); - - return File(filedata, contentType); + return NotFound(); + } } - else + catch { return NotFound(); } @@ -312,21 +436,29 @@ namespace gaseous_server.Controllers [HttpGet] [Route("{GameId}/screenshots")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GameScreenshot(long GameId) { - Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); - - List screenshots = new List(); - if (gameObject.Screenshots != null) + try { - foreach (long ScreenshotId in gameObject.Screenshots.Ids) - { - Screenshot GameScreenshot = Screenshots.GetScreenshot(ScreenshotId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); - screenshots.Add(GameScreenshot); - } - } + Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); - return Ok(screenshots); + List screenshots = new List(); + if (gameObject.Screenshots != null) + { + foreach (long ScreenshotId in gameObject.Screenshots.Ids) + { + Screenshot GameScreenshot = Screenshots.GetScreenshot(ScreenshotId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); + screenshots.Add(GameScreenshot); + } + } + + return Ok(screenshots); + } + catch + { + return NotFound(); + } } [HttpGet] @@ -366,29 +498,36 @@ namespace gaseous_server.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GameScreenshotImage(long GameId, long ScreenshotId) { - IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); - - IGDB.Models.Screenshot screenshotObject = Screenshots.GetScreenshot(ScreenshotId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); - - string coverFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), "Screenshots", screenshotObject.ImageId + ".png"); - if (System.IO.File.Exists(coverFilePath)) + try { - string filename = screenshotObject.ImageId + ".png"; - string filepath = coverFilePath; - byte[] filedata = System.IO.File.ReadAllBytes(filepath); - string contentType = "image/png"; + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); - var cd = new System.Net.Mime.ContentDisposition + IGDB.Models.Screenshot screenshotObject = Screenshots.GetScreenshot(ScreenshotId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject)); + + string coverFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Game(gameObject), "Screenshots", screenshotObject.ImageId + ".png"); + if (System.IO.File.Exists(coverFilePath)) { - FileName = filename, - Inline = true, - }; + string filename = screenshotObject.ImageId + ".png"; + string filepath = coverFilePath; + byte[] filedata = System.IO.File.ReadAllBytes(filepath); + string contentType = "image/png"; - Response.Headers.Add("Content-Disposition", cd.ToString()); + var cd = new System.Net.Mime.ContentDisposition + { + FileName = filename, + Inline = true, + }; - return File(filedata, contentType); + Response.Headers.Add("Content-Disposition", cd.ToString()); + + return File(filedata, contentType); + } + else + { + return NotFound(); + } } - else + catch { return NotFound(); } @@ -397,21 +536,29 @@ namespace gaseous_server.Controllers [HttpGet] [Route("{GameId}/videos")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GameVideo(long GameId) { - Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); - - List videos = new List(); - if (gameObject.Videos != null) + try { - foreach (long VideoId in gameObject.Videos.Ids) - { - GameVideo gameVideo = GamesVideos.GetGame_Videos(VideoId); - videos.Add(gameVideo); - } - } + Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); - return Ok(videos); + List videos = new List(); + if (gameObject.Videos != null) + { + foreach (long VideoId in gameObject.Videos.Ids) + { + GameVideo gameVideo = GamesVideos.GetGame_Videos(VideoId); + videos.Add(gameVideo); + } + } + + return Ok(videos); + } + catch + { + return NotFound(); + } } } } diff --git a/gaseous-server/gaseous-server.csproj b/gaseous-server/gaseous-server.csproj index 69b1685..a90a5d0 100644 --- a/gaseous-server/gaseous-server.csproj +++ b/gaseous-server/gaseous-server.csproj @@ -24,6 +24,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -33,6 +57,11 @@ + + + + + @@ -55,6 +84,25 @@ true PreserveNewest + + + + + + + + + + + + + + + + + + + From d3a90e679875c681d7f00a9ccf5688ac9d81486e Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Thu, 15 Jun 2023 23:44:56 +1000 Subject: [PATCH 24/71] chore: update nuget packages --- gaseous-server/gaseous-server.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gaseous-server/gaseous-server.csproj b/gaseous-server/gaseous-server.csproj index a90a5d0..74fb485 100644 --- a/gaseous-server/gaseous-server.csproj +++ b/gaseous-server/gaseous-server.csproj @@ -10,9 +10,9 @@ - + - + From 4f40d04d30dafa7ab5f65e7585eb234cf30bfb2d Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Fri, 16 Jun 2023 23:12:48 +1000 Subject: [PATCH 25/71] feat: added platforms to the API --- gaseous-server/Classes/Metadata/Games.cs | 17 ++- gaseous-server/Classes/Metadata/Platforms.cs | 17 ++- .../Controllers/PlatformsController.cs | 137 ++++++++++++++++++ gaseous-server/Program.cs | 4 + 4 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 gaseous-server/Controllers/PlatformsController.cs diff --git a/gaseous-server/Classes/Metadata/Games.cs b/gaseous-server/Classes/Metadata/Games.cs index ba235de..9e50b98 100644 --- a/gaseous-server/Classes/Metadata/Games.cs +++ b/gaseous-server/Classes/Metadata/Games.cs @@ -24,7 +24,22 @@ namespace gaseous_server.Classes.Metadata { if (Id == 0) { - return null; + Game returnValue = new Game(); + if ((Storage.GetCacheStatus("game", 0) == Storage.CacheStatus.NotPresent) || (forceRefresh == true)) + { + returnValue = new Game + { + Id = 0, + Name = "Unknown" + }; + Storage.NewCacheValue(returnValue); + + return returnValue; + } + else + { + return Storage.GetCacheValue(returnValue, "id", 0); + } } else { diff --git a/gaseous-server/Classes/Metadata/Platforms.cs b/gaseous-server/Classes/Metadata/Platforms.cs index 93ad177..4182a93 100644 --- a/gaseous-server/Classes/Metadata/Platforms.cs +++ b/gaseous-server/Classes/Metadata/Platforms.cs @@ -26,7 +26,22 @@ namespace gaseous_server.Classes.Metadata { if (Id == 0) { - return null; + Platform returnValue = new Platform(); + if (Storage.GetCacheStatus("platform", 0) == Storage.CacheStatus.NotPresent) + { + returnValue = new Platform + { + Id = 0, + Name = "Unknown" + }; + Storage.NewCacheValue(returnValue); + + return returnValue; + } + else + { + return Storage.GetCacheValue(returnValue, "id", 0); + } } else { diff --git a/gaseous-server/Controllers/PlatformsController.cs b/gaseous-server/Controllers/PlatformsController.cs new file mode 100644 index 0000000..650ea7d --- /dev/null +++ b/gaseous-server/Controllers/PlatformsController.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using gaseous_server.Classes.Metadata; +using gaseous_tools; +using IGDB.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis.Scripting; + +namespace gaseous_server.Controllers +{ + [Route("api/v1/[controller]")] + [ApiController] + public class PlatformsController : Controller + { + [HttpGet] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + public ActionResult Platform() + { + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + + string sql = "SELECT * FROM gaseous.platform WHERE id IN (SELECT DISTINCT platformid FROM games_roms) ORDER BY `name` ASC;"; + + List RetVal = new List(); + + DataTable dbResponse = db.ExecuteCMD(sql); + foreach (DataRow dr in dbResponse.Rows) + { + RetVal.Add(Classes.Metadata.Platforms.GetPlatform((long)dr["id"])); + } + + return Ok(RetVal); + } + + [HttpGet] + [Route("{PlatformId}")] + [ProducesResponseType(typeof(Platform), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult Platform(long PlatformId) + { + try + { + IGDB.Models.Platform platformObject = Classes.Metadata.Platforms.GetPlatform(PlatformId); + + if (platformObject != null) + { + return Ok(platformObject); + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + + [HttpGet] + [Route("{PlatformId}/platformlogo")] + [ProducesResponseType(typeof(PlatformLogo), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult PlatformLogo(long PlatformId) + { + try + { + IGDB.Models.Platform platformObject = Classes.Metadata.Platforms.GetPlatform(PlatformId); + if (platformObject != null) + { + IGDB.Models.PlatformLogo logoObject = PlatformLogos.GetPlatformLogo(platformObject.PlatformLogo.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(platformObject)); + if (logoObject != null) + { + return Ok(logoObject); + } + else + { + return NotFound(); + } + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + + [HttpGet] + [Route("{PlatformId}/platformlogo/image")] + [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult PlatformLogoImage(long PlatformId) + { + try + { + IGDB.Models.Platform platformObject = Classes.Metadata.Platforms.GetPlatform(PlatformId); + + string logoFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Platform(platformObject), "Logo_Medium.png"); + if (System.IO.File.Exists(logoFilePath)) + { + string filename = "Logo.png"; + string filepath = logoFilePath; + byte[] filedata = System.IO.File.ReadAllBytes(filepath); + string contentType = "image/png"; + + var cd = new System.Net.Mime.ContentDisposition + { + FileName = filename, + Inline = true, + }; + + Response.Headers.Add("Content-Disposition", cd.ToString()); + + return File(filedata, contentType); + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + } +} + diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index 00eea39..1cb716e 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -59,6 +59,10 @@ app.MapControllers(); // setup library directories Config.LibraryConfiguration.InitLibrary(); +// insert unknown platform and game if not present +gaseous_server.Classes.Metadata.Games.GetGame(0, false, true); +gaseous_server.Classes.Metadata.Platforms.GetPlatform(0); + // organise library //gaseous_server.Classes.ImportGame.OrganiseLibrary(); From 8658689ac429565d19b172ce1f876af6a7b157f8 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Fri, 16 Jun 2023 23:56:12 +1000 Subject: [PATCH 26/71] feat: Roms can now be downloaded via the API --- gaseous-server/Classes/Metadata/Games.cs | 2 +- gaseous-server/Classes/Metadata/Platforms.cs | 2 +- gaseous-server/Classes/Roms.cs | 64 +++++++++---- gaseous-server/Controllers/GamesController.cs | 91 +++++++++++++++++++ gaseous-server/Program.cs | 5 +- 5 files changed, 144 insertions(+), 20 deletions(-) diff --git a/gaseous-server/Classes/Metadata/Games.cs b/gaseous-server/Classes/Metadata/Games.cs index 9e50b98..539c2e5 100644 --- a/gaseous-server/Classes/Metadata/Games.cs +++ b/gaseous-server/Classes/Metadata/Games.cs @@ -30,7 +30,7 @@ namespace gaseous_server.Classes.Metadata returnValue = new Game { Id = 0, - Name = "Unknown" + Name = "Unknown Title" }; Storage.NewCacheValue(returnValue); diff --git a/gaseous-server/Classes/Metadata/Platforms.cs b/gaseous-server/Classes/Metadata/Platforms.cs index 4182a93..97c618b 100644 --- a/gaseous-server/Classes/Metadata/Platforms.cs +++ b/gaseous-server/Classes/Metadata/Platforms.cs @@ -32,7 +32,7 @@ namespace gaseous_server.Classes.Metadata returnValue = new Platform { Id = 0, - Name = "Unknown" + Name = "Unknown Platform" }; Storage.NewCacheValue(returnValue); diff --git a/gaseous-server/Classes/Roms.cs b/gaseous-server/Classes/Roms.cs index 20f8a17..a36decd 100644 --- a/gaseous-server/Classes/Roms.cs +++ b/gaseous-server/Classes/Roms.cs @@ -6,6 +6,30 @@ namespace gaseous_server.Classes { public class Roms { + public static List GetRoms(long GameId) + { + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "SELECT * FROM games_roms WHERE gameid = @id ORDER BY `name`"; + Dictionary dbDict = new Dictionary(); + dbDict.Add("id", GameId); + DataTable romDT = db.ExecuteCMD(sql, dbDict); + + if (romDT.Rows.Count > 0) + { + List romItems = new List(); + foreach (DataRow romDR in romDT.Rows) + { + romItems.Add(BuildRom(romDR)); + } + + return romItems; + } + else + { + throw new Exception("Unknown Game Id"); + } + } + public static RomItem GetRom(long RomId) { Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); @@ -17,23 +41,7 @@ namespace gaseous_server.Classes if (romDT.Rows.Count > 0) { DataRow romDR = romDT.Rows[0]; - RomItem romItem = new RomItem - { - Id = (long)romDR["id"], - PlatformId = (long)romDR["platformid"], - GameId = (long)romDR["gameid"], - Name = (string)romDR["name"], - Size = (long)romDR["size"], - CRC = (string)romDR["crc"], - MD5 = (string)romDR["md5"], - SHA1 = (string)romDR["sha1"], - DevelopmentStatus = (string)romDR["developmentstatus"], - Flags = Newtonsoft.Json.JsonConvert.DeserializeObject((string)romDR["flags"]), - RomType = (int)romDR["romtype"], - RomTypeMedia = (string)romDR["romtypemedia"], - MediaLabel = (string)romDR["medialabel"], - Path = (string)romDR["path"] - }; + RomItem romItem = BuildRom(romDR); return romItem; } else @@ -42,6 +50,28 @@ namespace gaseous_server.Classes } } + private static RomItem BuildRom(DataRow romDR) + { + RomItem romItem = new RomItem + { + Id = (long)romDR["id"], + PlatformId = (long)romDR["platformid"], + GameId = (long)romDR["gameid"], + Name = (string)romDR["name"], + Size = (long)romDR["size"], + CRC = (string)romDR["crc"], + MD5 = (string)romDR["md5"], + SHA1 = (string)romDR["sha1"], + DevelopmentStatus = (string)romDR["developmentstatus"], + Flags = Newtonsoft.Json.JsonConvert.DeserializeObject((string)romDR["flags"]), + RomType = (int)romDR["romtype"], + RomTypeMedia = (string)romDR["romtypemedia"], + MediaLabel = (string)romDR["medialabel"], + Path = (string)romDR["path"] + }; + return romItem; + } + public class RomItem { public long Id { get; set; } diff --git a/gaseous-server/Controllers/GamesController.cs b/gaseous-server/Controllers/GamesController.cs index 3c62d3c..3e44bf2 100644 --- a/gaseous-server/Controllers/GamesController.cs +++ b/gaseous-server/Controllers/GamesController.cs @@ -433,6 +433,97 @@ namespace gaseous_server.Controllers } } + [HttpGet] + [Route("{GameId}/roms")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameRom(long GameId) + { + try + { + Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + List roms = Classes.Roms.GetRoms(GameId); + + return Ok(roms); + } + catch + { + return NotFound(); + } + } + + [HttpGet] + [Route("{GameId}/roms/{RomId}")] + [ProducesResponseType(typeof(Classes.Roms.RomItem), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameRom(long GameId, long RomId) + { + try + { + Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + Classes.Roms.RomItem rom = Classes.Roms.GetRom(RomId); + if (rom.GameId == GameId) + { + return Ok(rom); + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + + [HttpGet] + [Route("{GameId}/roms/{RomId}/file")] + [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameRomFile(long GameId, long RomId) + { + try + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + Classes.Roms.RomItem rom = Classes.Roms.GetRom(RomId); + if (rom.GameId != GameId) + { + return NotFound(); + } + + string romFilePath = rom.Path; + if (System.IO.File.Exists(romFilePath)) + { + string filename = Path.GetFileName(romFilePath); + string filepath = romFilePath; + byte[] filedata = System.IO.File.ReadAllBytes(filepath); + string contentType = "application/octet-stream"; + + var cd = new System.Net.Mime.ContentDisposition + { + FileName = filename, + Inline = false, + }; + + Response.Headers.Add("Content-Disposition", cd.ToString()); + + return File(filedata, contentType); + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + [HttpGet] [Route("{GameId}/screenshots")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index 1cb716e..542ea6c 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -35,7 +35,10 @@ builder.Services.AddControllers().AddJsonOptions(x => // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(options => +{ + options.CustomSchemaIds(type => type.ToString()); +}); builder.Services.AddHostedService(); var app = builder.Build(); From 4413dccbdf3b5feed05e3de2c63061e27a6e0a4a Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sat, 17 Jun 2023 00:39:26 +1000 Subject: [PATCH 27/71] feat: Added Rom management to the API --- gaseous-server/Classes/Roms.cs | 34 ++++++++++++ gaseous-server/Controllers/GamesController.cs | 54 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/gaseous-server/Classes/Roms.cs b/gaseous-server/Classes/Roms.cs index a36decd..aed8ca7 100644 --- a/gaseous-server/Classes/Roms.cs +++ b/gaseous-server/Classes/Roms.cs @@ -50,6 +50,40 @@ namespace gaseous_server.Classes } } + public static RomItem UpdateRom(long RomId, long PlatformId, long GameId) + { + // ensure metadata for platformid is present + IGDB.Models.Platform platform = Classes.Metadata.Platforms.GetPlatform(PlatformId); + + // ensure metadata for gameid is present + IGDB.Models.Game game = Classes.Metadata.Games.GetGame(GameId, false, false); + + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "UPDATE games_roms SET platformid=@platformid, gameid=@gameid WHERE id = @id"; + Dictionary dbDict = new Dictionary(); + dbDict.Add("id", RomId); + dbDict.Add("platformid", PlatformId); + dbDict.Add("gameid", GameId); + db.ExecuteCMD(sql, dbDict); + + RomItem rom = GetRom(RomId); + + return rom; + } + + public static void DeleteRom(long RomId) + { + RomItem rom = GetRom(RomId); + if (File.Exists(rom.Path)) + { + File.Delete(rom.Path); + } + + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "DELETE FROM games_roms WHERE id = @id"; + db.ExecuteCMD(sql); + } + private static RomItem BuildRom(DataRow romDR) { RomItem romItem = new RomItem diff --git a/gaseous-server/Controllers/GamesController.cs b/gaseous-server/Controllers/GamesController.cs index 3e44bf2..37371e9 100644 --- a/gaseous-server/Controllers/GamesController.cs +++ b/gaseous-server/Controllers/GamesController.cs @@ -479,6 +479,60 @@ namespace gaseous_server.Controllers } } + [HttpPatch] + [Route("{GameId}/roms/{RomId}")] + [ProducesResponseType(typeof(Classes.Roms.RomItem), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameRomRename(long GameId, long RomId, long NewPlatformId, long NewGameId) + { + try + { + Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + Classes.Roms.RomItem rom = Classes.Roms.GetRom(RomId); + if (rom.GameId == GameId) + { + rom = Classes.Roms.UpdateRom(RomId, NewPlatformId, NewGameId); + return Ok(rom); + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + + [HttpDelete] + [Route("{GameId}/roms/{RomId}")] + [ProducesResponseType(typeof(Classes.Roms.RomItem), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameRomDelete(long GameId, long RomId) + { + try + { + Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + Classes.Roms.RomItem rom = Classes.Roms.GetRom(RomId); + if (rom.GameId == GameId) + { + Classes.Roms.DeleteRom(RomId); + return Ok(rom); + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + [HttpGet] [Route("{GameId}/roms/{RomId}/file")] [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] From 00cc051dc69b1c8ed2c9a80ed29193d8c27f64fd Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sun, 18 Jun 2023 23:08:02 +1000 Subject: [PATCH 28/71] feat: added metadata source field - this will determine the handling of the flags attribute --- .../RomSignatureObject.cs | 8 +++++ gaseous-server/Classes/ImportGames.cs | 8 +++-- gaseous-server/Classes/Metadata/Games.cs | 3 +- gaseous-server/Classes/Metadata/Platforms.cs | 3 +- gaseous-server/Classes/Roms.cs | 32 ++++++++++++------- .../Classes/SignatureIngestors/TOSEC.cs | 3 +- gaseous-server/Controllers/GamesController.cs | 18 +++++------ .../Controllers/SignaturesController.cs | 5 +-- gaseous-server/Models/Signatures_Games.cs | 8 +++++ gaseous-server/Program.cs | 7 ++-- .../Parsers/TosecParser.cs | 1 + gaseous-tools/Database/MySQL/gaseous-1000.sql | 14 ++++---- 12 files changed, 70 insertions(+), 40 deletions(-) diff --git a/gaseous-romsignatureobject/RomSignatureObject.cs b/gaseous-romsignatureobject/RomSignatureObject.cs index 5c04359..20021d2 100644 --- a/gaseous-romsignatureobject/RomSignatureObject.cs +++ b/gaseous-romsignatureobject/RomSignatureObject.cs @@ -72,6 +72,14 @@ namespace gaseous_romsignatureobject public string? RomTypeMedia { get; set; } public string? MediaLabel { get; set; } + public SignatureSourceType SignatureSource { get; set; } + + public enum SignatureSourceType + { + None = 0, + TOSEC = 1 + } + public enum RomTypes { ///

diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs index e5cea16..d6b48b5 100644 --- a/gaseous-server/Classes/ImportGames.cs +++ b/gaseous-server/Classes/ImportGames.cs @@ -155,6 +155,7 @@ namespace gaseous_server.Classes ri.Md5 = hash.md5hash; ri.Sha1 = hash.sha1hash; ri.Size = fi.Length; + ri.SignatureSource = Models.Signatures_Games.RomItem.SignatureSourceType.None; } } @@ -205,13 +206,14 @@ namespace gaseous_server.Classes } // add to database - sql = "INSERT INTO games_roms (platformid, gameid, name, size, crc, md5, sha1, developmentstatus, flags, romtype, romtypemedia, medialabel, path) VALUES (@platformid, @gameid, @name, @size, @crc, @md5, @sha1, @developmentstatus, @flags, @romtype, @romtypemedia, @medialabel, @path); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; + sql = "INSERT INTO games_roms (platformid, gameid, name, size, crc, md5, sha1, developmentstatus, flags, romtype, romtypemedia, medialabel, path, metadatasource) VALUES (@platformid, @gameid, @name, @size, @crc, @md5, @sha1, @developmentstatus, @flags, @romtype, @romtypemedia, @medialabel, @path, @metadatasource); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; dbDict.Add("platformid", Common.ReturnValueIfNull(determinedPlatform.Id, 0)); dbDict.Add("gameid", Common.ReturnValueIfNull(determinedGame.Id, 0)); dbDict.Add("name", Common.ReturnValueIfNull(discoveredSignature.Rom.Name, "")); dbDict.Add("size", Common.ReturnValueIfNull(discoveredSignature.Rom.Size, 0)); dbDict.Add("crc", Common.ReturnValueIfNull(discoveredSignature.Rom.Crc, "")); dbDict.Add("developmentstatus", Common.ReturnValueIfNull(discoveredSignature.Rom.DevelopmentStatus, "")); + dbDict.Add("metadatasource", discoveredSignature.Rom.SignatureSource); if (discoveredSignature.Rom.flags != null) { @@ -245,7 +247,7 @@ namespace gaseous_server.Classes public static string ComputeROMPath(long RomId) { - Classes.Roms.RomItem rom = Classes.Roms.GetRom(RomId); + Classes.Roms.GameRomItem rom = Classes.Roms.GetRom(RomId); // get metadata IGDB.Models.Platform platform = gaseous_server.Classes.Metadata.Platforms.GetPlatform(rom.PlatformId); @@ -275,7 +277,7 @@ namespace gaseous_server.Classes public static void MoveGameFile(long RomId) { - Classes.Roms.RomItem rom = Classes.Roms.GetRom(RomId); + Classes.Roms.GameRomItem rom = Classes.Roms.GetRom(RomId); string romPath = rom.Path; if (File.Exists(romPath)) diff --git a/gaseous-server/Classes/Metadata/Games.cs b/gaseous-server/Classes/Metadata/Games.cs index 539c2e5..016eed1 100644 --- a/gaseous-server/Classes/Metadata/Games.cs +++ b/gaseous-server/Classes/Metadata/Games.cs @@ -30,7 +30,8 @@ namespace gaseous_server.Classes.Metadata returnValue = new Game { Id = 0, - Name = "Unknown Title" + Name = "Unknown Title", + Slug = "Unknown" }; Storage.NewCacheValue(returnValue); diff --git a/gaseous-server/Classes/Metadata/Platforms.cs b/gaseous-server/Classes/Metadata/Platforms.cs index 97c618b..4e3fefa 100644 --- a/gaseous-server/Classes/Metadata/Platforms.cs +++ b/gaseous-server/Classes/Metadata/Platforms.cs @@ -32,7 +32,8 @@ namespace gaseous_server.Classes.Metadata returnValue = new Platform { Id = 0, - Name = "Unknown Platform" + Name = "Unknown Platform", + Slug = "Unknown" }; Storage.NewCacheValue(returnValue); diff --git a/gaseous-server/Classes/Roms.cs b/gaseous-server/Classes/Roms.cs index aed8ca7..f85fe68 100644 --- a/gaseous-server/Classes/Roms.cs +++ b/gaseous-server/Classes/Roms.cs @@ -6,7 +6,7 @@ namespace gaseous_server.Classes { public class Roms { - public static List GetRoms(long GameId) + public static List GetRoms(long GameId) { Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); string sql = "SELECT * FROM games_roms WHERE gameid = @id ORDER BY `name`"; @@ -16,7 +16,7 @@ namespace gaseous_server.Classes if (romDT.Rows.Count > 0) { - List romItems = new List(); + List romItems = new List(); foreach (DataRow romDR in romDT.Rows) { romItems.Add(BuildRom(romDR)); @@ -30,7 +30,7 @@ namespace gaseous_server.Classes } } - public static RomItem GetRom(long RomId) + public static GameRomItem GetRom(long RomId) { Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); string sql = "SELECT * FROM games_roms WHERE id = @id"; @@ -41,7 +41,7 @@ namespace gaseous_server.Classes if (romDT.Rows.Count > 0) { DataRow romDR = romDT.Rows[0]; - RomItem romItem = BuildRom(romDR); + GameRomItem romItem = BuildRom(romDR); return romItem; } else @@ -50,7 +50,7 @@ namespace gaseous_server.Classes } } - public static RomItem UpdateRom(long RomId, long PlatformId, long GameId) + public static GameRomItem UpdateRom(long RomId, long PlatformId, long GameId) { // ensure metadata for platformid is present IGDB.Models.Platform platform = Classes.Metadata.Platforms.GetPlatform(PlatformId); @@ -66,14 +66,14 @@ namespace gaseous_server.Classes dbDict.Add("gameid", GameId); db.ExecuteCMD(sql, dbDict); - RomItem rom = GetRom(RomId); + GameRomItem rom = GetRom(RomId); return rom; } public static void DeleteRom(long RomId) { - RomItem rom = GetRom(RomId); + GameRomItem rom = GetRom(RomId); if (File.Exists(rom.Path)) { File.Delete(rom.Path); @@ -84,9 +84,9 @@ namespace gaseous_server.Classes db.ExecuteCMD(sql); } - private static RomItem BuildRom(DataRow romDR) + private static GameRomItem BuildRom(DataRow romDR) { - RomItem romItem = new RomItem + GameRomItem romItem = new GameRomItem { Id = (long)romDR["id"], PlatformId = (long)romDR["platformid"], @@ -101,12 +101,13 @@ namespace gaseous_server.Classes RomType = (int)romDR["romtype"], RomTypeMedia = (string)romDR["romtypemedia"], MediaLabel = (string)romDR["medialabel"], - Path = (string)romDR["path"] + Path = (string)romDR["path"], + Source = (GameRomItem.SourceType)(Int32)romDR["metadatasource"] }; return romItem; } - public class RomItem + public class GameRomItem { public long Id { get; set; } public long PlatformId { get; set; } @@ -122,7 +123,14 @@ namespace gaseous_server.Classes public string? RomTypeMedia { get; set; } public string? MediaLabel { get; set; } public string? Path { get; set; } + public SourceType Source { get; set; } + + public enum SourceType + { + None = 0, + TOSEC = 1 + } } - } + } } diff --git a/gaseous-server/Classes/SignatureIngestors/TOSEC.cs b/gaseous-server/Classes/SignatureIngestors/TOSEC.cs index c1c46b4..72de44a 100644 --- a/gaseous-server/Classes/SignatureIngestors/TOSEC.cs +++ b/gaseous-server/Classes/SignatureIngestors/TOSEC.cs @@ -194,12 +194,13 @@ namespace gaseous_server.SignatureIngestors.TOSEC dbDict.Add("romtype", (int)romObject.RomType); dbDict.Add("romtypemedia", Common.ReturnValueIfNull(romObject.RomTypeMedia, "")); dbDict.Add("medialabel", Common.ReturnValueIfNull(romObject.MediaLabel, "")); + dbDict.Add("metadatasource", Classes.Roms.GameRomItem.SourceType.TOSEC); sigDB = db.ExecuteCMD(sql, dbDict); if (sigDB.Rows.Count == 0) { // entry not present, insert it - sql = "INSERT INTO signatures_roms (gameid, name, size, crc, md5, sha1, developmentstatus, flags, romtype, romtypemedia, medialabel) VALUES (@gameid, @name, @size, @crc, @md5, @sha1, @developmentstatus, @flags, @romtype, @romtypemedia, @medialabel); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; + sql = "INSERT INTO signatures_roms (gameid, name, size, crc, md5, sha1, developmentstatus, flags, romtype, romtypemedia, medialabel, metadatasource) VALUES (@gameid, @name, @size, @crc, @md5, @sha1, @developmentstatus, @flags, @romtype, @romtypemedia, @medialabel, @metadatasource); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; sigDB = db.ExecuteCMD(sql, dbDict); diff --git a/gaseous-server/Controllers/GamesController.cs b/gaseous-server/Controllers/GamesController.cs index 37371e9..ed99732 100644 --- a/gaseous-server/Controllers/GamesController.cs +++ b/gaseous-server/Controllers/GamesController.cs @@ -435,7 +435,7 @@ namespace gaseous_server.Controllers [HttpGet] [Route("{GameId}/roms")] - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GameRom(long GameId) { @@ -443,7 +443,7 @@ namespace gaseous_server.Controllers { Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); - List roms = Classes.Roms.GetRoms(GameId); + List roms = Classes.Roms.GetRoms(GameId); return Ok(roms); } @@ -455,7 +455,7 @@ namespace gaseous_server.Controllers [HttpGet] [Route("{GameId}/roms/{RomId}")] - [ProducesResponseType(typeof(Classes.Roms.RomItem), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(Classes.Roms.GameRomItem), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GameRom(long GameId, long RomId) { @@ -463,7 +463,7 @@ namespace gaseous_server.Controllers { Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); - Classes.Roms.RomItem rom = Classes.Roms.GetRom(RomId); + Classes.Roms.GameRomItem rom = Classes.Roms.GetRom(RomId); if (rom.GameId == GameId) { return Ok(rom); @@ -481,7 +481,7 @@ namespace gaseous_server.Controllers [HttpPatch] [Route("{GameId}/roms/{RomId}")] - [ProducesResponseType(typeof(Classes.Roms.RomItem), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(Classes.Roms.GameRomItem), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GameRomRename(long GameId, long RomId, long NewPlatformId, long NewGameId) { @@ -489,7 +489,7 @@ namespace gaseous_server.Controllers { Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); - Classes.Roms.RomItem rom = Classes.Roms.GetRom(RomId); + Classes.Roms.GameRomItem rom = Classes.Roms.GetRom(RomId); if (rom.GameId == GameId) { rom = Classes.Roms.UpdateRom(RomId, NewPlatformId, NewGameId); @@ -508,7 +508,7 @@ namespace gaseous_server.Controllers [HttpDelete] [Route("{GameId}/roms/{RomId}")] - [ProducesResponseType(typeof(Classes.Roms.RomItem), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(Classes.Roms.GameRomItem), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GameRomDelete(long GameId, long RomId) { @@ -516,7 +516,7 @@ namespace gaseous_server.Controllers { Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); - Classes.Roms.RomItem rom = Classes.Roms.GetRom(RomId); + Classes.Roms.GameRomItem rom = Classes.Roms.GetRom(RomId); if (rom.GameId == GameId) { Classes.Roms.DeleteRom(RomId); @@ -543,7 +543,7 @@ namespace gaseous_server.Controllers { IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); - Classes.Roms.RomItem rom = Classes.Roms.GetRom(RomId); + Classes.Roms.GameRomItem rom = Classes.Roms.GetRom(RomId); if (rom.GameId != GameId) { return NotFound(); diff --git a/gaseous-server/Controllers/SignaturesController.cs b/gaseous-server/Controllers/SignaturesController.cs index 1e272d2..4d521dc 100644 --- a/gaseous-server/Controllers/SignaturesController.cs +++ b/gaseous-server/Controllers/SignaturesController.cs @@ -55,7 +55,7 @@ namespace gaseous_server.Controllers private List _GetSignature(string sqlWhere, string searchString) { Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT \n view_signatures_games.*,\n signatures_roms.id AS romid,\n signatures_roms.name AS romname,\n signatures_roms.size,\n signatures_roms.crc,\n signatures_roms.md5,\n signatures_roms.sha1,\n signatures_roms.developmentstatus,\n signatures_roms.flags,\n signatures_roms.romtype,\n signatures_roms.romtypemedia,\n signatures_roms.medialabel\nFROM\n signatures_roms\n INNER JOIN\n view_signatures_games ON signatures_roms.gameid = view_signatures_games.id WHERE " + sqlWhere; + string sql = "SELECT \n view_signatures_games.*,\n signatures_roms.id AS romid,\n signatures_roms.name AS romname,\n signatures_roms.size,\n signatures_roms.crc,\n signatures_roms.md5,\n signatures_roms.sha1,\n signatures_roms.developmentstatus,\n signatures_roms.flags,\n signatures_roms.romtype,\n signatures_roms.romtypemedia,\n signatures_roms.medialabel,\n signatures_roms.metadatasource\nFROM\n signatures_roms\n INNER JOIN\n view_signatures_games ON signatures_roms.gameid = view_signatures_games.id WHERE " + sqlWhere; Dictionary dbDict = new Dictionary(); dbDict.Add("searchString", searchString); @@ -94,7 +94,8 @@ namespace gaseous_server.Controllers flags = Newtonsoft.Json.JsonConvert.DeserializeObject>((string)sigDbRow["flags"]), RomType = (Models.Signatures_Games.RomItem.RomTypes)(int)sigDbRow["romtype"], RomTypeMedia = (string)sigDbRow["romtypemedia"], - MediaLabel = (string)sigDbRow["medialabel"] + MediaLabel = (string)sigDbRow["medialabel"], + SignatureSource = (Models.Signatures_Games.RomItem.SignatureSourceType)(Int32)sigDbRow["metadatasource"] } }; GamesList.Add(gameItem); diff --git a/gaseous-server/Models/Signatures_Games.cs b/gaseous-server/Models/Signatures_Games.cs index 89f4524..0208008 100644 --- a/gaseous-server/Models/Signatures_Games.cs +++ b/gaseous-server/Models/Signatures_Games.cs @@ -133,6 +133,14 @@ namespace gaseous_server.Models public string? RomTypeMedia { get; set; } public string? MediaLabel { get; set; } + public SignatureSourceType SignatureSource { get; set; } + + public enum SignatureSourceType + { + None = 0, + TOSEC = 1 + } + public enum RomTypes { /// diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index 542ea6c..49342f4 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -35,10 +35,7 @@ builder.Services.AddControllers().AddJsonOptions(x => // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(options => -{ - options.CustomSchemaIds(type => type.ToString()); -}); +builder.Services.AddSwaggerGen(); builder.Services.AddHostedService(); var app = builder.Build(); @@ -63,7 +60,7 @@ app.MapControllers(); Config.LibraryConfiguration.InitLibrary(); // insert unknown platform and game if not present -gaseous_server.Classes.Metadata.Games.GetGame(0, false, true); +gaseous_server.Classes.Metadata.Games.GetGame(0, false, false); gaseous_server.Classes.Metadata.Platforms.GetPlatform(0); // organise library diff --git a/gaseous-signature-parser/Parsers/TosecParser.cs b/gaseous-signature-parser/Parsers/TosecParser.cs index d63c4f8..a8e6f4d 100644 --- a/gaseous-signature-parser/Parsers/TosecParser.cs +++ b/gaseous-signature-parser/Parsers/TosecParser.cs @@ -365,6 +365,7 @@ namespace gaseous_signature_parser.parsers romObject.Crc = xmlGameDetail.Attributes["crc"]?.Value; romObject.Md5 = xmlGameDetail.Attributes["md5"]?.Value; romObject.Sha1 = xmlGameDetail.Attributes["sha1"]?.Value; + romObject.SignatureSource = RomSignatureObject.Game.Rom.SignatureSourceType.TOSEC; // parse name string[] romNameTokens = romDescription.Split("("); diff --git a/gaseous-tools/Database/MySQL/gaseous-1000.sql b/gaseous-tools/Database/MySQL/gaseous-1000.sql index 457cd2d..75a7db3 100644 --- a/gaseous-tools/Database/MySQL/gaseous-1000.sql +++ b/gaseous-tools/Database/MySQL/gaseous-1000.sql @@ -300,9 +300,10 @@ CREATE TABLE `games_roms` ( `romtypemedia` varchar(100) DEFAULT NULL, `medialabel` varchar(100) DEFAULT NULL, `path` longtext, + `metadatasource` int DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `id_UNIQUE` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=398 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -497,7 +498,7 @@ CREATE TABLE `signatures_games` ( KEY `ingest_idx` (`name`,`year`,`publisherid`,`systemid`,`country`,`language`) USING BTREE, CONSTRAINT `publisher` FOREIGN KEY (`publisherid`) REFERENCES `signatures_publishers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `system` FOREIGN KEY (`systemid`) REFERENCES `signatures_platforms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=785672 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -513,7 +514,7 @@ CREATE TABLE `signatures_platforms` ( PRIMARY KEY (`id`), UNIQUE KEY `idsignatures_platforms_UNIQUE` (`id`), KEY `platforms_idx` (`platform`,`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=417 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -529,7 +530,7 @@ CREATE TABLE `signatures_publishers` ( PRIMARY KEY (`id`), UNIQUE KEY `id_UNIQUE` (`id`), KEY `publisher_idx` (`publisher`,`id`) -) ENGINE=InnoDB AUTO_INCREMENT=52259 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -552,6 +553,7 @@ CREATE TABLE `signatures_roms` ( `romtype` int DEFAULT NULL, `romtypemedia` varchar(100) DEFAULT NULL, `medialabel` varchar(100) DEFAULT NULL, + `metadatasource` int DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `id_UNIQUE` (`id`,`gameid`) USING BTREE, KEY `gameid_idx` (`gameid`), @@ -559,7 +561,7 @@ CREATE TABLE `signatures_roms` ( KEY `sha1_idx` (`sha1`) USING BTREE, KEY `flags_idx` ((cast(`flags` as char(255) array))), CONSTRAINT `gameid` FOREIGN KEY (`gameid`) REFERENCES `signatures_games` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=1734101 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -586,7 +588,7 @@ CREATE TABLE `signatures_sources` ( UNIQUE KEY `id_UNIQUE` (`id`), KEY `sourcemd5_idx` (`sourcemd5`,`id`) USING BTREE, KEY `sourcesha1_idx` (`sourcesha1`,`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=3109 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- From a9dee0dd4c42c80a5814f22a4bb0ace55f68e488 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Thu, 22 Jun 2023 23:38:05 +1000 Subject: [PATCH 29/71] feat: static pages can now be served --- gaseous-server/wwwroot/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gaseous-server/wwwroot/index.html b/gaseous-server/wwwroot/index.html index 045404b..e1e96d8 100644 --- a/gaseous-server/wwwroot/index.html +++ b/gaseous-server/wwwroot/index.html @@ -5,6 +5,6 @@ - + Hello World! From 8112f9e9a76aeeda90f385458a83c3f5de820064 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sat, 24 Jun 2023 14:06:14 +1000 Subject: [PATCH 30/71] feat: first pass at filter and game screen + some bug fixes --- .DS_Store | Bin 0 -> 10244 bytes gaseous-server/.DS_Store | Bin 0 -> 6148 bytes gaseous-server/Classes/Metadata/Storage.cs | 11 +- gaseous-server/Classes/MetadataManagement.cs | 11 +- gaseous-server/Classes/Roms.cs | 4 +- gaseous-server/Controllers/GamesController.cs | 4 +- gaseous-server/gaseous-server.csproj | 10 ++ gaseous-server/wwwroot/.DS_Store | Bin 0 -> 6148 bytes gaseous-server/wwwroot/images/logo.png | Bin 0 -> 1260 bytes gaseous-server/wwwroot/images/unknowngame.png | Bin 0 -> 2319 bytes gaseous-server/wwwroot/index.html | 30 ++++- .../wwwroot/scripts/filterformating.js | 76 +++++++++++ .../wwwroot/scripts/gamesformating.js | 41 ++++++ gaseous-server/wwwroot/scripts/main.js | 24 ++++ gaseous-server/wwwroot/styles/style.css | 121 ++++++++++++++++++ .../gaseous-signature-ingestor.csproj.user | 8 ++ gaseous-tools/Database/MySQL/gaseous-1000.sql | 2 +- 17 files changed, 330 insertions(+), 12 deletions(-) create mode 100644 .DS_Store create mode 100644 gaseous-server/.DS_Store create mode 100644 gaseous-server/wwwroot/.DS_Store create mode 100644 gaseous-server/wwwroot/images/logo.png create mode 100644 gaseous-server/wwwroot/images/unknowngame.png create mode 100644 gaseous-server/wwwroot/scripts/filterformating.js create mode 100644 gaseous-server/wwwroot/scripts/gamesformating.js create mode 100644 gaseous-server/wwwroot/scripts/main.js create mode 100644 gaseous-server/wwwroot/styles/style.css create mode 100644 gaseous-signature-ingestor/gaseous-signature-ingestor.csproj.user diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..87c57ab261fc0ddee6e8378a8fa6d29360931046 GIT binary patch literal 10244 zcmeHM&ubGw6n>k;W@B54rG?^Y#fxC7X=_7^ml#tJ1wm8%fz>wK#5S7jhRvoCY9OGe zdhj0*@#evc9@HWpdQfkklwOJ#!9#Bf3W68k%xs$3-8QBhXiJ%a**BS)H}B0i-@e(I z0DzPzX9fWj0FW^g8ScWz28pzj>`FeXCW=x*eaO!are^d)(bZE_2m}NI0s(=5KtLd{ zTM)oKnj;qepkO94Zpj*xG<4vD9sy7Wu$m?;he-jYV#&BAYfNGb zCW)z{WU7*N#E_Ue)+<)OxFu^$GIfxw!-r&9maG$sM6Kia3hfS3oJsK%2nYn+2#~XT z0FJ{f9ybR$g)Q=f>gZzj-g4KP=exAPdsy zPZs?tqZ~j^pkJVYiKSbZvtX!|QqkhUl3L8?N@g^=BKiD*eI3DIDA*aik($#hDKl-B zONq2`o&1{6^}Jdyr`5~3Y-K9kdq&sHbWY0}R3VqiVwcG)IW41C5_(z7l&EfsHH1QP zXe!*lurM+fizu<^Xf>iNj15I2%E{qawHlK9j+{7kY3feCpy`VQT-*Yztpd8)@Acdh5jkwpkRvAY!=OA-;Wo+{F!u;AJk8z46JP3qR}6XHe3Wn2j zADzao)T$1@-tusbtanozj&*hO)g3v=-lF09XECan7+nk=3wYd5Vy&lfc#M9$c~p;26*X~$6rtldE2<*KbQ7`9nXvNKTGx4x@BMZ zxZ2J*Ha-NFJ@LWu-gUe9MBXeN^3HGC)lX=yNpBvj@e98e@3J`4RHN~xw5DO3e}{Ey zCn$&{5D*A#2LgU(L?PcDAGPYp9V1QH9w!BqH72oyX@~v{*yvu!eNJfM?!)$dD$f6# U-Vr&zSDT(GclthQub=<_0sXhAdH?_b literal 0 HcmV?d00001 diff --git a/gaseous-server/.DS_Store b/gaseous-server/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f0d37de2d8be1531103bcdcde6a11a61f063f1dd GIT binary patch literal 6148 zcmeHK%}T>S5Z-O8Nhm@N3Oz1(Ef}j5#Y>3w1&ruHr6#0kFlI}W+CwSitS{t~_&m<+ zZop#BB6bFLzxmzGevtiPjPYQe95UuI#%ySa9F+z^cVnn!k`XzMQB31Z#$bKKQxp5^ zfZyI`F-utq^QjuJtIsPA7}8 zyL&G4GD`EwOckWb1XAv9(mauiK+f}|P_>~BIF9X%-QIFJJQ?)GVCb#-VtG37`r`O# zxLP^(-u}Va<@h;!$>p1-lLOmIb_~|=4vJaLYdFg@nLmQ3&aSfvi2-7O7$62Vn*nn! z*xk*hfmTlp5CaVi;Qk<>A^HX@jcV(F4zJG`?;xUpj&BJ>VbC{NX@m#}*QJ2El$$38 z*X7_BCeJrmY1HM6tC?XOGjsiT;c9m93zg2euaSCUfEZY3psh_8&;JYfWf~v(>m_6n z1H`~TV}N%?;V6JbnX~oB^6;z`&>o8YTO3)CUcH&|)JQP8i- Q0qG*32%(M`_yq>O0En7NX8-^I literal 0 HcmV?d00001 diff --git a/gaseous-server/Classes/Metadata/Storage.cs b/gaseous-server/Classes/Metadata/Storage.cs index bb26ccd..8d543cc 100644 --- a/gaseous-server/Classes/Metadata/Storage.cs +++ b/gaseous-server/Classes/Metadata/Storage.cs @@ -39,7 +39,7 @@ namespace gaseous_server.Classes.Metadata DataTable dt = db.ExecuteCMD(sql, dbDict); if (dt.Rows.Count == 0) { - // no data stored for this item, or lastUpdated + // no data stored for this item, or lastUpdated return CacheStatus.NotPresent; } else @@ -77,13 +77,16 @@ namespace gaseous_server.Classes.Metadata { fieldList = fieldList + ", "; valueList = valueList + ", "; - updateFieldValueList = updateFieldValueList + ", "; } fieldList = fieldList + key.Key; valueList = valueList + "@" + key.Key; if ((key.Key != "id") && (key.Key != "dateAdded")) - { - updateFieldValueList = key.Key + " = @" + key.Key; + { + if (updateFieldValueList.Length > 0) + { + updateFieldValueList = updateFieldValueList + ", "; + } + updateFieldValueList += key.Key + " = @" + key.Key; } // check property type diff --git a/gaseous-server/Classes/MetadataManagement.cs b/gaseous-server/Classes/MetadataManagement.cs index 4c51df2..8190f53 100644 --- a/gaseous-server/Classes/MetadataManagement.cs +++ b/gaseous-server/Classes/MetadataManagement.cs @@ -14,8 +14,15 @@ namespace gaseous_server.Classes foreach (DataRow dr in dt.Rows) { - Logging.Log(Logging.LogType.Information, "Metadata Refresh", "Refreshing metadata for game " + dr["name"] + " (" + dr["id"] + ")"); - Metadata.Games.GetGame((long)dr["id"], true, forceRefresh); + try + { + Logging.Log(Logging.LogType.Information, "Metadata Refresh", "Refreshing metadata for game " + dr["name"] + " (" + dr["id"] + ")"); + Metadata.Games.GetGame((long)dr["id"], true, forceRefresh); + } + catch (Exception ex) + { + Logging.Log(Logging.LogType.Critical, "Metadata Refresh", "An error occurred while refreshing metadata for " + dr["name"], ex); + } } } } diff --git a/gaseous-server/Classes/Roms.cs b/gaseous-server/Classes/Roms.cs index f85fe68..4195489 100644 --- a/gaseous-server/Classes/Roms.cs +++ b/gaseous-server/Classes/Roms.cs @@ -81,7 +81,9 @@ namespace gaseous_server.Classes Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); string sql = "DELETE FROM games_roms WHERE id = @id"; - db.ExecuteCMD(sql); + Dictionary dbDict = new Dictionary(); + dbDict.Add("id", RomId); + db.ExecuteCMD(sql, dbDict); } private static GameRomItem BuildRom(DataRow romDR) diff --git a/gaseous-server/Controllers/GamesController.cs b/gaseous-server/Controllers/GamesController.cs index ed99732..fa5a609 100644 --- a/gaseous-server/Controllers/GamesController.cs +++ b/gaseous-server/Controllers/GamesController.cs @@ -136,11 +136,11 @@ namespace gaseous_server.Controllers [Route("{GameId}")] [ProducesResponseType(typeof(Game), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult Game(long GameId) + public ActionResult Game(long GameId, bool forceRefresh = false) { try { - IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, forceRefresh); if (gameObject != null) { diff --git a/gaseous-server/gaseous-server.csproj b/gaseous-server/gaseous-server.csproj index 74fb485..9614837 100644 --- a/gaseous-server/gaseous-server.csproj +++ b/gaseous-server/gaseous-server.csproj @@ -9,6 +9,10 @@ + + 4 + bin\Debug\net7.0\gaseous-server.xml + @@ -62,6 +66,9 @@ + + + @@ -78,6 +85,9 @@ + + + diff --git a/gaseous-server/wwwroot/.DS_Store b/gaseous-server/wwwroot/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..cd3a2cf86a516c8064df523e3cb54a7528c4c45a GIT binary patch literal 6148 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 lBXbwbf3_9;+pp8IBL*j-1WR9Aazz^rhl literal 0 HcmV?d00001 diff --git a/gaseous-server/wwwroot/index.html b/gaseous-server/wwwroot/index.html index e1e96d8..ccca86d 100644 --- a/gaseous-server/wwwroot/index.html +++ b/gaseous-server/wwwroot/index.html @@ -2,9 +2,35 @@ - + + + + + + Gaseous - Hello World! + + + +
+
+
+
+ diff --git a/gaseous-server/wwwroot/scripts/filterformating.js b/gaseous-server/wwwroot/scripts/filterformating.js new file mode 100644 index 0000000..85dd088 --- /dev/null +++ b/gaseous-server/wwwroot/scripts/filterformating.js @@ -0,0 +1,76 @@ +function formatFilterPanel(targetElement, result) { + var panel = document.createElement('div'); + panel.id = 'filter_panel_box'; + + panel.appendChild(buildFilterPanelHeader('filter', 'Filter')); + + var containerPanelSearch = document.createElement('div'); + containerPanelSearch.className = 'filter_panel_box'; + var containerPanelSearchField = document.createElement('input'); + containerPanelSearchField.type = 'text'; + containerPanelSearch.appendChild(containerPanelSearchField); + + panel.appendChild(containerPanelSearch); + + if (result.platforms) { + panel.appendChild(buildFilterPanelHeader('platforms', 'Platforms')); + + var containerPanelPlatform = document.createElement('div'); + containerPanelPlatform.className = 'filter_panel_box'; + for (var i = 0; i < result.platforms.length; i++) { + containerPanelPlatform.appendChild(buildFilterPanelItem(result.platforms[i].id, result.platforms[i].name)); + } + panel.appendChild(containerPanelPlatform); + + targetElement.appendChild(panel); + } + + if (result.genres) { + panel.appendChild(buildFilterPanelHeader('genres', 'Genres')); + + var containerPanelGenres = document.createElement('div'); + containerPanelGenres.className = 'filter_panel_box'; + for (var i = 0; i < result.genres.length; i++) { + containerPanelGenres.appendChild(buildFilterPanelItem(result.genres[i].id, result.genres[i].name)); + } + panel.appendChild(containerPanelGenres); + + targetElement.appendChild(panel); + } + + +} + +function buildFilterPanelHeader(headerString, friendlyHeaderString) { + var header = document.createElement('div'); + header.id = 'filter_panel_header_' + headerString; + header.className = 'filter_header'; + header.innerHTML = friendlyHeaderString; + + return header; +} + +function buildFilterPanelItem(itemString, friendlyItemString) { + var filterPanelItem = document.createElement('div'); + filterPanelItem.id = 'filter_panel_item_' + itemString; + filterPanelItem.className = 'filter_panel_item'; + + var filterPanelItemCheckBox = document.createElement('div'); + + var filterPanelItemCheckBoxItem = document.createElement('input'); + filterPanelItemCheckBoxItem.id = 'filter_panel_item_checkbox_' + itemString; + filterPanelItemCheckBoxItem.type = 'checkbox'; + filterPanelItemCheckBoxItem.className = 'filter_panel_item_checkbox'; + filterPanelItemCheckBox.appendChild(filterPanelItemCheckBoxItem); + + var filterPanelItemLabel = document.createElement('label'); + filterPanelItemLabel.id = 'filter_panel_item_label_' + itemString; + filterPanelItemLabel.className = 'filter_panel_item_label'; + filterPanelItemLabel.setAttribute('for', filterPanelItemCheckBoxItem.id); + filterPanelItemLabel.innerHTML = friendlyItemString; + + filterPanelItem.appendChild(filterPanelItemCheckBox); + filterPanelItem.appendChild(filterPanelItemLabel); + + return filterPanelItem; +} \ No newline at end of file diff --git a/gaseous-server/wwwroot/scripts/gamesformating.js b/gaseous-server/wwwroot/scripts/gamesformating.js new file mode 100644 index 0000000..0728726 --- /dev/null +++ b/gaseous-server/wwwroot/scripts/gamesformating.js @@ -0,0 +1,41 @@ +function formatGamesPanel(targetElement, result) { + for (var i = 0; i < result.length; i++) { + var game = renderGameIcon(result[i], true, false); + targetElement.appendChild(game); + } +} + +function renderGameIcon(gameObject, showTitle, showRatings) { + var gameBox = document.createElement('div'); + gameBox.className = 'game_tile'; + + var gameImage = document.createElement('img'); + gameImage.className = 'game_tile_image'; + if (gameObject.cover) { + gameImage.src = '/api/v1/Games/' + gameObject.id + '/cover/image'; + } else { + gameImage.src = '/images/unknowngame.png'; + gameImage.className = 'game_tile_image unknown'; + } + gameBox.appendChild(gameImage); + + if (showTitle == true) { + var gameBoxTitle = document.createElement('div'); + gameBoxTitle.class = 'game_tile_label'; + gameBoxTitle.innerHTML = gameObject.name; + gameBox.appendChild(gameBoxTitle); + } + + if (showRatings == true) { + if (gameObject.ageRatings) { + for (var i = 0; i < gameObject.ageRatings.ids.length; i++) { + var ratingImage = document.createElement('img'); + ratingImage.src = '/api/v1/Games/' + gameObject.id + '/agerating/' + gameObject.ageRatings.ids[i] + '/image'; + ratingImage.className = 'rating_image_mini'; + gameBox.appendChild(ratingImage); + } + } + } + + return gameBox; +} \ No newline at end of file diff --git a/gaseous-server/wwwroot/scripts/main.js b/gaseous-server/wwwroot/scripts/main.js new file mode 100644 index 0000000..9fc64e2 --- /dev/null +++ b/gaseous-server/wwwroot/scripts/main.js @@ -0,0 +1,24 @@ +function ajaxCall(endpoint, method, successFunction) { + $.ajax({ + + // Our sample url to make request + url: + endpoint, + + // Type of Request + type: method, + + // Function to call when to + // request is ok + success: function (data) { + var x = JSON.stringify(data); + console.log(x); + successFunction(data); + }, + + // Error handling + error: function (error) { + console.log(`Error ${error}`); + } + }); +} diff --git a/gaseous-server/wwwroot/styles/style.css b/gaseous-server/wwwroot/styles/style.css new file mode 100644 index 0000000..0c5fb57 --- /dev/null +++ b/gaseous-server/wwwroot/styles/style.css @@ -0,0 +1,121 @@ +body { + background-color: #383838; + color: white; + font-family: "PT Sans", Arial, Helvetica, sans-serif; + font-kerning: normal; + font-style: normal; + font-weight: 100; + font-size: 13px; +} + +#banner_icon { + background-color: white; + position: fixed; + top: 0px; + left: 0px; + width: 40px; + height: 40px; + align-items: center; + justify-content: center; + padding: 0px; + margin: 0px; + display: flex; +} + +#banner_icon_image { + width: 30px; + height: 30px; +} + +#banner_header { + background-color: #001638; + position: fixed; + top: 0px; + left: 40px; + height: 40px; + right: 0px; + align-items: center; + display: flex; +} + +#banner_header_label { + display: inline; + padding: 10px; + font-size: 18pt; + font-weight: 700; + color: #edeffa; +} + +#content { + display: flex; + padding-top: 45px; + padding-left: 5px; + padding-right: 5px; +} + +#games_filter { + width: 200px; + border-style: solid; + border-width: 1px; + border-color: #2b2b2b; + margin-right: 10px; +} + +.filter_header { + padding: 10px; + background-color: #2b2b2b; +} + +.filter_panel_box { + padding: 10px; +} + +.filter_panel_item { + display: flex; + padding: 3px; +} + +.filter_panel_item_checkbox { + margin-right: 10px; +} + +#games_library { + width: 90%; + border-style: solid; + border-width: 1px; + border-color: #2b2b2b; + padding: 10px; +} + +.game_tile { + padding: 5px; + display: inline-block; + width: 220px; + align-items: center; + justify-content: center; + text-align: center; + vertical-align: top; + margin-bottom: 10px; +} + +.game_tile:hover { + cursor: pointer; + text-decoration: underline; + background-color: #2b2b2b; +} + +.game_tile_image { + max-width: 200px; + max-height: 200px; +} + +.game_tile_image, .unknown { + background-color: white; +} + +.rating_image_mini { + display: inline-block; + max-width: 32px; + max-height: 32px; + margin-right: 2px; +} \ No newline at end of file diff --git a/gaseous-signature-ingestor/gaseous-signature-ingestor.csproj.user b/gaseous-signature-ingestor/gaseous-signature-ingestor.csproj.user new file mode 100644 index 0000000..46978fa --- /dev/null +++ b/gaseous-signature-ingestor/gaseous-signature-ingestor.csproj.user @@ -0,0 +1,8 @@ + + + + Project + -tosecpath ~/Downloads/TOSEC\ -\ DAT\ Pack\ -\ Complete\ \(3764\)\ \(TOSEC-v2023-01-23\)/TOSEC/ + true + + \ No newline at end of file diff --git a/gaseous-tools/Database/MySQL/gaseous-1000.sql b/gaseous-tools/Database/MySQL/gaseous-1000.sql index 75a7db3..3e0143a 100644 --- a/gaseous-tools/Database/MySQL/gaseous-1000.sql +++ b/gaseous-tools/Database/MySQL/gaseous-1000.sql @@ -65,7 +65,7 @@ CREATE TABLE `alternativename` ( `id` bigint NOT NULL, `checksum` varchar(45) DEFAULT NULL, `comment` longtext, - `game` int DEFAULT NULL, + `game` bigint DEFAULT NULL, `name` varchar(255) DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, From 7eed686542ea7ee8d80ea8ba7ffbd115c1867122 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sun, 25 Jun 2023 13:05:44 +1000 Subject: [PATCH 31/71] feat: added filtering and favicons, also squashed some bugs --- gaseous-server/Controllers/GamesController.cs | 12 +--- .../wwwroot/android-chrome-192x192.png | Bin 0 -> 16738 bytes .../wwwroot/android-chrome-512x512.png | Bin 0 -> 74220 bytes gaseous-server/wwwroot/apple-touch-icon.png | Bin 0 -> 15164 bytes gaseous-server/wwwroot/favicon-16x16.png | Bin 0 -> 518 bytes gaseous-server/wwwroot/favicon-32x32.png | Bin 0 -> 1150 bytes gaseous-server/wwwroot/favicon.ico | Bin 0 -> 15406 bytes gaseous-server/wwwroot/index.html | 4 ++ .../wwwroot/scripts/filterformating.js | 54 +++++++++++++++++- .../wwwroot/scripts/gamesformating.js | 1 + gaseous-server/wwwroot/site.webmanifest | 1 + gaseous-server/wwwroot/styles/style.css | 19 ++++++ 12 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 gaseous-server/wwwroot/android-chrome-192x192.png create mode 100644 gaseous-server/wwwroot/android-chrome-512x512.png create mode 100644 gaseous-server/wwwroot/apple-touch-icon.png create mode 100644 gaseous-server/wwwroot/favicon-16x16.png create mode 100644 gaseous-server/wwwroot/favicon-32x32.png create mode 100644 gaseous-server/wwwroot/favicon.ico create mode 100644 gaseous-server/wwwroot/site.webmanifest diff --git a/gaseous-server/Controllers/GamesController.cs b/gaseous-server/Controllers/GamesController.cs index fa5a609..5e1834b 100644 --- a/gaseous-server/Controllers/GamesController.cs +++ b/gaseous-server/Controllers/GamesController.cs @@ -111,22 +111,14 @@ namespace gaseous_server.Controllers } Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT DISTINCT games_roms.gameid AS ROMGameId, game.ageratings, game.aggregatedrating, game.aggregatedratingcount, game.alternativenames, game.artworks, game.bundles, game.category, game.collection, game.cover, game.dlcs, game.expansions, game.externalgames, game.firstreleasedate, game.`follows`, game.franchise, game.franchises, game.gameengines, game.gamemodes, game.genres, game.hypes, game.involvedcompanies, game.keywords, game.multiplayermodes, (CASE WHEN games_roms.gameid = 0 THEN games_roms.`name` ELSE game.`name` END) AS `name`, game.parentgame, game.platforms, game.playerperspectives, game.rating, game.ratingcount, game.releasedates, game.screenshots, game.similargames, game.slug, game.standaloneexpansions, game.`status`, game.storyline, game.summary, game.tags, game.themes, game.totalrating, game.totalratingcount, game.versionparent, game.versiontitle, game.videos, game.websites FROM gaseous.games_roms LEFT JOIN game ON game.id = games_roms.gameid " + whereClause + " " + havingClause + " " + orderByClause; + string sql = "SELECT DISTINCT games_roms.gameid AS ROMGameId, game.ageratings, game.aggregatedrating, game.aggregatedratingcount, game.alternativenames, game.artworks, game.bundles, game.category, game.collection, game.cover, game.dlcs, game.expansions, game.externalgames, game.firstreleasedate, game.`follows`, game.franchise, game.franchises, game.gameengines, game.gamemodes, game.genres, game.hypes, game.involvedcompanies, game.keywords, game.multiplayermodes, game.`name`, game.parentgame, game.platforms, game.playerperspectives, game.rating, game.ratingcount, game.releasedates, game.screenshots, game.similargames, game.slug, game.standaloneexpansions, game.`status`, game.storyline, game.summary, game.tags, game.themes, game.totalrating, game.totalratingcount, game.versionparent, game.versiontitle, game.videos, game.websites FROM gaseous.games_roms LEFT JOIN game ON game.id = games_roms.gameid " + whereClause + " " + havingClause + " " + orderByClause; List RetVal = new List(); DataTable dbResponse = db.ExecuteCMD(sql, whereParams); foreach (DataRow dr in dbResponse.Rows) { - if ((long)dr["ROMGameId"] == 0) - { - // unknown game - } - else - { - // known game - RetVal.Add(Classes.Metadata.Games.GetGame((long)dr["ROMGameId"], false, false)); - } + RetVal.Add(Classes.Metadata.Games.GetGame((long)dr["ROMGameId"], false, false)); } return Ok(RetVal); diff --git a/gaseous-server/wwwroot/android-chrome-192x192.png b/gaseous-server/wwwroot/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..143a0d60a2b271fe80f4eb1200776a2be586702b GIT binary patch literal 16738 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 Gi2XlZLj^?u literal 0 HcmV?d00001 diff --git a/gaseous-server/wwwroot/favicon-16x16.png b/gaseous-server/wwwroot/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..a28d40349e8b0ad6ff6eb21039fe53346405d8d6 GIT binary patch literal 518 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uuz(rC1}QWNE&K#jR^aL47-At< zd+Dq;YoNsOj}L`7Cq~D4$FAJH;mE024NtK$u`XUSvs1_73np*c6zmnL5xJ}Krgx;r zretMl?@#OY)YU(qXUTW=E~uvrahbE>H6@5@zjH!KPG8t@%m)#k=%QNZJpua zg=|;P>`N3h)^JQ}k@`~jRZ;J4tgqSXi|f-oc}>hZY-$UB6&h<~tgq(lxZYo@($h>#rrm}R=U;Z&i*1u-c o&@$-aEO{hRbvZWYfvDa-M!gb?w9|*Kr-9Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$?=t)FDR9FeU)?0{GRT#kWY&56R zZem_W>tuOJ4b8iRGR=&j-~}QlBSU?1peN}?!w12Ko}wpT6onv!a1HP|JSo&U)DLEeIEMY$KHF*+Iz44t#7R}H4o&Ek=(Kkz40x&@xR8a zyzOUnU=AKc)sgq+9=vG?uH!IPVJNDOyaRZMH;utje1M)GVo!pUk01dpM3*Qyrbctgt%)(Q}wp@oi19+S#*Tb(r z2g9%5hhA`M(qX$wMx_I=`X^&Fwj;jwk8|CQCZwOYkhW%ce=yQF1Uu2_S4_eQn1Og) z!P1%qSlRh{|;uJ-~$Uo)L{Gw}+dc%G9(xPrL|-Iqxj{W8&q7T3Dj zI)i_ajvtB*!#GASDsXN_e!&d5b!WnjmYiD!|Cas%j3@At7Rb62*(0h~nvX39(q zg9A8$i!dXZC^Y~d@*2j$pWgdLHywP5gSKWC!KG;tx^ek8e?u}+Y5-5u(=Xow_xwuY zk7B3!92>D2zu_i2;TCRvB%;&+>gnZN`t_N@p)C~d2Q_#X{#1X8XvLi8+@16ZBqCJV zKBON0x8wX(8dvG~CrtfXxc;Ls6Sc576A>DK6+0BW@lT?6_M(BN$=HYvyi+_n@9{8; zzAG~Yp#hjGM|vLR!s%Ox#pr<#@fkv4U-t$j!atJ$C%53Y;5zqt znXjbHxi_0`ScwP?z+$p;`z3lNFT8Im2Eh5gjM-reI5_L$_jse{p#fZ?U87(o zLSdcw)n8#hEZAvi#tOWIC?4Tt1Wv<3O+;t_7R*76LtUcxy$h#k5uU>a{0#>%26OQY z%!K`nkrPl+Q)4|HKqf+~ zz)l@#LkmpvNf`ETgoFARw_serIkduGJ&V&mrv6QAg43FbQW
Oz&S<4fh4>!QJr% zxIx7?ns%<2Vja4R%*cEA5uYF$ET@%)FSOFXgj@1>)M5?xz(P!iZM$IhcEF$Fm*KO5 zVVcT?skOkG@H!ko7Z$+cb87rDGwIS>jHMU|Q@#?`gmn^(k|T@twvP{7hWU65{a}IZ z#6I{TGtr1&u@&p_6Yij7R5F0*=AN94z%0y$%halO&M)E^_7=5stZ2^v13;I%pM#5+ QP5=M^07*qoM6N<$g4*5+TL1t6 literal 0 HcmV?d00001 diff --git a/gaseous-server/wwwroot/favicon.ico b/gaseous-server/wwwroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df7b7687006b59e6ec5a62f4dfa3c9c510d7b64a GIT binary patch literal 15406 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 H*k<5=>_!{^ literal 0 HcmV?d00001 diff --git a/gaseous-server/wwwroot/index.html b/gaseous-server/wwwroot/index.html index ccca86d..565daf6 100644 --- a/gaseous-server/wwwroot/index.html +++ b/gaseous-server/wwwroot/index.html @@ -7,6 +7,10 @@ + + + + Gaseous diff --git a/gaseous-server/wwwroot/scripts/filterformating.js b/gaseous-server/wwwroot/scripts/filterformating.js index 85dd088..b5ed334 100644 --- a/gaseous-server/wwwroot/scripts/filterformating.js +++ b/gaseous-server/wwwroot/scripts/filterformating.js @@ -7,7 +7,10 @@ var containerPanelSearch = document.createElement('div'); containerPanelSearch.className = 'filter_panel_box'; var containerPanelSearchField = document.createElement('input'); + containerPanelSearchField.id = 'filter_panel_search'; containerPanelSearchField.type = 'text'; + containerPanelSearchField.placeholder = 'Search'; + containerPanelSearchField.setAttribute('onkeydown', 'executeFilterDelayed();'); containerPanelSearch.appendChild(containerPanelSearchField); panel.appendChild(containerPanelSearch); @@ -18,7 +21,7 @@ var containerPanelPlatform = document.createElement('div'); containerPanelPlatform.className = 'filter_panel_box'; for (var i = 0; i < result.platforms.length; i++) { - containerPanelPlatform.appendChild(buildFilterPanelItem(result.platforms[i].id, result.platforms[i].name)); + containerPanelPlatform.appendChild(buildFilterPanelItem('platforms', result.platforms[i].id, result.platforms[i].name)); } panel.appendChild(containerPanelPlatform); @@ -31,7 +34,7 @@ var containerPanelGenres = document.createElement('div'); containerPanelGenres.className = 'filter_panel_box'; for (var i = 0; i < result.genres.length; i++) { - containerPanelGenres.appendChild(buildFilterPanelItem(result.genres[i].id, result.genres[i].name)); + containerPanelGenres.appendChild(buildFilterPanelItem('genres', result.genres[i].id, result.genres[i].name)); } panel.appendChild(containerPanelGenres); @@ -50,7 +53,7 @@ function buildFilterPanelHeader(headerString, friendlyHeaderString) { return header; } -function buildFilterPanelItem(itemString, friendlyItemString) { +function buildFilterPanelItem(filterType, itemString, friendlyItemString) { var filterPanelItem = document.createElement('div'); filterPanelItem.id = 'filter_panel_item_' + itemString; filterPanelItem.className = 'filter_panel_item'; @@ -61,6 +64,9 @@ function buildFilterPanelItem(itemString, friendlyItemString) { filterPanelItemCheckBoxItem.id = 'filter_panel_item_checkbox_' + itemString; filterPanelItemCheckBoxItem.type = 'checkbox'; filterPanelItemCheckBoxItem.className = 'filter_panel_item_checkbox'; + filterPanelItemCheckBoxItem.name = 'filter_' + filterType; + filterPanelItemCheckBoxItem.setAttribute('filter_id', itemString); + filterPanelItemCheckBoxItem.setAttribute('oninput' , 'executeFilter();'); filterPanelItemCheckBox.appendChild(filterPanelItemCheckBoxItem); var filterPanelItemLabel = document.createElement('label'); @@ -73,4 +79,46 @@ function buildFilterPanelItem(itemString, friendlyItemString) { filterPanelItem.appendChild(filterPanelItemLabel); return filterPanelItem; +} + +var filterExecutor = null; +function executeFilterDelayed() { + if (filterExecutor) { + filterExecutor = null; + } + + filterExecutor = setTimeout(executeFilter, 1000); +} + +function executeFilter() { + // build filter lists + var platforms = ''; + var genres = ''; + + var searchString = document.getElementById('filter_panel_search').value; + var platformFilters = document.getElementsByName('filter_platforms'); + var genreFilters = document.getElementsByName('filter_genres'); + + for (var i = 0; i < platformFilters.length; i++) { + if (platformFilters[i].checked) { + if (platforms.length > 0) { + platforms += ','; + } + platforms += platformFilters[i].getAttribute('filter_id'); + } + } + + for (var i = 0; i < genreFilters.length; i++) { + if (genreFilters[i].checked) { + if (genres.length > 0) { + genres += ','; + } + genres += genreFilters[i].getAttribute('filter_id'); + } + } + + ajaxCall('/api/v1/Games?name=' + searchString + '&platform=' + platforms + '&genre=' + genres, 'GET', function (result) { + var gameElement = document.getElementById('games_library'); + formatGamesPanel(gameElement, result); + }); } \ No newline at end of file diff --git a/gaseous-server/wwwroot/scripts/gamesformating.js b/gaseous-server/wwwroot/scripts/gamesformating.js index 0728726..e72a0af 100644 --- a/gaseous-server/wwwroot/scripts/gamesformating.js +++ b/gaseous-server/wwwroot/scripts/gamesformating.js @@ -1,4 +1,5 @@ function formatGamesPanel(targetElement, result) { + targetElement.innerHTML = ''; for (var i = 0; i < result.length; i++) { var game = renderGameIcon(result[i], true, false); targetElement.appendChild(game); diff --git a/gaseous-server/wwwroot/site.webmanifest b/gaseous-server/wwwroot/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/gaseous-server/wwwroot/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/gaseous-server/wwwroot/styles/style.css b/gaseous-server/wwwroot/styles/style.css index 0c5fb57..eb14529 100644 --- a/gaseous-server/wwwroot/styles/style.css +++ b/gaseous-server/wwwroot/styles/style.css @@ -70,6 +70,25 @@ padding: 10px; } +input[type='text'] { + background-color: #2b2b2b; + color: white; + padding: 5px; + font-family: "PT Sans", Arial, Helvetica, sans-serif; + font-kerning: normal; + font-style: normal; + font-weight: 100; + font-size: 13px; + border-radius: 5px; + border-width: 1px; + border-style: solid; + border-color: lightgray; +} + +input[id='filter_panel_search'] { + width: 160px; +} + .filter_panel_item { display: flex; padding: 3px; From f21a926758bde12829ff1d75fe601b25c25da099 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sun, 25 Jun 2023 22:41:12 +1000 Subject: [PATCH 32/71] =?UTF-8?q?feat:=20initial=20game=20detail=20page=20?= =?UTF-8?q?+=20updated=20age=20rating=20icons=20with=20SVG=E2=80=99s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gaseous-server/Assets/.DS_Store | Bin 0 -> 6148 bytes gaseous-server/Assets/Ratings/.DS_Store | Bin 0 -> 6148 bytes gaseous-server/Assets/Ratings/ACB/ACB_G.png | Bin 3441 -> 0 bytes gaseous-server/Assets/Ratings/ACB/ACB_G.svg | 7 + gaseous-server/Assets/Ratings/ACB/ACB_M.png | Bin 3827 -> 0 bytes gaseous-server/Assets/Ratings/ACB/ACB_M.svg | 12 + .../Assets/Ratings/ACB/ACB_MA15.png | Bin 5263 -> 0 bytes .../Assets/Ratings/ACB/ACB_MA15.svg | 27 + gaseous-server/Assets/Ratings/ACB/ACB_PG.png | Bin 3195 -> 0 bytes gaseous-server/Assets/Ratings/ACB/ACB_PG.svg | 13 + gaseous-server/Assets/Ratings/ACB/ACB_R18.png | Bin 4738 -> 0 bytes gaseous-server/Assets/Ratings/ACB/ACB_R18.svg | 27 + gaseous-server/Assets/Ratings/ACB/ACB_RC.png | Bin 2930 -> 0 bytes gaseous-server/Assets/Ratings/ACB/ACB_RC.svg | 141 ++++ gaseous-server/Assets/Ratings/CERO/CERO_A.svg | 496 ++++++++++++++ gaseous-server/Assets/Ratings/CERO/CERO_B.svg | 557 ++++++++++++++++ gaseous-server/Assets/Ratings/CERO/CERO_C.svg | 558 ++++++++++++++++ gaseous-server/Assets/Ratings/CERO/CERO_D.svg | 290 ++++++++ gaseous-server/Assets/Ratings/CERO/CERO_Z.svg | 619 ++++++++++++++++++ .../Ratings/CLASS_IND/CLASS_IND_Eighteen.svg | 47 ++ .../Ratings/CLASS_IND/CLASS_IND_Fourteen.svg | 47 ++ .../Assets/Ratings/CLASS_IND/CLASS_IND_L.svg | 119 ++++ .../Ratings/CLASS_IND/CLASS_IND_Sixteen.svg | 47 ++ .../Ratings/CLASS_IND/CLASS_IND_Ten.svg | 47 ++ .../Ratings/CLASS_IND/CLASS_IND_Twelve.svg | 47 ++ .../Assets/Ratings/GRAC/GRAC_All.svg | 53 ++ .../Assets/Ratings/GRAC/GRAC_Eighteen.svg | 68 ++ .../Assets/Ratings/GRAC/GRAC_Fifteen.svg | 56 ++ .../Assets/Ratings/GRAC/GRAC_Testing.svg | 49 ++ .../Assets/Ratings/GRAC/GRAC_Twelve.svg | 57 ++ .../Assets/Ratings/PEGI/Eighteen.jpg | Bin 50956 -> 0 bytes .../Assets/Ratings/PEGI/Eighteen.svg | 68 ++ .../PEGI_Parental_Guidance_Recommended.png | Bin 16222 -> 0 bytes gaseous-server/Assets/Ratings/PEGI/Seven.jpg | Bin 57204 -> 0 bytes gaseous-server/Assets/Ratings/PEGI/Seven.svg | 61 ++ .../Assets/Ratings/PEGI/Sixteen.jpg | Bin 61446 -> 0 bytes .../Assets/Ratings/PEGI/Sixteen.svg | 67 ++ gaseous-server/Assets/Ratings/PEGI/Three.jpg | Bin 64687 -> 0 bytes gaseous-server/Assets/Ratings/PEGI/Three.svg | 64 ++ gaseous-server/Assets/Ratings/PEGI/Twelve.jpg | Bin 67077 -> 0 bytes gaseous-server/Assets/Ratings/PEGI/Twelve.svg | 65 ++ gaseous-server/Assets/Ratings/USK/USK_0.svg | 59 ++ gaseous-server/Assets/Ratings/USK/USK_12.svg | 59 ++ gaseous-server/Assets/Ratings/USK/USK_16.svg | 59 ++ gaseous-server/Assets/Ratings/USK/USK_18.svg | 55 ++ gaseous-server/Assets/Ratings/USK/USK_6.svg | 59 ++ gaseous-server/Controllers/GamesController.cs | 24 +- gaseous-server/gaseous-server.csproj | 103 ++- gaseous-server/wwwroot/.DS_Store | Bin 6148 -> 6148 bytes gaseous-server/wwwroot/index.html | 21 +- gaseous-server/wwwroot/pages/game.html | 36 + gaseous-server/wwwroot/pages/home.html | 14 + .../wwwroot/scripts/gamesformating.js | 1 + gaseous-server/wwwroot/styles/style.css | 13 + 54 files changed, 4174 insertions(+), 38 deletions(-) create mode 100644 gaseous-server/Assets/.DS_Store create mode 100644 gaseous-server/Assets/Ratings/.DS_Store delete mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_G.png create mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_G.svg delete mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_M.png create mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_M.svg delete mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_MA15.png create mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_MA15.svg delete mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_PG.png create mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_PG.svg delete mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_R18.png create mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_R18.svg delete mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_RC.png create mode 100644 gaseous-server/Assets/Ratings/ACB/ACB_RC.svg create mode 100644 gaseous-server/Assets/Ratings/CERO/CERO_A.svg create mode 100644 gaseous-server/Assets/Ratings/CERO/CERO_B.svg create mode 100644 gaseous-server/Assets/Ratings/CERO/CERO_C.svg create mode 100644 gaseous-server/Assets/Ratings/CERO/CERO_D.svg create mode 100644 gaseous-server/Assets/Ratings/CERO/CERO_Z.svg create mode 100644 gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Eighteen.svg create mode 100644 gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Fourteen.svg create mode 100644 gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_L.svg create mode 100644 gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Sixteen.svg create mode 100644 gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Ten.svg create mode 100644 gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Twelve.svg create mode 100644 gaseous-server/Assets/Ratings/GRAC/GRAC_All.svg create mode 100644 gaseous-server/Assets/Ratings/GRAC/GRAC_Eighteen.svg create mode 100644 gaseous-server/Assets/Ratings/GRAC/GRAC_Fifteen.svg create mode 100644 gaseous-server/Assets/Ratings/GRAC/GRAC_Testing.svg create mode 100644 gaseous-server/Assets/Ratings/GRAC/GRAC_Twelve.svg delete mode 100644 gaseous-server/Assets/Ratings/PEGI/Eighteen.jpg create mode 100644 gaseous-server/Assets/Ratings/PEGI/Eighteen.svg delete mode 100644 gaseous-server/Assets/Ratings/PEGI/PEGI_Parental_Guidance_Recommended.png delete mode 100644 gaseous-server/Assets/Ratings/PEGI/Seven.jpg create mode 100644 gaseous-server/Assets/Ratings/PEGI/Seven.svg delete mode 100644 gaseous-server/Assets/Ratings/PEGI/Sixteen.jpg create mode 100644 gaseous-server/Assets/Ratings/PEGI/Sixteen.svg delete mode 100644 gaseous-server/Assets/Ratings/PEGI/Three.jpg create mode 100644 gaseous-server/Assets/Ratings/PEGI/Three.svg delete mode 100644 gaseous-server/Assets/Ratings/PEGI/Twelve.jpg create mode 100644 gaseous-server/Assets/Ratings/PEGI/Twelve.svg create mode 100644 gaseous-server/Assets/Ratings/USK/USK_0.svg create mode 100644 gaseous-server/Assets/Ratings/USK/USK_12.svg create mode 100644 gaseous-server/Assets/Ratings/USK/USK_16.svg create mode 100644 gaseous-server/Assets/Ratings/USK/USK_18.svg create mode 100644 gaseous-server/Assets/Ratings/USK/USK_6.svg create mode 100644 gaseous-server/wwwroot/pages/game.html create mode 100644 gaseous-server/wwwroot/pages/home.html diff --git a/gaseous-server/Assets/.DS_Store b/gaseous-server/Assets/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..95b3a7bf754364e02cea52aa30abcf7d678534f6 GIT binary patch literal 6148 zcmeHK%Sr=55UkdKfn0LTael!+7()DkzE~o%@f34I3_Yfv!oJ}YBge5(wT2n*9*tQq{C|XuzIr9gkte@-ru4e))N(_fD|}Y z;5xTU@Ber73-kXeNjoVZ1^$%+HrwnrYrazT*2&9xuWj^uy4QTs-M9`4L$qUJv}10( f9j~G&>zc25-V4XXpfewIqJ9Qk7nv0JYX!amWDymU literal 0 HcmV?d00001 diff --git a/gaseous-server/Assets/Ratings/.DS_Store b/gaseous-server/Assets/Ratings/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..fad90d524646827df19966f880c094d797f60d72 GIT binary patch literal 6148 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 C=w~+o diff --git a/gaseous-server/Assets/Ratings/ACB/ACB_G.svg b/gaseous-server/Assets/Ratings/ACB/ACB_G.svg new file mode 100644 index 0000000..85a81e3 --- /dev/null +++ b/gaseous-server/Assets/Ratings/ACB/ACB_G.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/gaseous-server/Assets/Ratings/ACB/ACB_M.png b/gaseous-server/Assets/Ratings/ACB/ACB_M.png deleted file mode 100644 index 7bdaa1d075fde6b97bc285c3f497cd9843fbe425..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3827 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 hJy=T&bXj2sp_*mtyYSFzfAB9B(APGGS7|y#{1 + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/Assets/Ratings/ACB/ACB_MA15.png b/gaseous-server/Assets/Ratings/ACB/ACB_MA15.png deleted file mode 100644 index bec82bd2d94e879d5a75d0ab096212ac4de65a0c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5263 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 N>1i8jRcSg!{|^%jBS!!L diff --git a/gaseous-server/Assets/Ratings/ACB/ACB_MA15.svg b/gaseous-server/Assets/Ratings/ACB/ACB_MA15.svg new file mode 100644 index 0000000..829d9bc --- /dev/null +++ b/gaseous-server/Assets/Ratings/ACB/ACB_MA15.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/Assets/Ratings/ACB/ACB_PG.png b/gaseous-server/Assets/Ratings/ACB/ACB_PG.png deleted file mode 100644 index c505b3fc7cd26643bca7927ec94e49bcccffa6b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3195 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 diff --git a/gaseous-server/Assets/Ratings/ACB/ACB_PG.svg b/gaseous-server/Assets/Ratings/ACB/ACB_PG.svg new file mode 100644 index 0000000..1b3a8ce --- /dev/null +++ b/gaseous-server/Assets/Ratings/ACB/ACB_PG.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/Assets/Ratings/ACB/ACB_R18.png b/gaseous-server/Assets/Ratings/ACB/ACB_R18.png deleted file mode 100644 index 62edcae4f2eddde46607e97839bec8f55bc200d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4738 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 R{7)r-hKi2z7e$-!{{bc!E(8Do diff --git a/gaseous-server/Assets/Ratings/ACB/ACB_R18.svg b/gaseous-server/Assets/Ratings/ACB/ACB_R18.svg new file mode 100644 index 0000000..a65c5dd --- /dev/null +++ b/gaseous-server/Assets/Ratings/ACB/ACB_R18.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/Assets/Ratings/ACB/ACB_RC.png b/gaseous-server/Assets/Ratings/ACB/ACB_RC.png deleted file mode 100644 index 1b0f511bbb516e90abe7cba680e33b7448e2d14b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2930 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 LzhQ=j`k?;-DQ%5j diff --git a/gaseous-server/Assets/Ratings/ACB/ACB_RC.svg b/gaseous-server/Assets/Ratings/ACB/ACB_RC.svg new file mode 100644 index 0000000..8707732 --- /dev/null +++ b/gaseous-server/Assets/Ratings/ACB/ACB_RC.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/CERO/CERO_A.svg b/gaseous-server/Assets/Ratings/CERO/CERO_A.svg new file mode 100644 index 0000000..fff2608 --- /dev/null +++ b/gaseous-server/Assets/Ratings/CERO/CERO_A.svg @@ -0,0 +1,496 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/CERO/CERO_B.svg b/gaseous-server/Assets/Ratings/CERO/CERO_B.svg new file mode 100644 index 0000000..e4a844a --- /dev/null +++ b/gaseous-server/Assets/Ratings/CERO/CERO_B.svg @@ -0,0 +1,557 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/CERO/CERO_C.svg b/gaseous-server/Assets/Ratings/CERO/CERO_C.svg new file mode 100644 index 0000000..e6896d0 --- /dev/null +++ b/gaseous-server/Assets/Ratings/CERO/CERO_C.svg @@ -0,0 +1,558 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/CERO/CERO_D.svg b/gaseous-server/Assets/Ratings/CERO/CERO_D.svg new file mode 100644 index 0000000..14b1a3e --- /dev/null +++ b/gaseous-server/Assets/Ratings/CERO/CERO_D.svg @@ -0,0 +1,290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/CERO/CERO_Z.svg b/gaseous-server/Assets/Ratings/CERO/CERO_Z.svg new file mode 100644 index 0000000..a3cc19a --- /dev/null +++ b/gaseous-server/Assets/Ratings/CERO/CERO_Z.svg @@ -0,0 +1,619 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Eighteen.svg b/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Eighteen.svg new file mode 100644 index 0000000..6e86f63 --- /dev/null +++ b/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Eighteen.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Fourteen.svg b/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Fourteen.svg new file mode 100644 index 0000000..4f7a6c9 --- /dev/null +++ b/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Fourteen.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_L.svg b/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_L.svg new file mode 100644 index 0000000..1a85899 --- /dev/null +++ b/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_L.svg @@ -0,0 +1,119 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Sixteen.svg b/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Sixteen.svg new file mode 100644 index 0000000..c5beafb --- /dev/null +++ b/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Sixteen.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Ten.svg b/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Ten.svg new file mode 100644 index 0000000..506cfc3 --- /dev/null +++ b/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Ten.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Twelve.svg b/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Twelve.svg new file mode 100644 index 0000000..618a536 --- /dev/null +++ b/gaseous-server/Assets/Ratings/CLASS_IND/CLASS_IND_Twelve.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/GRAC/GRAC_All.svg b/gaseous-server/Assets/Ratings/GRAC/GRAC_All.svg new file mode 100644 index 0000000..a09ab19 --- /dev/null +++ b/gaseous-server/Assets/Ratings/GRAC/GRAC_All.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/GRAC/GRAC_Eighteen.svg b/gaseous-server/Assets/Ratings/GRAC/GRAC_Eighteen.svg new file mode 100644 index 0000000..f296c58 --- /dev/null +++ b/gaseous-server/Assets/Ratings/GRAC/GRAC_Eighteen.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/GRAC/GRAC_Fifteen.svg b/gaseous-server/Assets/Ratings/GRAC/GRAC_Fifteen.svg new file mode 100644 index 0000000..6e997df --- /dev/null +++ b/gaseous-server/Assets/Ratings/GRAC/GRAC_Fifteen.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/GRAC/GRAC_Testing.svg b/gaseous-server/Assets/Ratings/GRAC/GRAC_Testing.svg new file mode 100644 index 0000000..72cbd9b --- /dev/null +++ b/gaseous-server/Assets/Ratings/GRAC/GRAC_Testing.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/GRAC/GRAC_Twelve.svg b/gaseous-server/Assets/Ratings/GRAC/GRAC_Twelve.svg new file mode 100644 index 0000000..2ed7295 --- /dev/null +++ b/gaseous-server/Assets/Ratings/GRAC/GRAC_Twelve.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/PEGI/Eighteen.jpg b/gaseous-server/Assets/Ratings/PEGI/Eighteen.jpg deleted file mode 100644 index c4fc8f24aeb87ce7703eb34ce53acabd24ca10e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50956 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/PEGI/PEGI_Parental_Guidance_Recommended.png b/gaseous-server/Assets/Ratings/PEGI/PEGI_Parental_Guidance_Recommended.png deleted file mode 100644 index 575c375c74c6e1265b567e8922a62dc578ecb5b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16222 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 Rzg~Km6qOUH7Sa#+zW{mkeJub0 diff --git a/gaseous-server/Assets/Ratings/PEGI/Seven.jpg b/gaseous-server/Assets/Ratings/PEGI/Seven.jpg deleted file mode 100644 index 264cac2765f25ad708b9e5a2aedf01ee62438095..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57204 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/PEGI/Sixteen.jpg b/gaseous-server/Assets/Ratings/PEGI/Sixteen.jpg deleted file mode 100644 index 55fe2ecda48a4074d76624093fced44e45f09467..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61446 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 JX6;MQ{{wbuDk1;? diff --git a/gaseous-server/Assets/Ratings/PEGI/Sixteen.svg b/gaseous-server/Assets/Ratings/PEGI/Sixteen.svg new file mode 100644 index 0000000..b2eaac9 --- /dev/null +++ b/gaseous-server/Assets/Ratings/PEGI/Sixteen.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/PEGI/Three.jpg b/gaseous-server/Assets/Ratings/PEGI/Three.jpg deleted file mode 100644 index 2d6c734fbc092705acc128d0f5326ea72adeeda0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64687 zcmeFa2|UzY`#=5}`!1ohShA#K8#^^5JIPLxEMx3uFk_EtU$iKSqKHUjNp_M-QV~(e z5>XNn+1HuhnXx3@-FNjo_w#>!U;jQe$N8M~eXetz>s;rYYvvGN6W>BBbTxG}As7V= ziUj{4Vh7(24SyF$2-4SwL?8&FgJ@wZAxhwbfqxKeEksTBLy$de-J;(WwrM^M1xN#j zfD3H`A8(ix+0Oy|(Z^jNs`;Guz0&ZnhW+9sID*EYeVmbMAdXC6CDiDI#A-nr5sNE>(nb>{vOm(rCK24abw zOuKc|)NBlm4K#IjYCtfMnn`=73mQYQ0)jlyIB#RE9elej_wdnH1B%f@G!Ptu?2uSb zeG^SnQZ+gnYJ6Cbh~)qJj5Uz!gcxOZX`bisr4M*03h&lk^D|`eo=CoIsbN&-@yaz0Md{!dpdYHko=E;|Aem( z3iv5^0{<~z7nC3HzXEFCxV|={%u%32E6rb2~CjRY=OJFU* zA!>|rM0umopel2~WiDtZ(A{VU7n}2t*GIE zDEq4*Dt1PQa&Ih%fi3piPI_~QH1aHXyXW{G_(43`{l=jP1FsZV7biYau$r+cAJWI$ zm*fXyg5*#`3=k{C0j+^}AORqE#ULq29@+|RhcqBv$N(~hETMf65^{z-AaBSI+7BIs zjzOoObI?U78j6Dwp%f?sx&!4yccBN+W2h2(0o6k-&|9br`UDL^W6(4V2BU>B!`NYK zV7xFP*cKoi6=2FR4cJbYG0YNX3v+^@VLq_^u*0xZuy9y3EFP8u%Yqfc9>AW$YGF;V z4pM7n*e4-cyqMDI%73Bs>F-iqWHA(}@J(P}=-jpGfp_Gx736vR>MU)kkFDc(r_EAn! z(NM8d@llCUZKcwtGNVFLc~ON>ou-PWx=EEs^@!>vRR`4|6@i+WdOh_PY9(qN>fO{% z)PB^*s3WOwQ0G#YQ`b{>QIFBk(5#{nrje)7rm>`Pr3s=rO>>ndljc56EzLWcQCeEs zHMCo35wr%hcCu9@ZC+HaIc!zEg zUqLTKuS9P^??4|wA4Z=@UqoL+-$g&oz{ViVfM76XaAi2iaET#X zWz22N6D;g3;w(BW4lH<<7?wPiI+o9@^sGXxs;v8116d#wh`T0hFo!>z^b$$go-jJuC#70-4a zSDp(z_jr1E*?Emh$%Uaq_A1x$|A(d&D=mVf_Z}4L%#plP6}u(YA-+m{r}!c9eDTi`f)aZq&P!BC5F}+K-6azxTi`3NX<)0^R=2IGTR(0S-iF$ixa}Q+A7O`x zN4!<$Q?^yUruf=cIhf$a|4Z*1>Y-K^@Wnx^`BhvW|L9l1Nk)s)nN)$XfP zs%xo-s#j~Q)G*VC)@ak@*L2dnr8%f2s}-bmUz=K6Py4)fqYk$YQYS@cP*+|zSoe`0 zlb(rQv|i^DY6VK=ccNi`WW-C-JL+G4iJ%*X7ZIji|z^Az)O3w4Wdi?_Qab_egSv|MZHVp*_< zc8~d<8+%5rG^{RKb?ueidu(rmwXn6n_0xT8_qp#Yv0=5bv&pulwzaTLwVkmuuuHHT zwb!w~Vn2vfLq;J#Ic#^h;Lw9Ypu$nzjtIwa$B#}3rwFGWXBFp2=ROxTmuQzSuG+3~ zuH$Y7Zpm&$cMJDS4>}K9k3#fHv>W;nh6m%1srB6AdCc>zmy*{-uR(7;?;BVc)*4%Y z;{eK3mCq)hqdpzJ%Dyqa6Mp7?+5T+)X#c7Jk${r{-GQ2cNkLRW$e@S&H|#&Szav;R zI6edxVjuDlFMvOS?>eA$;O0SwgKh_(ACf#2erV*d#o?kO+(!-^d3RLn=&fTc$Gnd< z9^ZOA?gZ5dmlM?|rB7ZyNeFcaeRfLnROG3d)5z1$&cM%HIzv3`c(yuBE-dyO%{lbB zhVv@tZ-%pl2Zq0k(2vNwz;_|^!pKFNi%%nEBIBYMqI{y>UfOx7;IiQ5bC+kLoulhx zcEn^}S%2l^mGP?%SLmx$L>2d9b{Iyup0u{I>;`1+|5`g%69i7UdR;7boA{a5tue zvn1>u!@VQ-h^2w0qxU`U_dRfW@b00_!^Kn^)YDMW2#FM*E zm7W$pQ+SqNDOZ_SC0mvIT=scxwOn;xjeJeP3&j^jwcBd%)v44yc&YyK@hhEImG!&o zYa7fOUN`P-eEZtr^~WanrZ3ID&C@LhTB%z@->i5O*|xqdp?!0E=3BY9B^~M=Rh=fC zE$@);KE21jpXoaCf%!vZH*a^!N2!l@do+7q^jh_Hee(P?-FK{?t^dmBO`mfHwhvSd z?jG#=;{Am?(Y7&tVj{1cY}E8281Xa2LVklB4gk#nMr?tOKumOWbo6vg^z=+@j0}uyoUBYt ztejlz?40cETx?9_Xa3LJ%W@aS%*e>h!n}foWd#Qd3kwIyVd0p|!uB%-5br~5bkH^E zFeQv1qF{qjvcZUNfbK$1d<0VgD5-&Z0UiLD^iYE3C>1pgEgd}rBUp5P8wrCbC>J7E zKrkv8B?T1)H7yMtH6@cYh-9Or+OQH%t!iY)&hK?liiYESeAW&DK~Ccb_R=!mhicVm zg~Ck+JH8;Xva1sA9NwrNVd|jq@H$Rzb)8u!YAE|i*-M{y!~5)5{b*X|h_5@k!M&ybH4$pxQ##DsON3^diBSG= zLbzV93PG3%jff09HE!R1rA9hh@s6gVAaow0DBrAGok2TOF;z;0d`tT~h)`lU5xPW# zEQrwO#@V|nBjN2j8slk1sN0DkrI6HG!24~{1mvs@5h}JB8ze$?dclJCM&oAsh|psT zj^ZfFZz6T@Rrp&(Xxxp^#yh2hxgp46Jl%jF&L%=@_8NcQ%R3o1iYN5rTgr%#jt~)o z6Eca=o6Ko{e2-9xK)I4lU=6_+|87gDb>+ctVg*Y-sMHgo>0Ba&VVxDsuhSc_Btq3S zGbk^;LQj=%BBN#=FKKDyluqy`mB}U|bUI_RpTs(h##E-hqMyyldRIr5_MFeb#*LOe zQ)WRtvkkjjJ_Y8~+)^=(dnH*EWF0w;?VZ*T$~)AqD}VfTf6V1x+02pITUNq19QITv za!oy`eo}rfz5K8x0SCGp-w!A&X~C;bRck> zqq*2b)LM-D*2wd>_NoLNrT4jNLS0ypkp}C{f1`JIy~;r56wBUoQbI`V{`@SZwGa0^ zB6!(faLx$V5vepyHW&s>usg(zS_vPUIaM~4=qs{6Zibg|6pV*Dy#TaJE5s7* zX+~bE_02Kee51&!+=H2SL$23p==FX7)}{$GW7ag9cPfww)nLZcC60L>J{L~=jG71? zc^a0AifSlIDhRo1N;8#5(b-MU?ELa0VfadBa6aoS0^glK-H2%H&uASh%9riul+n}X z_p^MPjx8*ZjG$-X-+tT2T3Ss@@4Al_cKwtTWw1GOKUSThsToyzw?4Hn?$#qh-OCb# zxO=%T1)jW*+{stuR2)Ia41w_hNR2cV5iR@Vr$p!h-~^l5S;5kI zkI+jw8)DtB5O1?w!vs~2U{d@&7?+)+R{q-8+tj#u&%=n2YCGlRY7ebpSt4W_qTv?N zb^S`!8m_^E`J+*q@@R`4J^AlSCKZ`ST0iuqAKOUZ+O4FLT7M^8&xwVr^S}}Gag*Xr z9Gmq{R~Fbr`P{E`4-iv%%F}><;oA7gdT)LRGc%S)-3oaSlS70O-i+kqlZa4W!`iBe zF!-7$$dmUdc7}}tdALgP*+^evxQ%#( zUPofVgL@uDeECZR5bQ(td7?Zd0y@%H8U(Lp3n`BwLS7jF&YRh%52dzJ>_2o)K~J0e zfweWlB$?s$%ZS~c&Qz;mX#p9SKFiYawFA>0?XAaAzUKXrSyg^p_tHu3zmohQPFUPQ zUD=iATA=tz_{Xu+K|C+U6cm#U-#YL{hkqbsyjQxLk2Lt$@8XHE9jhq7TgK_&P9YFPY_NO9@_`EJm-V0(-FGxIB9;o-Q4_gW)$DRdr=_Ew!Tch24(y?khT=FaS?Bq@#_wel! zezak;?Z_e5?a_Ug&#Vn*CzvJ!gj3-~xkq0p`HZ&q-Foc?OS+i*aJ`NIc8q|jwMyBq zyz&^UVaDb)jQvAIs5O=1P?X|m#FUTC7n||UBH5~0TYo>JW|apG4RI&M@w1bxC*nR5 zyhEk-XSnJJX6?T43!`8(BQwtfE+YuO55;1C8hrJ#B*>cwE?pj3YNEOLTQP-k6P^+U%cow&S@J zN5c!gQ)>@ZC59UXHCgHPc_P=PU{Axo2yu#c%bpSUVs=vx_lLx#xA47x-*OKBu;3i4 zK-S^znLW>Tw-O=g^>y@agKQ>FbbexEEn>HCe_M?7XLg3Gb3wS@Cg(?hmVRr_BjCVgy~ zK06({?@VdIK*3G-4WYVo-uZBIj?}(8$(WSS=@sD*_ZU=Q>da#r0j-ILF7XE6SG7RNh1}q@7K_-Vhki_Nn4Q8t-_$c}vFK<2J~2fiv$f zO|T}sDp{HwSX<4F-4+qY-fDj)0SJ1tfc`vKpbb5rO8!aZ!Hfe5vP5uv9-pB(Tv@(HbYf{su3 zHJr_z;$$Xs!b?njNBzB*C4}VWz zTBXBx+#aCcJkBf3%s4jf24>L{hJDFVqjywZS`i_%3K0^FNA4+6y`kItYHSzYO1O1qn2!-n{C0WVweKeScy@4*NO1>6G z8ATr>W_c=GtVG(ClSll55z~#j)ls81DL!qtMv;s+X2YA;ogd@5CKi;whJ(8rl@@f> z0;u2*ENrGkN?%&_z5;bU4*XGFeUkSwO-|JXiUDSJ+MAymmgCfhFy0*Bm7Z|LAiMBP z>VOU*KgEd%QRW%c-@7=9A0wcHDN`(74v9SKxzvbwZs)6JdA=7FXl3IU9~NxWI4dz_ zV4kFQCl*P$E@f1-`L^U$k3vhTywv9z?&lhJ4+OO}$iJ=-8M8ib8D!&etzmCtn9&DD z9YFl8lUs7GUk~2%{P|S8??%R~FGVl2Sx_`B^c&B;Xm0WSf^n+&RjI`*6Fk#J66j<+l}0E$de}z4>V73!&RPcX`Np#hW*O_A zR(Bsi3uKc`ZW~ztfPSA6IgZ*kc5ky_xCoy3q603K_OPnr`Az+TD2pK4lPUcvI>`bwFoGz)T%yh;6(oOZFLA7O$oJY1*PPt{dKOD=7 z87q41P&}FYFflwfsyEE4C;gM0~tt}?pHp8dFbcD7d*w-bST^sw0IptIe@$l!Dx1O2hsiw#pY8fXi1C*9&ii?O{^?mp>q$gJnG)`vgJww2T* z6(c0tiO{K8!7tcA3B#T@LCL%pNh2}dF3;{i&x&$rG&|j5;#!^WlUMJEGRvx;io_qu zp5-*NdUC5oI&GBa0(yE~Q%{$S^u|-SJYF?7T(&Jbr54>E(eJGrBz>_~$>iR32eZiT zkt(%U>-#F*u#5-AkOLzd#v09<)|IeMAWnteJ9`zQU;S*_j$5r?0xd^pcWQG$t9S73 zF(RZmqVl$`*V;lQ^}W2Xa;?Y3+h&GtR#scmOLs)O+?2i8P;VrJxZ%{mbIQrGw@LJw zTgtPkdov6?YbShl+}6TJ9sAqbCvp#Avs6BCUlo4Ei}5pCv*U#H$J4$hy`Rs-Rp(>s zEH?`#=RBRNs1={;Zy2%;$2<7k6*z$1-N3kHL&nuzGg?YqvG>vmA$`Z*nn~upIi5m>bR|?lP))5SkBQ_EHY52dM z;!BH5LJB){^r^^4RS^4n3UHVOLF*}~AzEo0dv|aqI~OlR-uzi%V>Ji`VGmr!F-K)? zx@`hhgy~j)IH;oWp`g8&xNQ(Z;DB^z-N2Ud-XmUr8-#~{fc_W>+XU?Va?rl z0)qw!_Dwz1GZ zc0M?a77C5>w!@K(EFj1ErEgsmLYeF_IKWWfg)@>ZS(XKp$JHiMMG(nShC*eK?4Of)9ArO< zh3CfV2J(9laLN1YT#zc{0AavTLqUAN{J)7yz1+W? zhYuJpNkZb_L+XAq-nr3F3z1|8Ktk+L=A5@qL1V@3NS0sXNQ{Sszn!N9TwGED+NR_yyAI6iw6;CuxCKyfKQpNj*6-$GVjQr}Yz1b)C4U4zxo{=mCk0FJ^O2L_+z|Zaa}vJY`i8(l#2dvYD=sC5l$3A#eiq=J%+EXn3<{^19;c{?~N8LMl~*9G1X{7X!3{G`P( z-cAz01W8dsQc6NfN(`hB!v>&ncK%{$tiVDE>cH~K+r^V?p+YLr4#*}Pf*&9yH^Bk9 zSgI!&Rpc@qkP@hQ>o`CzIH^^0&GqnHO#GL0U6N2=|L>D}cq|r+#R03DzfX-NjS2~4 z6c(8JAWz(Q46oQ5xG&foR+x0yA3fz{t)b{BVdA1n??HjMXQ7qTP)nROQG1&m;7 z==9fl7T63Y;pw!bmFg}Yf1iF1C`g7vV}U^t=)*D@B{GTE-nsAnhNroaJamT zq`Z`*oB|xKAgLg&s}yMuOmKGII3G_#Z;YdhJD|3q zH!yM~;{&{_q-}>acfq>YyQ2)iROLyEL-1?bxnog(k;K&74h?MVNVabQ%Tw(#1YL|p z@Wc7}7THUQJZI@+96+_wOT&Sk>LQ-+tRngONc^S*nhywPN0M3OQpth4NyFdWXFgC? zlJo%S37+{}mj9ICYrGlS1+*1Vi(K+9P?Il8-32s)6e=qxDkUoqJ__KYC@Li1$lu>&I<@D1D64@Bp09s?p%mG*{?uy6#-7*lG7;40>8WzDO?_O z0&qcB$-ossnIxB#S5bkKS5bkKMp1#3Mp2QRMiEZ-lS)%0m!_yls!@?#rXmRm5LI%b zpgTnY3y8{z%8M!h^D96aI9wDiB?^}oh0BP7?gfe9@}htjq(EB0A%H;u@5ll+0$czX z2JpdFxT=(@tfCrR6%1Ob9cqez_8Rhl{xUn@Qj!{QMbg|2L@3S$=Z>0J1XSH|^FDVM z(yXB5ZifcbkH6Tw!pW~Zzues>L7L^=T|jEGZ!V+6lH!0M1ENhG6cSCst0p>^|hMlJ;SpSfPkAx=%ikf&LQ2oD5sKa0D5(l*k)LB;*MGg)d;tmBdFb zimWuwry?!a5d4y)d5oNutdQ6NO8iBeNvi@aB`g`!TjTk6f1 zk0TcizUics11OQ-g$Iy{`CTx1uXsu5k}>ujKr-Fu>;7K@ z1RC!`JO2(MiDG=lq&a7f82>pLmsC7g<^mzVi})2%l5x)Q_kR!SB8C3jK()NV#^@tVAB5-sQ#QZ`E`;mtN7nV zih#@V=_3FU_$(2Ml6-o=`|F6|GU8xU5q!0AA?t!5{ULD`mo;*p4M^XtB|TD0RlM(n zsTu-&{g#B*5}_aqApZv9Wz{dzcsZ^GW>An_5TSDd;+NQ#3|E0n;r}vRg=OtnfUEc) z!<8k$C9%bSmAp&zAabuR;QfwIex1J3vi|}*NYB%8ZhKElX#wwl#tx(rw>aK@gL&sc zfz7kInR;Qg{fHTsh5s6DrATA#KVk;B;t!<5GF%J9{STP|aPEHycsZ9YN{4?3u>1l) z{Pu8@UpCzTWxzn_lBSGB0r5Li#_|!g2>I(2@N=rz*Ytm@+>)jXeFQiz1JfKy2uuIQ zxLZCsEMk_PXAH1C{Eb-qff)Xp{O=(zG6wMcmKgq3zF40A*CYj);pW*w<~Q*!pUxHu z{I!$6C9upg#{V_I@?^lIdE+;yIr(J*;Va%{3ekdSTbBOU;F53xJo+jK45+Ox`ECmiu=4RNRUkm>j^LH@| zpr!triN6N_gp`;={hNHTeAq9N`G=7IBR(ci(EkzO<^1|D0|u()!V(F1e{YGjT$B7~ z$jfugGxJwb`1fo9=H~^_fZ)G1eSEEIei$q$nm^==G| zTrAg2e+@2>5&9%SVTq9WO}t-+_%Z@7k5~{D((|gc+`M4=HNXI-J_5|mKp>L<|Aus1 zUjHJ6mrukCfPcUci*ow!d6+CIfMbb}BLV&$dAuCJBI4x{3y9_BnP_3jH&5cPJ@OBb zNlXFI0>~s$A@dt*rtGr$U=i|iXp4}SWtfNjwX^;nGMSG7Z_Gpf4a8rGvy6|wLYRyA z2gHlx_wNA%Enc6*!j=fKV*ESHkFP5DGQ>Y(i{*3VJdMAK$bW#m$QD3L{yk1!J|Fxe zTP)Y6f02{P(gHB?{Jx;{e^Xq@E#ue)zF5A(THxgQf59 zNap3r{sQLjCc}BqUpeXTS%WO($e_urA^#hgzgD%3n}3A)2dwcmISKNzvq7yn1FiWp zIdbHZ1HXmzWHXXQdlHgoNuoJ)OHKvJ=4HU#6<9I<=tvO$n@56ko5bV=KOpXZRh?Ka zJQg_NEBWRcI?o6{Db+|4g3JcwZTR2eg1If~@3sY(!&-p*gTeL-aLHTnzq>gxKc>Hf zyByX6+#g8bpM*=EGf0wwy!-PT)Lq``i*jKRz+9&OWwfz&7FxKQg=D71@bPV`mZqqd1H;l48Kd= z<>LOIm|=M<{tE8@V0O^*whO=_?c5j6d&v{WoT13?&I+W(`?rH%%UIQNel_)1h9XV? z1IfZy_HR`WJ+Qs}&0f*Zl`5t952Y&nPN|a8|4`~O72~I{!sQkIp;VGB@si!LpDPvE zpZ}e91Qw~kQ>v8wKa{#`S@v^SCE@>2>asJ1pDI;e?jH)3`V9gAqWwKpmo1ro3Mp89 z|9zp~E)agIO!!>)|Gq3)mXl<^w#-)TM~W)=o)k%EUHpQU^1>IX{`~zzfj<=ZLxDdO z_(Op|6!`xU1-^d88-)hnZSezN;Qm*x$pF6>@aLM0h3hl^T$AzVnhfw048M8b#cyT< z6)t`LT$AzVnv6f!Wc;}%P*JOOX7-KuoMdmKXn7el3r(BA! z59Q*sBq!+_2#UpspY@P0#(+7)gehU*CW)^XV+etZF%kew8VYc^!{S3rM?+0TPsso- zBLo*?Ks1zJ@5}&i)KrwT6m&3ph=FkhL;<4&S7T6;5K++3`@q1Z8En)mX*STZ^FwrS z4pk`uPI{vqtLy}&R~vgBX;7xkF#V6Vy$y8k+K2$W3PE z7HjW3tm_Kxy z>eA)t#H1U^H&aq`a`W;F3X2|JvQ_rM`lI|YYz|iQ_qG!#$Q{Udb zQO}!F6mxrP2=sMkqal zc*eEtgP+khJGbaYv+JItfrL-;SaY*ZxRidNBtpuJM94l)7hKa&F}?!)?5t{^&8$4{ z7^(o@BCMg(;1NME!M|IHf59C@Sc{oF0DfEf5D|KvTvs}qH9JjUV6`VgJ>UnTv&G5? zO-kdVF%CrNLi@y{xDncYMCe!y!B%DXZRyzNkxcOGxcCqvbm;Kw(kZk`U!^Db>H1wI zvyM$FUGfCD@GPvcwBrW2F@$wyLSi;FcnkP-Xx{eHx7Wrb2r*&PQmrce5t(?)JcCJn zaLa}c5!&uE@E1)OnAn6kMTBm=BUI(HPCI;R|I>m$eej2i{)~e^vgrSsG_92&Lh5f0 z62MRYzpxo(op=`4vwD^t?*k@Bl@GS~fYL%DbYJBR5n4(3jIXF57>wHljM;pykw4Y= zc^@Ii-KHM%DY+5<#AzJgFwrasZcEwE%UeD;;$Pb2_p!D!gnMmPZGOG#NU3G!j8$3V z9^PZow{$Izx#-T^Y9zjh$9Kttd!S$+C1z6b(_KNlt+4THL@0T64euzpn8%NC*STT& zne!6(W_R>?|&E|iWewe{r=tq!UrOBr~gX} zcY9oU>0Om})=uvDoT--St#_qnPaf(WI2?qF$T_XgU+wl(HvuS#z`zP_g1<&jsiNl^A=h8fPOGIFA$SSgB~rRg!6 zC+*Sa;F;;_c-KMPx^Xx6H&3rTTQ9SF&COm5w==aOJ$r8Fveor674@xoKTyoEx2NBB zoYw8b*no=D(AG_B8i7o zWL=`2b|*R{AJbyMO&+N@@td{ZY(IdP^0twgY7f9v#XgMxG?|w%?s1(*?y!nc0W$rJ zr^4x)c7(p>mX$9Q-Q&X^vmFheD!jcasHc^alak`V|uYI~+FYa}*-#ZrjJ+55G zC9I-bK3Shp1vf*9DSyDbD?S>sxWnr=`%ovxV|-hsY~tj#!34>#+^PXcJ1c(*Mm7%o5bC1MO2qoQGz=)#4=B3S=_VO5c`C=XNc!Yjlioi zjaM9#%<0w^7aUffwQf0H=H$jN{tzx}Cb{JftIb-Ibw7M*(tuwkPC$1ilqlYwB`pwqzY|kB-qK&l&k4WA(6`_jXjQ74@nZ z65e*=M7$B3@8ec8bu*ZmnZ|+ju+K8nZ^K*bpSVAY+)8DpG90+?RAKS#vnq8=n*#Q*4E6i9TkR4{K3{EN_AZM#s?8E+P;kAoiv(kH#+~iw&<4X1U)a(i0PrFZhKe61xj(WJ3v;Upin|Ij5p2t1T za(KR|NXmsooUd~Ta}6i&4V>=uv+cL}z}t=&9cl`WFl)G)^6K@~qlfR;S$ybjyU?V& z-a_BdR6nNtRsXa7R0@THpNjg$SacJ1gg@|qoI(}U9{l2UzKUIdK=UUntMU(G*|?y~ zLa5VL7o19M1-v$2C|GUOk)Uc8D7V@?x%F+7{hb!hy?eDb+dIp=xXi-rwy)#;wkbLS zGZ7MBTNytq1moCqOqHQ5)rhO`F zw2z&2eeW|J^CWcpIkN$kHW~?R;pzy*eRa*fTRzI@6>K{stAiFeYP3^G(AT*#r+UA} z>BAjk5!NF4XJ*j8a=tgyxSbs~UJ#{*?{M8>5*>h(z)RSKB2IphM~-DQ^|@D9KDQW% zZG47F+gZFt;L4Lalg=$(Tvzs+POgzI#-jCEG#ul-pK@T;ISvVKd`pd(7~g6;t`^XB zwZb;%?lDHObg?UY1)a+qwCoMFM6V@r8HV4;?eCGBE__h<+(W2H!SwXzSI~2va{7ur z9r!nUz>QS=?K4AdQ$Q8C4fK%Hw6mQ`Jm9iOFbE8JM^9xEU;ZyS{5chSy*@WWM(UO6 z&Q1l}L-Nrs)&-_#H0DW z?|v+t<|y4LtNgLV{SO4jc%sU4(I$o!C?VF%9K1O%a6(2i{F9r_kLk#9%F^AD5_9Zt zhP5ixKQwU?*&`L#%))k1#UTCos&=2apwH}9L*eRheLr6x*-6npjcsLyGex`a78l=6 z$)oH?5upu;okYk1`TqFKkV57xqc6CfZEwjOiO(kErdKDOmAC1VZvrS zi3q9eRJ1u(D(EFLq}Na{doBgi3$5|kAEV=WH%W{Kz2_4}!>GFQ@h>%Zu9khS9dK-2 z^ZtEQq@;hjmz!8fxlUDFF0H=ugJLaX4|?8>CNl>!hd#2lz6riJhIDz;B8}@mpSk0n zA7w;&nZUlXas*0LvWjb#0Z#knvuL~G))+;HmzN4136IP##?sr<_Bk1*5AmJbUnCyg z@S%M=$j=~Gn+UP$u-e^=7##@t^d_jiXnf!J=aR;f=}e!W57=X~lnV!zaA9O=H@GzZ1whd4QpHPk$KFVEs#5SL%^E?(+syzWU6;q#%YI$AW>9Acfc z?e|$9=x*v3A8MO@UYwp+Q5j^~7?W1YZy9#+!eb4yTiyCVjDU+`qa=W~WOF(SH z5O44O$tyn2S2bqtdgge(L={KK%k9gp7u>4nVfs|#)Qydg%EGG;TxL4VDThkpi02AK zox=`nD=D0g?vJchEtrlfl6_ta%?f36oD4CpIVt#YqWeW`TXjP{uI2tsWi^${op|Lao^tEW z-z){vOw^cn8k@Y*;27L_hU3NawUr!M1tMRbt~;isWnxdq7lw=6^x;fLw5%R!&si5_lV)SFtaj`ZQANTP8A%0U(fzXLCbbhA6WpF3Mi*0^ zxT}aT;mz?>xl^i~di^e8v#*wRPSc*7COG#vxVAicExo$wlj>C*HSaD}Dz6M+gy>yd zPOZ2lEuV;;euF6<-x{M&KbyLl@0pTt$T^{NTMm;~_&zCZ#?9_HhSog2lA??< z>(Vc*5+h}mlVNch$6MKJ<^MJ2um6{sKdQv3aa~;Qc%-mHzmlp^#M8(2Zm)WW~IN-Uh2P`k8^0{zxoGRoE&u{k(h{AM*?8|Qq-ikVd)8M*Mn8e2ozb3o! zNQ8!A^|K_itBH+`^cafznm)>^xN}4Z8D=qDhghAhB=1&!xCQsrEGjj0Rclbf4UU~R zLh=wh_Zc(JW+>C1KZ+26Ya0&j+cBiFuOxl!dSr0QYh-htHdV~&&{7lJGpc-+EfE#! zt~!IAw7M)pSF&@i=6mE#yYnk(?48m7P%(4(ML=*af?4_f=hI4mzlNnY6O;9k)!_En z#(BZV@Tq~Bo*9!5N4?DOhO^Ehd>8j^j*pZRd3OI6r9C1Sad)+$zK0KkjtBom2irB4 z+VSQlhN@hAhv*)B+8KY)97^eAHSGeoVRo8Y`;8-F8gfkJp543|ATLhAC%A4$!VT1l z2IQfG8(J<(RbG4Svv#v6CD#?Yfb{XO>AS)k{bxp>u1X8AZjFiYio5Q}AI2N^5-ua+ zYUj#u={4PbtFgyoTLPR*JKv8>49--H;2(^6W*YcoUReC%;ymZxQk`}bTm@!_B|==R z(~o?_@L%WrhJTskGOarBn&tJ@7=uErObFsjsY}nD%WB(vZ$9eaT64QVmbv%nT^12n z^Ji@V`!RS1wtBGO=gl=5q7^K=hxTm_xaFr2X{}borBDV9)9QRdanL^UV76fn3=hD2 ztTI33vU8h7IdQgT7Y{yQqNm~!NWF9G zmcY$T8g$CGqnXE_#zw^?2B`?;_k>$CPvR8c#90W)oStw^NmL7qwig|}6qp$3SNQ5< z*0b#^VFPDlCxeg@xp&8dv+t+*t~>v(sqnd-z1Fdc=N-04j8y#kgK0jiG3x?aGI=)4 zo$XeZ1e`EZ`lwWz}1qmR_)f{0BIC6&P0#c>a86J^7^($LYKc+$Rkx2Vdnr{}xc*ji@^KSuxE=1RSo zEUZDz@$QC4*GMOb)Cbl>v@c*2l#WGLol`!9^?x@kf$P$Ms| z0=LmejyK|GkJilCstmBb0_)brHR`{)1(=iBm78Ohx>X`Xe|5$t&uOi7vIQ@GZW{OoqKdi%zWE)_G5tzV}17p)n(u;y-|z>`fx zh@J?=1?JiWfPK=&?}%`XH9JIqq{jHY6=Sjdp7m37vF_pNZIMKt zQxfuL?v>wF?4GshfFdSeQVt8;Xe*httGyZ(mE1DH-L`2s_s$7txYc1^t0OTRwyel1 z(=fAG@%UB!&ORzn(GLNv49A;%eWT4W_7)w@mcA8 zB9A-~#_ETz_3>3!y$GZdc-2%doQM^QC9N^|c6<;Q=j`HvJL4Z<>0|uQX^wQCy0A z?D&EBr|+^7B51;)-lo=^FZN%=YvtKB^eoJ?pTsqXZk!4_Q}U)8**}oK|M_z`+vtl2 z2@uxugB^!vtQ~jR?O`+YIkX>M4e8~nEV)Vf<-uwC%&vyQ-1_=L-R8TInyCrJ8*EGv z9zGs>GZALi);rT>HT2boR)xECU#ygd3teiR?yP_F=J{-Wd4GRdWq@>caiM5+pN^xW ztk9)!9Yw)W4r%G^ty!Y_I@@%5rM5o)B?(<$g5xbW9ZI z*;eoTq)XOa&mvV~Vz+M#dgs^1?q+?xEd-yVaV_Y9N;j6nPmthfj4#`j@cc7;)u+Jg z&ucQp!p5qHCIdfZ-h7LFm2&6Cnbarv9i=%D zgng%J3lCr4^^KkY_QgL)L_|j=%iWXXH}k%FG|ctTnqb@r7jv29k@y|DxjWA*U(<}> zTHoHrJ3E3&JltY^?6I^)K^w9@b_;jOgO|DiS>`wSqqjc~WWrjVw_{)}-$c-TV?uk+ zVU(A=8(<|dRR2s*jy3Q0(JK$aDm~UGx*GJK#Bv1m>RhJD;Etd8%b3?6Zbv)nMxa&oM z3J1;5S+BFt>0nw@TNIvMKJ#?rbD1iSk=3`~wY$$=Ke}Nov&G@G(XMj)_Lvs+=hHd} zJu6#E^li%7p|~%#FLFPt_4s|WpfztC1}BeU0_mlLy_NXS@xdL!?HGa?8f*e39Vs1k zz_ER3ALpr=RU|^RW{d+@uEGLIi?6idN!N%Ox4iP_XC<7%Rj5Ks%8-~|aeT{kbxk2m zr3CSav*KkF5vusC#5EK)7S(Q#KU$Qh63p-s^W8>LJ=gxR^StlH&5UZJ#WM2*E}9+Y zYOQ^`Ne2?VeksvBE35AQEZz7sgOalAK~D>)ZFOJoIjna7q!5U{&Dvf_? z1}>$Rm{1ZTe2KUQHc5VJ!>S>wecZfs=1D#g+LnK>7;Myu1*hZW@OLYUTUcsy-N7j&g-4!wt?J4cE8i{aH)TC$lyS&nxVBP9_@wN|UUA#j zj!*^UWp(FxE!RPc!#(n}3vKhZfgydB*F8dGl^K?~oX2lR8S*;7#%gps{!p_Dp zhd!1ulYY3l%Qu2oZ`aVV6ML$(d2MXPL&lHCAu4lP`T|OEXFHo4G{YrtmvgR((h+eQ1W0K71lK5)QBq;uwNXO^K7$fZUp{lu-XKvxQguE6%3oJK3jnZ4&1> z#9VG^wN@wSTxIxyJFkN|h9^D>w;skgWRI`*ap()TQn>K4TDHIsnwiml%DZ8bxln^n zuV?ci(~#%tMCgc*bI5RIeAIWWY05aPwdt}!xb=xzsckZ>`Ech-w@>lS1=^|g&x#Ly zX$-I!+zzdvQ=ViTzf!74gn|O99^bE^&pTn#uyr@(;fV7avnCA>xV=JFv1a-fCBqU! z$~g%>AJc;lO#SSE|4-?2|Mqs(Hxt)48$1YES$z+E1UL7#Q6C>{xxSvBK1al>SFdXL z%c&9D*tS&44C{6najbIWgW=H+s2Zi!JUke6yacG#4J|KUsx>n;&4%+qFu`b+JeWPF zs^~6SE9(7by}B7!duGHbC03RdVr4nkUB^C5geN`gvQIuMij@>)D!6McgxFT3qqYtG zOq(yZY#P@8rXufNOMj5^J1*;?uIAp4UN1%Cw}wD+|SSU=80>Oj`rKl48?WcvIaVun)}(V zUK@UvHb=rvQcBZz(^ z>c2Vt{o8v>->`>~2h&<~a8sV#tBcHW+I1=sVxuwTs zS2`Fb*-GHt*)V`(C=;vE9Z*cNZ5Z?Q}(Xif!XW!s_z*&SG9YK9TcJ&j>8BZt`f@e z%4eI1)`7~k9%9wH)N;GHw!8^^zyAX3Q0y6Dll>MwZLzmbN#x%)TjTMRg7Y{Q9eQb~ z${q`tGZb#ZbTe4Qzhr;%(;l5xV z#mdG?2&*BD=AAdZ3E&qY4;C!;*jC`ROlaDHawpar+8IgllqDw#3;PQ@V|?we71inzx7XA;cry-rySK^$!z_TU;6z- zhDRWLpEz)QwVo510~!s<@ME5196@XDH-KtNJIj_HE@Z?A4G^sJ@E)#}#LY`bJpVq8 zCY!R*=r;aqW{PX9!RfH}9RvH-BuGWU;cUQT5o^o11C8;~vJuML9P%9BA=zoBK3%;H zj8C>F0O-PN&pVCM+|)q~Y-`Zz&-g#`?{!R1O-x;+wM?~|<+SEgKCSR2rWm`34@JK# z^>OHO-iHf;Y!{j0X{w$^mGMY~TKPncYjWD}{;1KE)M2NQy>a*1XTG)~&+BOAhI<5* zv(3&<7#ZA_MAMNXVX;`g>AK;4)k`vxmKUXNa#|~xG}tTG%_hn`qWTr_YSYhRV4p#s zh+&Z|`1B7`_HPG@d0;!mU~HrV!PCLl9Z+$u@)Vzh&E+)8eghdb%APE-n)%w&luGMZz0;LT8LABx@|KmN^U6p!5DZ>rRyW7uC?0$M`z$N%XecJ|{{Jfy}<2ol=uo)i(&_))qhuo5Z`%L z&AK+^h9NoB_=D&8Ul07br0`!!9R0!e{`W2MZ+hbu+~P>86Lkg^pPW1ST0xmDb8>p} zOCtW4rbs^3iEUs|TzHNgcSf(;lOcAIJK0nNen=;1EsnY2o&c&)fQGm8$>UF*gkhQ| zaza-;3P0sann7aX<1{h53HD47EaOw(S3o9;Cq-Ia7@0y|cS`F{N2Z_5>fc!ut7qce zu5fth33<}}&BUXpyDCxc(xYNUap)5r?Ym(^$x+QaIQeFre6NMotoA^)!cASo753=# zoL5v2S;Et08M1OAKV!^H?%R8t&X^J?6GQP_%hK3u2aL;1={&P4Lk@qyt^a-?2lJ1L zypnUGzf1CL{y~!WS28PqSY|!fV!Ow}55gR~1atk|5ZC;%+U z1Testc`NxDF8cchKYz422Tyq}b-ZsDK1@G6hL{{ux#KaF`0eM7^eAN_5jD&1c(;<( zU}jb2^vkYg#5mq2Ky(ChHv+HAhn*9Z5>C&F2u%jV;04KP&~`59^kxAFQv<}qY(wBC zvb1u<&(Ogc{|iL zewaKU^lDlx$P3o$UUC^ElXY;1z2DcRsV zJc?IH+SYX0P?PDyuq4m!X4%iXln3`L*@;#*avV9QWx{-?j<*~zpeU%4Zi9v%;<3Dr zGPV6%M$#>~ygK{=or&AUz%LJGi_{#eq7cpE?7q~Fpk>e_uL%j3@RHVLn>`vi9m@*s z`spG!sbJR;F&0;neIO~<3l%ZT%l{S`G#}xk-(1l!%yjq$J zlG?ZYO!6~^vfQL~MWU`9GJV~=SJm`q6Q}>z8J#~^=symuyKD@6KZ_l%)73d%wT3|L zzl~LR3$vY7QEv0iG&YkPKSwmHAru^><7fw0UKs_AZpJS_F%uqsurPM&+5srlj|01B zaBFtGrtNF_aT&(nOJ@f)I{L2o@s(>0S0_wQ&e3qWawf&ZvMcM?aU>sy`>gn8B^}EO zYU{T{f)>MCMwpe?_i9b8jUqKzWF9M3)d%m-aU>|4kWc!wU_Xb)5!%I`{1eCi8~1Gn zH@n&jOd(KFJ*pCmaED$Evdz#(pKdHC^n~AxQM zQ`qwVgkb64!g!e7;wgEgvx*{QSdcI3+#H zs#_ULhzrk-a_@n1%|ROL02}X257p>{q(Qm+Y#$Ga;MXsG;_mo38VO^2M?sG$c&1A2 z=K-2$g#LH#^=}m4KLuPxpjG9MvQL}z>hFlkO2^DSWh2`r3%d8Y zifcik6u2GK4V=Y8hwhZU0cU&os7z$%$Fo$55vIi#KA&b0R3;Tki6lL^mrQtK$Pdhb z!6*7ZcQUzX z^(yC)QJ%p1aJH-Q_t=1hk2#N#r-SJ#+6}85Ul-hBUt<#i*?VN%P1{;g8_TW|8yGKX z`?NEoU0SMv^Y*(Pw?U<(OgjxmV_rIOlfrN3L}tWGVEpN}7Qxq@2jDcD4wDJQjAoz> ze}J(30R*7pZ#ehPx3^@P+y!c5o3J`%e%VvQo=clz>m@a~W?iwnNNR~4OC5pt z^r}zu72kaQ2gkA=1m?XVvne7>DoQIxZZe-28gLmJf2E#{fe#-0nq83AS1!UM9@7KW zX)2guc^r24=n{E~QT=Q2{>`&=m;0+Z#CD;laWz82p}+mr|zCRid2G0n|KtbY{tsDXP62|T_41!`RkD4goN!ALQ2+&R&{#_<8c z6AfBAKD+q50VmIRSWKYWEEk*4d-=CQr>x4|n*)SiQczIW5@F5Wuztu_oP3{N+~&g) zu9UvCqY0GeDT#T#2b=I`B&!Fx&2Ht9g7W0>)mHe8!H_r(+@M-e;26PhB~L314M~yt zXD09W#NR=t5iv%1-gzm6&@&2&$iOAG!w-d+y*qfC28pM7y2EJ$O7Wic`)XbU4 zk(2>earDFgI#&AoQvW$L{TGoQ>#aU2MUz%UwEc)Xk0E93q6Yy3zr=NuzH&c|rcq6P!p#P7UV_e0w<=mSzz^`z zTBjsMiKuUi@kI>cl4)Hsu!#|{M=9o4O6Ku56J1sP@hL^$i@hh`EwY5fn4^Ixk(!Os(aPILwb#0s;}Q<+ipzsA1c zRfOtOB40O-pVnf&F#4t%CT+!gawKMkm+K(<93B!Gy~`B2dyp=)ePRCQNC=t#izN76v+Ln2pBOgg;5QGqnLh^{Or{G3Cwf}tNxO4 ztVDaRuEaCr2R(K#grC+VW;u5I(kQ;df8Fes``-Fh%C3>d9?Cf()ZRD= z{ubwxLMK=@1!ICgTSK}Z`*zc>SxsZS!5D_k>F4ohocSfNt{^o^)$7LL8Yeoh&N7=s zal9M-|Ac8}91syu9IK^;o6dYE{4{_Bo z*?$lKN-*X>rWlSc0>!UcSUAO09=jaUGCD}|RlCwN$TKF-R!4GN90bIPB+(B8vJW0G z^>MukeDrF!tMk~sGtGI<3sE{BpPCb7JvH_z@{aaeog9it1Et# zT!cda>WUxfrtX5%BmUrv|K-4+LRo($$psq-oEdV@gCn8_*t_(rGa*x+SJc3l?67aN zYa@h7+ztZPU%|tOwMY`y47%fYBZ0xdsADG6Hxus z(t7`?vPfjh9VC)YeD~dGBXS2RymPAy(me;x2^ojjK0wBoIx{c0mA{Y6WGw@HY?jOIgPKu?OE<41i z?)Z*JXY14zBokLNvB>w;Ih8AtiNBF5i{*t>Xv#tFXXHAPc4DZ!M+Wx19Z;*d-kVSK zhrYL1zUb>s|Juo5%nLDRu%abncF}7v2)Rji3`g;TQ0^RKfWa(NlOMiuIwwr20#xRmu?sn9D5Bq@%KnT1-bxti5J@b^tX(N+`M z>rpe~V~)nN#jLW%AKDFsNb=SqzdK5ikt<1Zv@$z&j7G{lz{B;ZT)+-SxYE)+(Vja) z61tsLq7ka;0 zuL}GZUhXl8dnrnEz!$?}lJ0+EM@{VQ2o*S{-Mh*VwhHPN*S|Tp>=*w>2>w4X=Rf&5 z<0MO~jJV89?esi_}Jshzzi(!;;#xIH9;j$~>mz@*^g1=`15(c8T zS7e#ziTGrH(p!GNz&uEn@4T`GCfh`Qd-XBjZBp+!Bwy$(q{uUAie8JQb~!ZCwdiS) zOH06T;w2qjS>k57vnAg-hP4ChX*&vvZc!^s)X69QXjyIEbBKm#>0?~Z z7MydL2psgU@dL?zeug|%hpJ*r&^P*j4p{$w$v@@0{~|8w@Ae$H^A#;NG9=m}Ohz_# z61+$Hcx@)S(OK%Hw!}ME2L)qhmL++m-{#X}f5<4Mx0+Vcv?pA&n@*La2NW zcX7fH>(~3kbwiKT6S9e7nf+;KhCCSUyISbu zt{#?m?~N65*|Xlz6j7Oe)gD|~7p!b~VrCS@=JEK3F01PrQnv|)p^X*mnPQs58am#2 zKqOD0V*aV~VMCRW9h$3^iR^UZOthEAw3hcZ-VO~8jpS4-s;Rx*YrS*vW!b<^?lVVfa4xyjXpIf}xNpGJZO1xlk$u}`^|41np_rfoCqrsYxN6ore&$G7 z2h}u-*}7hQOSfndEt1RrK*v6EH%cgjsZZNXpY&?BW9OW;m*35AC{qf(Zq-+-ps~pP zXPcc~FlsMomsi=*fIF|j|*9S`cRwAN1uMAsYfVl+&^=9b!g~W-U;@l zVUn?JYF`<7NO^U$|5HS)DKyf5qA%&W`iGHw7X~W^k~%N&q?aB>13hj!!QA0<{aCR< z+=4N+efP|0wQ2~7-$K4!x&AW#*LRB%26i*X1GDVQPS+XsqvOtwkK>c#cxw)tVw|C; z^sTfy9P4-IrSs2;vN_2knKCG8JEokK$xs&#?EzBse%}7NQ+9Dy=PP=REBblZXzXyW5mn@87>Fk^c0k-peZzhTqMU{2_ zFri?MS|U)~_Ees{l^Us7r>!^9(GYrj<*o8I6{siEcQUP4`TgRZ%!uNeem(bzi>jP8 z);Mn>ZnDJ1JhL=mU5HGVUDmmoV4b}oIkSuzkFjScXt=#gl8>p< zFV7{Cm#H~x1;e(cOc5TkJLC;Mm>s)`us+cq!KiAJny(&iL#I|>DGCB)?#P)&B}^!| z+-aIc3tug6t5#;AR>6W}M)9Y_VjWHQ{vYnNzoFFstBlz{?YDpOgt>#uPSjliQbNs8 z#%JtrMg`S|6>iYWz4`va!PQ>+1*5f{g9E4OUQx+Ppy8sF>9d8{2TO$<_4l)G`AMa4 zWS|&&^THnHZR=w$Ke;B8ftc}sGI&S(OBmk`$jv(*jE2U~um`E{C4(895mCH9Z#H}+ zp1d5hvFGh^<;rY2IM?CeDEf@kEe($^J|{XS%7|M$C)$btltU%t;(-3w0r+dm41Q+2 z1=jZl=&Go5q9!1#AaCkC)!UJ_I8{`mCi527<|G(9{rVpJy&dzo7KJc}4PZN|ZTn-~ z5$^1Md$T*-Y^`mP$N;N7ieGuhX$Fa&rqN505!138c~fA;|2IqdkI3XtdC|X!5c~J5 z`sZ-#KN-S*{*yl$zWh~0%^wON{?dUz`LcgyDE*J|(OzK+i(CUd^NAN}S^Lt&FOTt{uFE_YM zyEW~1_?CLj*rA2GT($x|hPBh=iS<=FX=vpk6{Gk00%?d2E3`9H?z3fSh3=7DxSH7c zY$-J`;@Hs-XdW}Cf)6<-8k(5`pGA9L0~bqI!%h_4$Z_%0;PKQ*!sZNU9YKs=QvNOa z|KkrZr=#9_fZ~0^2*}d-=ABt>A?s@fPK95c6FnRGT~+XdF%E`_0Y5q?I&?cW1PTx4 z&Ir{EK;m#ga1yj*2v9M)UYMQYN}*YJ9`l@yR1>z&;J6wjhIQnGQF$Tn)4b&_p7BO1 zW5B|(hnI^bD))y=<*$%myDtQHT{cEuJ9&M9{vC8H#92; z&xyK5R!Tcdzv#yvV2n)b0~ta#iz{boE1ie!>6en>4qd#yOwdA%(Ed9EWbX8H5?6io z+HDvAZw$>Wsq!2GMzh|Kdv9Bm86e5Xdf0EqY^>-wZQliJGs<|Wru2Xc;mMs5oh}?c zwWUAxEvwCe4q@@0o2T08S*XFJZeN|yPaNEe0q7SgUtfq zoc?`!(<#EY{FKF=$9z^F^=FN6Ul*N~T%J8u|HpjZol7yS%J&9AXA0rhcDJ ziI`F4dtQ)g_81kF;H{C7cELA8ZwOloEt1Nb?guym1$sXk>*`}xgyo<0Z5h;qUI|0! zsaa~F!mB!uA1ia#aZPEd$V!XRn{Q0g%7y0!CKhx>{*Yja$RUTRK{!8!5sAHp!Cd}X zW&=JS``f||Rh;DQTC7;~;hT4V;3Fq~ zQ}lBur8K6X)q+=TKtfD|R9?l@a`~0ui;9un^cPC&-Ef5M{ortV`m;~?Tu?&1l?xYZ z7wXgAXZIb0;L>T@&wfJ=n_?|x?{f;L_4H0k_h$RV??T;GcdU%FsO2)tgPC3|aE^H~tpllQYOZ?cm6@;!=>P zkPpTM{2tbk`X9T@_NaIPTQK=fZOuF^D>b~?EjIS)vO1lMiQiLTUzVnx6Y-1eH`Ivp z;~f$D1rrIVv8X8x?=0HX$w?8G*^^K=My{w139@a3_Ki(gRmQ+DzaojUa)|`>_b$5N z)Gf&|#p?raEJQ!|*!`HZuv^QwXkT+9Xg&PgXTt7ZtR%|ytuW~`lf+v{Z=o2AnP|uS zol9(M$a<(I597W!!PLptzHmEw&)r#VI~4`l>Lx4d*F=+*!E<*@QC_;bb4}?P86`+c ztcWhEPf>?ff>iyfER7vsku5d3tG!y#{iYsy)J$ z^0N=;rQfR)ai|m*Kla<-NL`-Rn7LP356mu}!o!X;R!Y1CdOcd8>ZnjrQ1fx+!?64b zSuM)oU1d(&@M|V$gzYU!X|m9{OIR!XA|M&Ow}#CKI5{V(wf5sxvn%duSVbj5@3zAi z@7bZ~=CtfB_CMc_M5rfc$`tTx>n1QsX7G(xT)NmC|Crq^4$IgmryZ`?9^13 zDWK$)+84b=#EPs(YMIo47KCAa=R_`c^;}(y9h`2YPLwcD`7g6)T_exLIv;wpeP^*} zDD=ga3#xr~xbi+P_3@&5=x6lk28>F~3qt-q?LKa4F~W4YJ_u0s+s|cfzf-?8t_T&j zf1xI3FO(~rnbT)f;WDi8-ktkmiM5p~U7$!VB-rBH7>l{oCB(a1lCR*}%^)9VG-IrO z+Lw*e2zs-6QIiVH#U8<{lhdOg`Zxsy&?K&jKlM%1b`+%y}k zw^K%vW;L{XO|R{1jH--iNXU~oa29|XS+x_xZ0dlxFKNl!COOhD$_1uga!ji&Kd zHUVNWoq;xkae{LKnc5)miUJx3K=l!wsx;?=_1Q0&!&%llh$wuTCOVH(-*%z@9W$Gyie zD$nyW_IO1byO?qJZj1Qv=u5;EO+V`$pN%enEO(h;!pwI)`RZgWHF^`4BU{thJFYV! zgf4q^5jQ~UPx|nU1T%cc6mAXdW3}Fku||7Dz0v3_?D~Kc4SFZFcM2(Y1|nNo(C)avVL7<8g3Rd&HLUd_Bjf;e>U+ zbw2#C!&W&~Ot{vols7H6uz33D$W>dvrCQjI>e5J#oz(c8w-)P<>ALZBTqmjeEsbhL z##F)l@W-xdJ4Hp*>I_hTgZ$D4<{pTJk>M^a18oKIsp~)B8Y9O%dag=LMf_Nw*ojM95j7FHx*~pEgdf{DaxC3Nz+#mp z=0?+QSHhSurlL2}95Gi#`A2IJrTWdzRS3b*nI@}nb%A8_`=*)A?9a2xqueMO zOXfB@yWd{uNYO;LrmX}WB(GIHS4eB$@t^g^SkG@I(fsmWc04Teiawh(C7i*Mf>#EJ z3Jm==&wi(t49~~^hD^pg40q$jj}3ANRVIOn5I@c?6?P5z+RBuj1{h?I8$SJvxR);Gb9$s+bjjwZ#x%n7v z_(h}6wuB)k(zvkg@QhLR2yP~1Kns7^Y2P<7gHp+T3h|+G)6gJt$nMCl#Ghh+;;)<} z>#N~6M0gh6jDvj|jJUArz1WUOYUjC zid~I-qLvTCl%zpZdRq13vUc^GZ8kD776ooB^w;9r6-`IWX9YU*d<9vZ>lpMcm++$m z?v>-UgLo{@-o&MgZVO}Rgv#4vOoGHb=whIX39rilW?$bQMjqU{W=t@)n;

?knrj3&=*`dP=l} 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/PEGI/Twelve.jpg b/gaseous-server/Assets/Ratings/PEGI/Twelve.jpg deleted file mode 100644 index 40dc135de9e33268f49d0eb6ac5cf9267411dee1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67077 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 diff --git a/gaseous-server/Assets/Ratings/PEGI/Twelve.svg b/gaseous-server/Assets/Ratings/PEGI/Twelve.svg new file mode 100644 index 0000000..24aa8d7 --- /dev/null +++ b/gaseous-server/Assets/Ratings/PEGI/Twelve.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/Assets/Ratings/USK/USK_0.svg b/gaseous-server/Assets/Ratings/USK/USK_0.svg new file mode 100644 index 0000000..90f38a6 --- /dev/null +++ b/gaseous-server/Assets/Ratings/USK/USK_0.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/Assets/Ratings/USK/USK_12.svg b/gaseous-server/Assets/Ratings/USK/USK_12.svg new file mode 100644 index 0000000..89b7ce3 --- /dev/null +++ b/gaseous-server/Assets/Ratings/USK/USK_12.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/Assets/Ratings/USK/USK_16.svg b/gaseous-server/Assets/Ratings/USK/USK_16.svg new file mode 100644 index 0000000..9f1b4d7 --- /dev/null +++ b/gaseous-server/Assets/Ratings/USK/USK_16.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/Assets/Ratings/USK/USK_18.svg b/gaseous-server/Assets/Ratings/USK/USK_18.svg new file mode 100644 index 0000000..9b2bd7a --- /dev/null +++ b/gaseous-server/Assets/Ratings/USK/USK_18.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/Assets/Ratings/USK/USK_6.svg b/gaseous-server/Assets/Ratings/USK/USK_6.svg new file mode 100644 index 0000000..cc8fee8 --- /dev/null +++ b/gaseous-server/Assets/Ratings/USK/USK_6.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gaseous-server/Controllers/GamesController.cs b/gaseous-server/Controllers/GamesController.cs index 5e1834b..27e4e54 100644 --- a/gaseous-server/Controllers/GamesController.cs +++ b/gaseous-server/Controllers/GamesController.cs @@ -198,12 +198,28 @@ namespace gaseous_server.Controllers fileType = "image/svg+xml"; break; case AgeRatingCategory.PEGI: - fileExtension = "jpg"; - fileType = "image/jpg"; + fileExtension = "svg"; + fileType = "image/svg+xml"; break; case AgeRatingCategory.ACB: - fileExtension = "png"; - fileType = "image/png"; + fileExtension = "svg"; + fileType = "image/svg+xml"; + break; + case AgeRatingCategory.CERO: + fileExtension = "svg"; + fileType = "image/svg+xml"; + break; + case AgeRatingCategory.USK: + fileExtension = "svg"; + fileType = "image/svg+xml"; + break; + case AgeRatingCategory.GRAC: + fileExtension = "svg"; + fileType = "image/svg+xml"; + break; + case AgeRatingCategory.CLASS_IND: + fileExtension = "svg"; + fileType = "image/svg+xml"; break; } diff --git a/gaseous-server/gaseous-server.csproj b/gaseous-server/gaseous-server.csproj index 9614837..aa3be2e 100644 --- a/gaseous-server/gaseous-server.csproj +++ b/gaseous-server/gaseous-server.csproj @@ -40,18 +40,47 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -69,6 +98,11 @@ + + + + + @@ -88,6 +122,7 @@ + @@ -101,18 +136,38 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gaseous-server/wwwroot/.DS_Store b/gaseous-server/wwwroot/.DS_Store index cd3a2cf86a516c8064df523e3cb54a7528c4c45a..022e6adfa8abea1fa9290e20313444f157376fd2 100644 GIT binary patch delta 296 zcmZoMXfc=|#>B`mu~2NHo}wrd0|Nsi1A_nqLn=dYQh9MfQcix-#Er`*8;G#fvoT~c z!A4ToZP(pE}-LpfDz~;FyMvK rFsgfFk2>3Cb`E|HUw&zlbgmNEW1{0f;yIi0ojV*uVk+H}y=l delta 88 zcmZoMXfc=|#>CJzu~2NHo}wrt0|NsP3otOGG8C5u7v<&T=cP|9RA*$|{D4uNZL

-
-
+
- diff --git a/gaseous-server/wwwroot/pages/game.html b/gaseous-server/wwwroot/pages/game.html new file mode 100644 index 0000000..38fd0ef --- /dev/null +++ b/gaseous-server/wwwroot/pages/game.html @@ -0,0 +1,36 @@ +
+ +
+ + \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/home.html b/gaseous-server/wwwroot/pages/home.html new file mode 100644 index 0000000..97cf028 --- /dev/null +++ b/gaseous-server/wwwroot/pages/home.html @@ -0,0 +1,14 @@ +
+
+ + \ No newline at end of file diff --git a/gaseous-server/wwwroot/scripts/gamesformating.js b/gaseous-server/wwwroot/scripts/gamesformating.js index e72a0af..1ff0fc2 100644 --- a/gaseous-server/wwwroot/scripts/gamesformating.js +++ b/gaseous-server/wwwroot/scripts/gamesformating.js @@ -9,6 +9,7 @@ function renderGameIcon(gameObject, showTitle, showRatings) { var gameBox = document.createElement('div'); gameBox.className = 'game_tile'; + gameBox.setAttribute('onclick', 'window.location.href = "/index.html?page=game&id=' + gameObject.id + '";'); var gameImage = document.createElement('img'); gameImage.className = 'game_tile_image'; diff --git a/gaseous-server/wwwroot/styles/style.css b/gaseous-server/wwwroot/styles/style.css index eb14529..519a8ba 100644 --- a/gaseous-server/wwwroot/styles/style.css +++ b/gaseous-server/wwwroot/styles/style.css @@ -132,9 +132,22 @@ input[id='filter_panel_search'] { background-color: white; } +.rating_image { + display: inline-block; + max-width: 72px; + max-height: 72px; + margin-right: 10px; +} + .rating_image_mini { display: inline-block; max-width: 32px; max-height: 32px; margin-right: 2px; +} + +.game_cover_image { + display: block; + max-width: 250px; + max-height: 350px; } \ No newline at end of file From c9dc1262b94b1777df8d0eb8663dcf0dd600bf1a Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Mon, 26 Jun 2023 01:03:56 +1000 Subject: [PATCH 33/71] feat: further updates to the game detail page --- gaseous-server/wwwroot/pages/game.html | 87 ++++++++++++++++-- gaseous-server/wwwroot/pages/home.html | 6 +- .../wwwroot/scripts/gamesformating.js | 5 +- gaseous-server/wwwroot/styles/style.css | 89 +++++++++++++++++-- 4 files changed, 171 insertions(+), 16 deletions(-) diff --git a/gaseous-server/wwwroot/pages/game.html b/gaseous-server/wwwroot/pages/game.html index 38fd0ef..82ed2b3 100644 --- a/gaseous-server/wwwroot/pages/game.html +++ b/gaseous-server/wwwroot/pages/game.html @@ -1,16 +1,59 @@ -
+
+
+ +
+
+
+

Age Ratings

+
+
-
+
+

+
+
+ +
+
+
+ +
\ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/home.html b/gaseous-server/wwwroot/pages/home.html index 97cf028..439e388 100644 --- a/gaseous-server/wwwroot/pages/home.html +++ b/gaseous-server/wwwroot/pages/home.html @@ -1,5 +1,7 @@ -
-
+
+
+
+
\ No newline at end of file diff --git a/gaseous-server/wwwroot/styles/style.css b/gaseous-server/wwwroot/styles/style.css index 4715c9a..33cf6cd 100644 --- a/gaseous-server/wwwroot/styles/style.css +++ b/gaseous-server/wwwroot/styles/style.css @@ -96,6 +96,36 @@ input[id='filter_panel_search'] { width: 160px; } +/* width */ +::-webkit-scrollbar { + width: 10px; + height: 5px; +} + +/* Track */ +::-webkit-scrollbar-track { + background: #383838; +} + +/* Handle */ +::-webkit-scrollbar-thumb { + background: #888; +} + +/* Handle on hover */ +::-webkit-scrollbar-thumb:hover { + background: #555; +} + +.text_link { + +} + +.text_link:hover { + cursor: pointer; + text-decoration: underline; +} + #games_home { display: flex; margin-top: 20px; @@ -168,13 +198,13 @@ input[id='filter_panel_search'] { } #mainbody { - margin-right: 300px; + margin-left: 270px; } #gamesummary { - float: right; - margin-left: 30px; - width: 280px; + float: left; + margin-right: 20px; + width: 250px; } .game_cover_image { @@ -186,8 +216,8 @@ input[id='filter_panel_search'] { .rating_image { display: inline-block; - max-width: 72px; - max-height: 72px; + max-width: 64px; + max-height: 64px; margin-right: 20px; margin-bottom: 20px; } @@ -202,7 +232,7 @@ input[id='filter_panel_search'] { #gamescreenshots { background-color: black; padding: 10px; - height: 350px; + /*height: 350px;*/ margin-bottom: 20px; } @@ -215,9 +245,17 @@ input[id='filter_panel_search'] { #gamescreenshots_gallery { left: 0px; right: 0px; - height: 50px; + height: 65px; overflow-x: scroll; - text-align: center; + overflow-y: hidden; + white-space: normal; +} + +#gamescreenshots_gallery_panel { + display: block; + height: 50px; + margin-bottom: 10px; + white-space: nowrap; } .gamescreenshosts_gallery_item { @@ -225,4 +263,57 @@ input[id='filter_panel_search'] { height: 50px; width: 70px; margin-right: 10px; + white-space: nowrap; + border-color: black; + border-style: solid; + border-width: 2px; +} + +.gamescreenshots_arrows { + margin-top: 140px; + height: 50px; + width: 30px; + font-size: 40px; + background-color: #383838; + color: black; + text-align: center; + + user-select: none; /* standard syntax */ + -webkit-user-select: none; /* webkit (safari, chrome) browsers */ + -moz-user-select: none; /* mozilla browsers */ + -khtml-user-select: none; /* webkit (konqueror) browsers */ + -ms-user-select: none; /* IE10+ */ +} + +.gamescreenshots_arrows:hover { + cursor: pointer; + background-color: #555; +} + +#gamescreenshots_left_arrow { + float: left; +} + +#gamescreenshots_right_arrow { + float: right; +} + +.gamescreenshosts_gallery_item:hover { + border-color: lightblue; +} + +.gamescreenshosts_gallery_item_selected { + border-color: white; +} + +#gamesummarytext_label { + +} + +.line-clamp-4 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + /* truncate to 4 lines */ + -webkit-line-clamp: 4; } \ No newline at end of file From 9142ff4899671c33baff0976c17651b15478a3c3 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Mon, 26 Jun 2023 12:13:17 +1000 Subject: [PATCH 35/71] cleanup: removed redundant styling --- gaseous-server/wwwroot/pages/game.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gaseous-server/wwwroot/pages/game.html b/gaseous-server/wwwroot/pages/game.html index b36b000..aa36cb4 100644 --- a/gaseous-server/wwwroot/pages/game.html +++ b/gaseous-server/wwwroot/pages/game.html @@ -14,8 +14,8 @@
-
<
-
>
+
<
+
>
-

ROM's

+

ROM's/Images

@@ -238,7 +238,7 @@ formatBytes(result[i].size, 2), result[i].romTypeMedia, result[i].mediaLabel, - '...' + '...' ]; newTable.appendChild(createTableRow(false, newRow, 'romrow', 'romcell')); } diff --git a/gaseous-server/wwwroot/scripts/main.js b/gaseous-server/wwwroot/scripts/main.js index de5e108..07b9ac7 100644 --- a/gaseous-server/wwwroot/scripts/main.js +++ b/gaseous-server/wwwroot/scripts/main.js @@ -33,4 +33,41 @@ function formatBytes(bytes, decimals = 2) { const i = Math.floor(Math.log(bytes) / Math.log(k)) return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}` +} + +function showDialog(dialogPage, variables) { + // Get the modal + var modal = document.getElementById("myModal"); + + // Get the modal content + var modalContent = document.getElementById("modal-content"); + + // Get the button that opens the modal + var btn = document.getElementById("myBtn"); + + // Get the element that closes the modal + var span = document.getElementsByClassName("close")[0]; + + // When the user clicks on the button, open the modal + modal.style.display = "block"; + + // When the user clicks on (x), close the modal + span.onclick = function () { + modal.style.display = "none"; + modalContent.innerHTML = ""; + modalVariables = null; + } + + // When the user clicks anywhere outside of the modal, close it + window.onclick = function (event) { + if (event.target == modal) { + modal.style.display = "none"; + modalContent.innerHTML = ""; + modalVariables = null; + } + } + + modalVariables = variables; + + $('#modal-content').load('/pages/dialogs/' + dialogPage + '.html'); } \ No newline at end of file diff --git a/gaseous-server/wwwroot/styles/style.css b/gaseous-server/wwwroot/styles/style.css index 1305b25..80e91c5 100644 --- a/gaseous-server/wwwroot/styles/style.css +++ b/gaseous-server/wwwroot/styles/style.css @@ -22,6 +22,53 @@ h3 { border-image: linear-gradient(to right, rgba(255,0,0,1) 0%, rgba(251,255,0,1) 16%, rgba(0,255,250,1) 30%, rgba(0,16,255,1) 46%, rgba(250,0,255,1) 62%, rgba(255,0,0,1) 78%, rgba(255,237,0,1) 90%, rgba(20,255,0,1) 100%) 5; } +/* The Modal (background) */ +.modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 1; /* Sit on top */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0,0,0); /* Fallback color */ + background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ +} + +/* Modal Content/Box */ +.modal-content { + background-color: #383838; + margin: 15% auto; /* 15% from the top and centered */ + padding: 10px; + border: 1px solid #888; + width: 600px; /* Could be more or less, depending on screen size */ +} +#modal-heading { + margin-block: 5px; + border-bottom-style: solid; + /*border-bottom-color: #916b01;*/ + border-bottom-width: 3px; + /*border-image: linear-gradient(to right, blue 25%, yellow 25%, yellow 50%,red 50%, red 75%, teal 75%) 5;*/ + + border-image: linear-gradient(to right, rgba(255,0,0,1) 0%, rgba(251,255,0,1) 16%, rgba(0,255,250,1) 30%, rgba(0,16,255,1) 46%, rgba(250,0,255,1) 62%, rgba(255,0,0,1) 78%, rgba(255,237,0,1) 90%, rgba(20,255,0,1) 100%) 5; +} + +/* The Close Button */ +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; +} + +.close:hover, +.close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + #banner_icon { background-color: white; position: fixed; @@ -202,6 +249,7 @@ input[id='filter_panel_search'] { width: 90%; min-width: 911px; max-width: 1122px; + min-height: 550px; padding-top: 1px; padding-left: 20px; @@ -351,6 +399,7 @@ iframe { .romrow:hover { background-color: #383838; + background: rgba(56, 56, 56, 0.3); } .romcell { From 495c39f2ddfe51feabf97c6e830b2823bbb38983 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Thu, 29 Jun 2023 09:18:07 +1000 Subject: [PATCH 41/71] refactor: search engine enhancements to make finding titles more reliable --- gaseous-server/Classes/ImportGames.cs | 107 ++++++++++++++---- gaseous-server/Controllers/GamesController.cs | 37 ++++++ 2 files changed, 123 insertions(+), 21 deletions(-) diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs index 69f3fa3..febd54a 100644 --- a/gaseous-server/Classes/ImportGames.cs +++ b/gaseous-server/Classes/ImportGames.cs @@ -86,7 +86,7 @@ namespace gaseous_server.Classes determinedPlatform = new IGDB.Models.Platform(); } - IGDB.Models.Game determinedGame = SearchForGame(discoveredSignature); + IGDB.Models.Game determinedGame = SearchForGame(discoveredSignature.Game.Name, discoveredSignature.Flags.IGDBPlatformId); // add to database StoreROM(hash, determinedGame, determinedPlatform, discoveredSignature, GameFileImportPath); @@ -186,35 +186,41 @@ namespace gaseous_server.Classes return discoveredSignature; } - public static IGDB.Models.Game SearchForGame(Models.Signatures_Games discoveredSignature) + public static IGDB.Models.Game SearchForGame(string GameName, long PlatformId) { // search discovered game - case insensitive exact match first IGDB.Models.Game determinedGame = new IGDB.Models.Game(); // remove version numbers from name - discoveredSignature.Game.Name = Regex.Replace(discoveredSignature.Game.Name, @"v(\d+\.)?(\d+\.)?(\*|\d+)$", "").Trim(); - discoveredSignature.Game.Name = Regex.Replace(discoveredSignature.Game.Name, @"Rev (\d+\.)?(\d+\.)?(\*|\d+)$", "").Trim(); + GameName = Regex.Replace(GameName, @"v(\d+\.)?(\d+\.)?(\*|\d+)$", "").Trim(); + GameName = Regex.Replace(GameName, @"Rev (\d+\.)?(\d+\.)?(\*|\d+)$", "").Trim(); - Logging.Log(Logging.LogType.Information, "Import Game", " Searching for title: " + discoveredSignature.Game.Name); + List SearchCandidates = GetSearchCandidates(GameName); - foreach (Metadata.Games.SearchType searchType in Enum.GetValues(typeof(Metadata.Games.SearchType))) + foreach (string SearchCandidate in SearchCandidates) { - Logging.Log(Logging.LogType.Information, "Import Game", " Search type: " + searchType.ToString()); - IGDB.Models.Game[] games = Metadata.Games.SearchForGame(discoveredSignature.Game.Name, discoveredSignature.Flags.IGDBPlatformId, searchType); - if (games.Length == 1) + + Logging.Log(Logging.LogType.Information, "Import Game", " Searching for title: " + SearchCandidate); + + foreach (Metadata.Games.SearchType searchType in Enum.GetValues(typeof(Metadata.Games.SearchType))) { - // exact match! - determinedGame = Metadata.Games.GetGame((long)games[0].Id, false, false); - Logging.Log(Logging.LogType.Information, "Import Game", " IGDB game: " + determinedGame.Name); - break; - } - else if (games.Length > 0) - { - Logging.Log(Logging.LogType.Information, "Import Game", " " + games.Length + " search results found"); - } - else - { - Logging.Log(Logging.LogType.Information, "Import Game", " No search results found"); + Logging.Log(Logging.LogType.Information, "Import Game", " Search type: " + searchType.ToString()); + IGDB.Models.Game[] games = Metadata.Games.SearchForGame(SearchCandidate, PlatformId, searchType); + if (games.Length == 1) + { + // exact match! + determinedGame = Metadata.Games.GetGame((long)games[0].Id, false, false); + Logging.Log(Logging.LogType.Information, "Import Game", " IGDB game: " + determinedGame.Name); + break; + } + else if (games.Length > 0) + { + Logging.Log(Logging.LogType.Information, "Import Game", " " + games.Length + " search results found"); + } + else + { + Logging.Log(Logging.LogType.Information, "Import Game", " No search results found"); + } } } if (determinedGame == null) @@ -231,6 +237,65 @@ namespace gaseous_server.Classes return determinedGame; } + public static List SearchForGame_GetAll(string GameName, long PlatformId) + { + List searchResults = new List(); + + // remove version numbers from name + GameName = Regex.Replace(GameName, @"v(\d+\.)?(\d+\.)?(\*|\d+)$", "").Trim(); + GameName = Regex.Replace(GameName, @"Rev (\d+\.)?(\d+\.)?(\*|\d+)$", "").Trim(); + + List SearchCandidates = GetSearchCandidates(GameName); + + foreach (string SearchCandidate in SearchCandidates) + { + foreach (Metadata.Games.SearchType searchType in Enum.GetValues(typeof(Metadata.Games.SearchType))) + { + if ((PlatformId == 0 && searchType == SearchType.searchNoPlatform) || (PlatformId != 0 && searchType != SearchType.searchNoPlatform)) + { + IGDB.Models.Game[] games = Metadata.Games.SearchForGame(SearchCandidate, PlatformId, searchType); + foreach (IGDB.Models.Game foundGame in games) + { + bool gameInResults = false; + foreach (IGDB.Models.Game searchResult in searchResults) + { + if (searchResult.Id == foundGame.Id) + { + gameInResults = true; + } + } + + if (gameInResults == false) + { + searchResults.Add(foundGame); + } + } + } + } + } + + return searchResults; + + } + + private static List GetSearchCandidates(string GameName) + { + List SearchCandidates = new List(); + SearchCandidates.Add(GameName); + if (GameName.Contains(":")) + { + GameName = GameName.Substring(0, GameName.IndexOf(":")); + SearchCandidates.Add(GameName.Trim()); + } + if (GameName.Contains("-")) + { + GameName = GameName.Substring(0, GameName.IndexOf("-")); + SearchCandidates.Add(GameName.Trim()); + } + + return SearchCandidates; + } + public static long StoreROM(Common.hashObject hash, IGDB.Models.Game determinedGame, IGDB.Models.Platform determinedPlatform, Models.Signatures_Games discoveredSignature, string GameFileImportPath, long UpdateId = 0) { Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); diff --git a/gaseous-server/Controllers/GamesController.cs b/gaseous-server/Controllers/GamesController.cs index 47cfbad..93e4d51 100644 --- a/gaseous-server/Controllers/GamesController.cs +++ b/gaseous-server/Controllers/GamesController.cs @@ -629,6 +629,43 @@ namespace gaseous_server.Controllers } } + [HttpGet] + [Route("search")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameSearch(long RomId = 0, string SearchString = "") + { + try + { + if (RomId > 0) + { + Classes.Roms.GameRomItem romItem = Classes.Roms.GetRom(RomId); + Common.hashObject hash = new Common.hashObject(romItem.Path); + Models.Signatures_Games romSig = Classes.ImportGame.GetFileSignature(hash, new FileInfo(romItem.Path), romItem.Path); + List searchResults = Classes.ImportGame.SearchForGame_GetAll(romSig.Game.Name, romSig.Flags.IGDBPlatformId); + + return Ok(searchResults); + } + else + { + if (SearchString.Length > 0) + { + List searchResults = Classes.ImportGame.SearchForGame_GetAll(SearchString, 0); + + return Ok(searchResults); + } + else + { + return NotFound(); + } + } + } + catch + { + return NotFound(); + } + } + [HttpGet] [Route("{GameId}/screenshots")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] From e86aa80df6956edcc4c390c84919cf5db644201e Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Thu, 29 Jun 2023 09:29:23 +1000 Subject: [PATCH 42/71] fix: added early breakout from game search if the title is found --- gaseous-server/Classes/ImportGames.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs index febd54a..fdde254 100644 --- a/gaseous-server/Classes/ImportGames.cs +++ b/gaseous-server/Classes/ImportGames.cs @@ -199,6 +199,7 @@ namespace gaseous_server.Classes foreach (string SearchCandidate in SearchCandidates) { + bool GameFound = false; Logging.Log(Logging.LogType.Information, "Import Game", " Searching for title: " + SearchCandidate); @@ -211,6 +212,7 @@ namespace gaseous_server.Classes // exact match! determinedGame = Metadata.Games.GetGame((long)games[0].Id, false, false); Logging.Log(Logging.LogType.Information, "Import Game", " IGDB game: " + determinedGame.Name); + GameFound = true; break; } else if (games.Length > 0) @@ -222,6 +224,7 @@ namespace gaseous_server.Classes Logging.Log(Logging.LogType.Information, "Import Game", " No search results found"); } } + if (GameFound == true) { break; } } if (determinedGame == null) { From fba9b7a6c988a0e23aafd2e074e55ee5ef05aff2 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Thu, 29 Jun 2023 14:46:41 +1000 Subject: [PATCH 43/71] refactor: moved import file name version trimming --- gaseous-server/Classes/ImportGames.cs | 27 +++++++++++-------------- gaseous-server/Support/PlatformMap.json | 17 ++++++++++++++++ 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs index fdde254..3ba9ef6 100644 --- a/gaseous-server/Classes/ImportGames.cs +++ b/gaseous-server/Classes/ImportGames.cs @@ -191,12 +191,8 @@ namespace gaseous_server.Classes // search discovered game - case insensitive exact match first IGDB.Models.Game determinedGame = new IGDB.Models.Game(); - // remove version numbers from name - GameName = Regex.Replace(GameName, @"v(\d+\.)?(\d+\.)?(\*|\d+)$", "").Trim(); - GameName = Regex.Replace(GameName, @"Rev (\d+\.)?(\d+\.)?(\*|\d+)$", "").Trim(); - List SearchCandidates = GetSearchCandidates(GameName); - + foreach (string SearchCandidate in SearchCandidates) { bool GameFound = false; @@ -244,10 +240,6 @@ namespace gaseous_server.Classes { List searchResults = new List(); - // remove version numbers from name - GameName = Regex.Replace(GameName, @"v(\d+\.)?(\d+\.)?(\*|\d+)$", "").Trim(); - GameName = Regex.Replace(GameName, @"Rev (\d+\.)?(\d+\.)?(\*|\d+)$", "").Trim(); - List SearchCandidates = GetSearchCandidates(GameName); foreach (string SearchCandidate in SearchCandidates) @@ -283,19 +275,24 @@ namespace gaseous_server.Classes private static List GetSearchCandidates(string GameName) { + // remove version numbers from name + GameName = Regex.Replace(GameName, @"v(\d+\.)?(\d+\.)?(\*|\d+)$", "").Trim(); + GameName = Regex.Replace(GameName, @"Rev (\d+\.)?(\d+\.)?(\*|\d+)$", "").Trim(); + List SearchCandidates = new List(); SearchCandidates.Add(GameName); - if (GameName.Contains(":")) + if (GameName.Contains(" - ")) { - GameName = GameName.Substring(0, GameName.IndexOf(":")); - SearchCandidates.Add(GameName.Trim()); + SearchCandidates.Add(GameName.Replace(" - ", ": ")); + SearchCandidates.Add(GameName.Substring(0, GameName.IndexOf(" - ")).Trim()); } - if (GameName.Contains("-")) + if (GameName.Contains(": ")) { - GameName = GameName.Substring(0, GameName.IndexOf("-")); - SearchCandidates.Add(GameName.Trim()); + SearchCandidates.Add(GameName.Substring(0, GameName.IndexOf(": ")).Trim()); } + Logging.Log(Logging.LogType.Information, "Import Game", " Search candidates: " + String.Join(", ", SearchCandidates)); + return SearchCandidates; } diff --git a/gaseous-server/Support/PlatformMap.json b/gaseous-server/Support/PlatformMap.json index c620907..2790941 100644 --- a/gaseous-server/Support/PlatformMap.json +++ b/gaseous-server/Support/PlatformMap.json @@ -72,5 +72,22 @@ "KnownFileExtensions": [ ".Z64" ] + }, + { + "IGDBId": 18, + "IGDBName": "Nintendo Entertainment System", + "AlternateNames": [ + "Nintendo Entertainment System", + "NES" + ], + "KnownFileExtensions": [ + ".NES", + ".FDS", + ".FIG", + ".MGD", + ".SFC", + ".SMC", + ".SWC" + ] } ] From 0cc8e76f775dbe1ad528b4c44ab5df003b539563 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Fri, 30 Jun 2023 19:18:19 +1000 Subject: [PATCH 44/71] feat: added company info support --- gaseous-server/Classes/Metadata/Company.cs | 123 ++++++++++++ .../Classes/Metadata/CompanyLogos.cs | 175 ++++++++++++++++++ gaseous-server/Classes/Metadata/Games.cs | 20 ++ .../Classes/Metadata/InvolvedCompany.cs | 126 +++++++++++++ gaseous-server/Classes/Metadata/Storage.cs | 10 + gaseous-server/Controllers/GamesController.cs | 165 +++++++++++++++++ gaseous-server/wwwroot/pages/game.html | 79 +++++++- gaseous-server/wwwroot/scripts/main.js | 5 + gaseous-server/wwwroot/styles/style.css | 25 ++- gaseous-tools/Config.cs | 7 + gaseous-tools/Database/MySQL/gaseous-1000.sql | 79 ++++++++ 11 files changed, 811 insertions(+), 3 deletions(-) create mode 100644 gaseous-server/Classes/Metadata/Company.cs create mode 100644 gaseous-server/Classes/Metadata/CompanyLogos.cs create mode 100644 gaseous-server/Classes/Metadata/InvolvedCompany.cs diff --git a/gaseous-server/Classes/Metadata/Company.cs b/gaseous-server/Classes/Metadata/Company.cs new file mode 100644 index 0000000..a00e183 --- /dev/null +++ b/gaseous-server/Classes/Metadata/Company.cs @@ -0,0 +1,123 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; + +namespace gaseous_server.Classes.Metadata +{ + public class Companies + { + const string fieldList = "fields change_date,change_date_category,changed_company_id,checksum,country,created_at,description,developed,logo,name,parent,published,slug,start_date,start_date_category,updated_at,url,websites;"; + + public Companies() + { + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static Company? GetCompanies(long? Id) + { + if ((Id == 0) || (Id == null)) + { + return null; + } + else + { + Task RetVal = _GetCompanies(SearchUsing.id, Id); + return RetVal.Result; + } + } + + public static Company GetCompanies(string Slug) + { + Task RetVal = _GetCompanies(SearchUsing.slug, Slug); + return RetVal.Result; + } + + private static async Task _GetCompanies(SearchUsing searchUsing, object searchValue) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("Company", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("Company", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + Company returnValue = new Company(); + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause); + if (returnValue != null) { Storage.NewCacheValue(returnValue); } + UpdateSubClasses(returnValue); + break; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause); + if (returnValue != null) { Storage.NewCacheValue(returnValue, true); } + UpdateSubClasses(returnValue); + break; + case Storage.CacheStatus.Current: + returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + break; + default: + throw new Exception("How did you get here?"); + } + + return returnValue; + } + + private static void UpdateSubClasses(Company company) + { + if (company.Logo != null) + { + CompanyLogo companyLogo = CompanyLogos.GetCompanyLogo(company.Logo.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Company(company)); + } + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause) + { + // get Companies metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.Companies, query: fieldList + " " + WhereClause + ";"); + if (results.Length > 0) + { + var result = results.First(); + + return result; + } + else + { + return null; + } + + } + } +} + diff --git a/gaseous-server/Classes/Metadata/CompanyLogos.cs b/gaseous-server/Classes/Metadata/CompanyLogos.cs new file mode 100644 index 0000000..88ec456 --- /dev/null +++ b/gaseous-server/Classes/Metadata/CompanyLogos.cs @@ -0,0 +1,175 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; +using MySqlX.XDevAPI.Common; +using static gaseous_tools.Config.ConfigFile; + +namespace gaseous_server.Classes.Metadata +{ + public class CompanyLogos + { + const string fieldList = "fields alpha_channel,animated,checksum,height,image_id,url,width;"; + + public CompanyLogos() + { + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static CompanyLogo? GetCompanyLogo(long? Id, string LogoPath) + { + if ((Id == 0) || (Id == null)) + { + return null; + } + else + { + Task RetVal = _GetCompanyLogo(SearchUsing.id, Id, LogoPath); + return RetVal.Result; + } + } + + public static CompanyLogo GetCompanyLogo(string Slug, string LogoPath) + { + Task RetVal = _GetCompanyLogo(SearchUsing.slug, Slug, LogoPath); + return RetVal.Result; + } + + private static async Task _GetCompanyLogo(SearchUsing searchUsing, object searchValue, string LogoPath) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("CompanyLogo", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("CompanyLogo", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + CompanyLogo returnValue = new CompanyLogo(); + bool forceImageDownload = false; + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause, LogoPath); + if (returnValue != null) + { + Storage.NewCacheValue(returnValue); + forceImageDownload = true; + } + break; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause, LogoPath); + if (returnValue != null) + { + Storage.NewCacheValue(returnValue, true); + forceImageDownload = true; + } + break; + case Storage.CacheStatus.Current: + returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + break; + default: + throw new Exception("How did you get here?"); + } + + if (returnValue != null) + { + if ((!File.Exists(Path.Combine(LogoPath, "Logo.jpg"))) || forceImageDownload == true) + { + GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_thumb); + GetImageFromServer(returnValue.Url, LogoPath, LogoSize.t_logo_med); + } + } + + return returnValue; + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause, string LogoPath) + { + // get CompanyLogo metadata + var results = await igdb.QueryAsync(IGDBClient.Endpoints.CompanyLogos, query: fieldList + " " + WhereClause + ";"); + if (results.Length > 0) + { + var result = results.First(); + + GetImageFromServer(result.Url, LogoPath, LogoSize.t_thumb); + GetImageFromServer(result.Url, LogoPath, LogoSize.t_logo_med); + + return result; + } + else + { + return null; + } + } + + private static void GetImageFromServer(string Url, string LogoPath, LogoSize logoSize) + { + using (var client = new HttpClient()) + { + string fileName = "Logo.jpg"; + string extension = "jpg"; + switch (logoSize) + { + case LogoSize.t_thumb: + fileName = "Logo_Thumb"; + extension = "jpg"; + break; + case LogoSize.t_logo_med: + fileName = "Logo_Medium"; + extension = "png"; + break; + default: + fileName = "Logo"; + extension = "jpg"; + break; + } + string imageUrl = Url.Replace(LogoSize.t_thumb.ToString(), logoSize.ToString()).Replace("jpg", extension); + + using (var s = client.GetStreamAsync("https:" + imageUrl)) + { + if (!Directory.Exists(LogoPath)) { Directory.CreateDirectory(LogoPath); } + using (var fs = new FileStream(Path.Combine(LogoPath, fileName + "." + extension), FileMode.OpenOrCreate)) + { + s.Result.CopyTo(fs); + } + } + } + } + + private enum LogoSize + { + t_thumb, + t_logo_med + } + } +} + diff --git a/gaseous-server/Classes/Metadata/Games.cs b/gaseous-server/Classes/Metadata/Games.cs index f83a351..09a6be6 100644 --- a/gaseous-server/Classes/Metadata/Games.cs +++ b/gaseous-server/Classes/Metadata/Games.cs @@ -122,6 +122,7 @@ namespace gaseous_server.Classes.Metadata AgeRating GameAgeRating = AgeRatings.GetAgeRatings(AgeRatingId); } } + if (Game.AlternativeNames != null) { foreach (long AlternativeNameId in Game.AlternativeNames.Ids) @@ -129,6 +130,7 @@ namespace gaseous_server.Classes.Metadata AlternativeName GameAlternativeName = AlternativeNames.GetAlternativeNames(AlternativeNameId); } } + if (Game.Artworks != null) { foreach (long ArtworkId in Game.Artworks.Ids) @@ -136,6 +138,7 @@ namespace gaseous_server.Classes.Metadata Artwork GameArtwork = Artworks.GetArtwork(ArtworkId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(Game)); } } + if (followSubGames) { List gamesToFetch = new List(); @@ -152,14 +155,17 @@ namespace gaseous_server.Classes.Metadata Game relatedGame = GetGame(gameId, false, false); } } + if (Game.Collection != null) { Collection GameCollection = Collections.GetCollections(Game.Collection.Id); } + if (Game.Cover != null) { Cover GameCover = Covers.GetCover(Game.Cover.Id, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(Game)); } + if (Game.ExternalGames != null) { foreach (long ExternalGameId in Game.ExternalGames.Ids) @@ -167,10 +173,12 @@ namespace gaseous_server.Classes.Metadata ExternalGame GameExternalGame = ExternalGames.GetExternalGames(ExternalGameId); } } + if (Game.Franchise != null) { Franchise GameFranchise = Franchises.GetFranchises(Game.Franchise.Id); } + if (Game.Franchises != null) { foreach (long FranchiseId in Game.Franchises.Ids) @@ -178,6 +186,7 @@ namespace gaseous_server.Classes.Metadata Franchise GameFranchise = Franchises.GetFranchises(FranchiseId); } } + if (Game.Genres != null) { foreach (long GenreId in Game.Genres.Ids) @@ -185,6 +194,15 @@ namespace gaseous_server.Classes.Metadata Genre GameGenre = Genres.GetGenres(GenreId); } } + + if (Game.InvolvedCompanies != null) + { + foreach (long involvedCompanyId in Game.InvolvedCompanies.Ids) + { + InvolvedCompany involvedCompany = InvolvedCompanies.GetInvolvedCompanies(involvedCompanyId); + } + } + if (Game.Platforms != null) { foreach (long PlatformId in Game.Platforms.Ids) @@ -192,6 +210,7 @@ namespace gaseous_server.Classes.Metadata Platform GamePlatform = Platforms.GetPlatform(PlatformId); } } + if (Game.Screenshots != null) { foreach (long ScreenshotId in Game.Screenshots.Ids) @@ -199,6 +218,7 @@ namespace gaseous_server.Classes.Metadata Screenshot GameScreenshot = Screenshots.GetScreenshot(ScreenshotId, Config.LibraryConfiguration.LibraryMetadataDirectory_Game(Game)); } } + if (Game.Videos != null) { foreach (long GameVideoId in Game.Videos.Ids) diff --git a/gaseous-server/Classes/Metadata/InvolvedCompany.cs b/gaseous-server/Classes/Metadata/InvolvedCompany.cs new file mode 100644 index 0000000..0d6c3f9 --- /dev/null +++ b/gaseous-server/Classes/Metadata/InvolvedCompany.cs @@ -0,0 +1,126 @@ +using System; +using gaseous_tools; +using IGDB; +using IGDB.Models; + +namespace gaseous_server.Classes.Metadata +{ + public class InvolvedCompanies + { + const string fieldList = "fields *;"; + + public InvolvedCompanies() + { + } + + private static IGDBClient igdb = new IGDBClient( + // Found in Twitch Developer portal for your app + Config.IGDB.ClientId, + Config.IGDB.Secret + ); + + public static InvolvedCompany? GetInvolvedCompanies(long? Id) + { + if ((Id == 0) || (Id == null)) + { + return null; + } + else + { + Task RetVal = _GetInvolvedCompanies(SearchUsing.id, Id); + return RetVal.Result; + } + } + + public static InvolvedCompany GetInvolvedCompanies(string Slug) + { + Task RetVal = _GetInvolvedCompanies(SearchUsing.slug, Slug); + return RetVal.Result; + } + + private static async Task _GetInvolvedCompanies(SearchUsing searchUsing, object searchValue) + { + // check database first + Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); + if (searchUsing == SearchUsing.id) + { + cacheStatus = Storage.GetCacheStatus("InvolvedCompany", (long)searchValue); + } + else + { + cacheStatus = Storage.GetCacheStatus("InvolvedCompany", (string)searchValue); + } + + // set up where clause + string WhereClause = ""; + switch (searchUsing) + { + case SearchUsing.id: + WhereClause = "where id = " + searchValue; + break; + case SearchUsing.slug: + WhereClause = "where slug = " + searchValue; + break; + default: + throw new Exception("Invalid search type"); + } + + InvolvedCompany returnValue = new InvolvedCompany(); + switch (cacheStatus) + { + case Storage.CacheStatus.NotPresent: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue); + UpdateSubClasses(returnValue); + break; + case Storage.CacheStatus.Expired: + returnValue = await GetObjectFromServer(WhereClause); + Storage.NewCacheValue(returnValue, true); + UpdateSubClasses(returnValue); + break; + case Storage.CacheStatus.Current: + returnValue = Storage.GetCacheValue(returnValue, "id", (long)searchValue); + break; + default: + throw new Exception("How did you get here?"); + } + + return returnValue; + } + + private static void UpdateSubClasses(InvolvedCompany involvedCompany) + { + if (involvedCompany.Company != null) + { + Company company = Companies.GetCompanies(involvedCompany.Company.Id); + } + } + + private enum SearchUsing + { + id, + slug + } + + private static async Task GetObjectFromServer(string WhereClause) + { + // get InvolvedCompanies metadata + try + { + var results = await igdb.QueryAsync(IGDBClient.Endpoints.InvolvedCompanies, query: fieldList + " " + WhereClause + ";"); + var result = results.First(); + + return result; + } + catch (Exception ex) + { + Logging.Log(Logging.LogType.Critical, "Involved Companies", "Failure when requesting involved companies."); + Logging.Log(Logging.LogType.Critical, "Involved Companies", "Field list: " + fieldList); + Logging.Log(Logging.LogType.Critical, "Involved Companies", "Where clause: " + WhereClause); + Logging.Log(Logging.LogType.Critical, "Involved Companies", "Error", ex); + throw; + } + } + } +} + diff --git a/gaseous-server/Classes/Metadata/Storage.cs b/gaseous-server/Classes/Metadata/Storage.cs index d2fb9be..1f10712 100644 --- a/gaseous-server/Classes/Metadata/Storage.cs +++ b/gaseous-server/Classes/Metadata/Storage.cs @@ -204,6 +204,10 @@ namespace gaseous_server.Classes.Metadata { switch (objectTypeName) { + //case "boolean": + // Boolean storedBool = Convert.ToBoolean((int)dataRow[property.Name]); + // property.SetValue(EndpointType, storedBool); + // break; case "datetimeoffset": DateTimeOffset? storedDate = (DateTime?)dataRow[property.Name]; property.SetValue(EndpointType, storedDate); @@ -219,6 +223,9 @@ namespace gaseous_server.Classes.Metadata case "collection": objectToStore = new IdentityOrValue(id: (long)dataRow[property.Name]); break; + case "company": + objectToStore = new IdentityOrValue(id: (long)dataRow[property.Name]); + break; case "cover": objectToStore = new IdentityOrValue(id: (long)dataRow[property.Name]); break; @@ -354,6 +361,9 @@ namespace gaseous_server.Classes.Metadata case "[igdb.models.externalcategory": property.SetValue(EndpointType, (ExternalCategory)dataRow[property.Name]); break; + case "[igdb.models.startdatecategory": + property.SetValue(EndpointType, (StartDateCategory)dataRow[property.Name]); + break; default: property.SetValue(EndpointType, dataRow[property.Name]); break; diff --git a/gaseous-server/Controllers/GamesController.cs b/gaseous-server/Controllers/GamesController.cs index 93e4d51..5ec5efc 100644 --- a/gaseous-server/Controllers/GamesController.cs +++ b/gaseous-server/Controllers/GamesController.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Data; using System.IO; @@ -11,6 +12,7 @@ using IGDB.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.CodeAnalysis.Scripting; +using Org.BouncyCastle.Asn1.X509; using static gaseous_server.Classes.Metadata.AgeRatings; namespace gaseous_server.Controllers @@ -481,6 +483,169 @@ namespace gaseous_server.Controllers } } + [HttpGet] + [Route("{GameId}/genre")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ResponseCache(CacheProfileName = "7Days")] + public ActionResult GameGenre(long GameId) + { + try + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + if (gameObject != null) + { + List genreObjects = new List(); + if (gameObject.Genres != null) + { + foreach (long genreId in gameObject.Genres.Ids) + { + genreObjects.Add(Classes.Metadata.Genres.GetGenres(genreId)); + } + } + + List sortedGenreObjects = genreObjects.OrderBy(o => o.Name).ToList(); + + return Ok(sortedGenreObjects); + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + + [HttpGet] + [Route("{GameId}/companies")] + [ProducesResponseType(typeof(List>), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ResponseCache(CacheProfileName = "7Days")] + public ActionResult GameInvolvedCompanies(long GameId) + { + try + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + if (gameObject != null) + { + List> icObjects = new List>(); + if (gameObject.InvolvedCompanies != null) + { + foreach (long icId in gameObject.InvolvedCompanies.Ids) + { + InvolvedCompany involvedCompany = Classes.Metadata.InvolvedCompanies.GetInvolvedCompanies(icId); + Company company = Classes.Metadata.Companies.GetCompanies(involvedCompany.Company.Id); + company.Developed = null; + company.Published = null; + + Dictionary companyData = new Dictionary(); + companyData.Add("involvement", involvedCompany); + companyData.Add("company", company); + + icObjects.Add(companyData); + } + } + + return Ok(icObjects); + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + + [HttpGet] + [Route("{GameId}/companies/{CompanyId}")] + [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ResponseCache(CacheProfileName = "7Days")] + public ActionResult GameInvolvedCompanies(long GameId, long CompanyId) + { + try + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + if (gameObject != null) + { + List> icObjects = new List>(); + if (gameObject.InvolvedCompanies != null) + { + InvolvedCompany involvedCompany = Classes.Metadata.InvolvedCompanies.GetInvolvedCompanies(CompanyId); + Company company = Classes.Metadata.Companies.GetCompanies(involvedCompany.Company.Id); + company.Developed = null; + company.Published = null; + + Dictionary companyData = new Dictionary(); + companyData.Add("involvement", involvedCompany); + companyData.Add("company", company); + + return Ok(companyData); + } else + { + return NotFound(); + } + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + + [HttpGet] + [Route("{GameId}/companies/{CompanyId}/image")] + [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult GameCompanyImage(long GameId, long CompanyId) + { + try + { + IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false); + + InvolvedCompany involvedCompany = Classes.Metadata.InvolvedCompanies.GetInvolvedCompanies(CompanyId); + Company company = Classes.Metadata.Companies.GetCompanies(involvedCompany.Company.Id); + + string coverFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMetadataDirectory_Company(company), "Logo_Medium.png"); + if (System.IO.File.Exists(coverFilePath)) + { + string filename = "Logo.png"; + string filepath = coverFilePath; + byte[] filedata = System.IO.File.ReadAllBytes(filepath); + string contentType = "image/png"; + + var cd = new System.Net.Mime.ContentDisposition + { + FileName = filename, + Inline = true, + }; + + Response.Headers.Add("Content-Disposition", cd.ToString()); + Response.Headers.Add("Cache-Control", "public, max-age=604800"); + + return File(filedata, contentType); + } + else + { + return NotFound(); + } + } + catch + { + return NotFound(); + } + } + [HttpGet] [Route("{GameId}/roms")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] diff --git a/gaseous-server/wwwroot/pages/game.html b/gaseous-server/wwwroot/pages/game.html index 56ffe81..6682078 100644 --- a/gaseous-server/wwwroot/pages/game.html +++ b/gaseous-server/wwwroot/pages/game.html @@ -1,8 +1,11 @@ -
+
+
+

+

Also known as:

@@ -11,6 +14,15 @@
+
+

Genres

+
+
+

Developers

+
+
+

Publishers

+

Age Ratings

@@ -97,7 +109,8 @@ // load artwork if (result.artworks) { artworks = result.artworks.ids; - artworksPostition = 0; + var startPos = randomIntFromInterval(0, result.artworks.ids.length); + artworksPosition = startPos; rotateBackground(); } else { if (result.cover) { @@ -106,6 +119,52 @@ } } + // load companies + var gameHeaderDeveloperLabel = document.getElementById('gamedeveloper_label'); + var gameHeaderDeveloperLogo = document.getElementById('gamedev_logo'); + var gameDeveloperLabel = document.getElementById('gamesummary_developer'); + var gamePublisherLabel = document.getElementById('gamesummary_publishers'); + var gameDeveloperLoaded = false; + var gamePublisherLoaded = false; + if (result.involvedCompanies) { + ajaxCall('/api/v1/games/' + gameId + '/companies', 'GET', function (result) { + for (var i = 0; i < result.length; i++) { + var companyLabel = document.createElement('span'); + companyLabel.className = 'gamegenrelabel'; + companyLabel.innerHTML = result[i].company.name; + + if (result[i].involvement.developer == true) { + if (gameHeaderDeveloperLabel.innerHTML.length > 0) { + gameHeaderDeveloperLabel += ", "; + } + gameHeaderDeveloperLabel.innerHTML += result[i].company.name; + + gameDeveloperLabel.appendChild(companyLabel); + + gameDeveloperLoaded = true; + } else { + if (result[i].involvement.publisher == true) { + gamePublisherLabel.appendChild(companyLabel); + gamePublisherLoaded = true; + } + } + } + + if (gameDeveloperLoaded == false) { + gameHeaderDeveloperLabel.setAttribute('style', 'display: none;'); + gameDeveloperLabel.setAttribute('style', 'display: none;'); + } + if (gamePublisherLoaded == false) { + gamePublisherLabel.setAttribute('style', 'display: none;'); + } + }); + } else { + gameHeaderDeveloperLabel.setAttribute('style', 'display: none;'); + gameHeaderDeveloperLogo.setAttribute('style', 'display: none;'); + gameDeveloperLabel.setAttribute('style', 'display: none;'); + gamePublisherLabel.setAttribute('style', 'display: none;'); + } + // load cover var gameSummaryCover = document.getElementById('gamesummary_cover'); var gameImage = document.createElement('img'); @@ -133,6 +192,22 @@ gameSummaryRatings.setAttribute('style', 'display: none;'); } + // load genres + var gameSummaryGenres = document.getElementById('gamesumarry_genres'); + if (result.genres) { + ajaxCall('/api/v1/Games/' + gameId + '/genre', 'GET', function (result) { + for (var i = 0; i < result.length; i++) { + var genreLabel = document.createElement('span'); + genreLabel.className = 'gamegenrelabel'; + genreLabel.innerHTML = result[i].name; + + gameSummaryGenres.appendChild(genreLabel); + } + }); + } else { + gameSummaryGenres.setAttribute('style', 'display: none;'); + } + // load screenshots var gameScreenshots = document.getElementById('gamescreenshots'); if (result.screenshots || result.videos) { diff --git a/gaseous-server/wwwroot/scripts/main.js b/gaseous-server/wwwroot/scripts/main.js index 07b9ac7..94789ac 100644 --- a/gaseous-server/wwwroot/scripts/main.js +++ b/gaseous-server/wwwroot/scripts/main.js @@ -70,4 +70,9 @@ function showDialog(dialogPage, variables) { modalVariables = variables; $('#modal-content').load('/pages/dialogs/' + dialogPage + '.html'); +} + +function randomIntFromInterval(min, max) { // min and max included + var rand = Math.floor(Math.random() * (max - min + 1) + min); + return rand; } \ No newline at end of file diff --git a/gaseous-server/wwwroot/styles/style.css b/gaseous-server/wwwroot/styles/style.css index 80e91c5..ebf6388 100644 --- a/gaseous-server/wwwroot/styles/style.css +++ b/gaseous-server/wwwroot/styles/style.css @@ -240,6 +240,16 @@ input[id='filter_panel_search'] { z-index: -100; } +#bgImage_Opacity { + background: rgba(56, 56, 56, 0.7); + position: fixed; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; + z-index: -90; +} + #gamepage { top: 0px; bottom: 0; @@ -255,7 +265,6 @@ input[id='filter_panel_search'] { padding-left: 20px; padding-right: 20px; padding-bottom: 20px; - background: rgba(56, 56, 56, 0.7); } #mainbody { @@ -275,6 +284,11 @@ input[id='filter_panel_search'] { width: 100%; } +.gamegenrelabel { + display: block; + white-space: pre; +} + .rating_image { display: inline-block; max-width: 64px; @@ -420,4 +434,13 @@ th { color: white; text-decoration: underline; cursor: pointer; +} + +#gamedev_logo { + float: right; + max-height: 48px; +} + +#gamedeveloper_label { + font-size: 16px; } \ No newline at end of file diff --git a/gaseous-tools/Config.cs b/gaseous-tools/Config.cs index 05ac47f..a2292dd 100644 --- a/gaseous-tools/Config.cs +++ b/gaseous-tools/Config.cs @@ -308,6 +308,13 @@ namespace gaseous_tools return MetadataPath; } + public string LibraryMetadataDirectory_Company(Company company) + { + string MetadataPath = Path.Combine(LibraryMetadataDirectory, "Companies", company.Slug); + if (!Directory.Exists(MetadataPath)) { Directory.CreateDirectory(MetadataPath); } + return MetadataPath; + } + public string LibrarySignatureImportDirectory { get diff --git a/gaseous-tools/Database/MySQL/gaseous-1000.sql b/gaseous-tools/Database/MySQL/gaseous-1000.sql index b44033b..ee50687 100644 --- a/gaseous-tools/Database/MySQL/gaseous-1000.sql +++ b/gaseous-tools/Database/MySQL/gaseous-1000.sql @@ -118,6 +118,61 @@ CREATE TABLE `collection` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `company` +-- + +DROP TABLE IF EXISTS `company`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `company` ( + `id` bigint NOT NULL, + `changedate` datetime DEFAULT NULL, + `changedatecategory` int DEFAULT NULL, + `changedcompanyid` bigint DEFAULT NULL, + `checksum` varchar(45) DEFAULT NULL, + `country` int DEFAULT NULL, + `createdat` datetime DEFAULT NULL, + `description` longtext, + `developed` json DEFAULT NULL, + `logo` bigint DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `parent` bigint DEFAULT NULL, + `published` json DEFAULT NULL, + `slug` varchar(100) DEFAULT NULL, + `startdate` datetime DEFAULT NULL, + `startdatecategory` int DEFAULT NULL, + `updatedat` datetime DEFAULT NULL, + `url` varchar(255) DEFAULT NULL, + `websites` json DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `companylogo` +-- + +DROP TABLE IF EXISTS `companylogo`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `companylogo` ( + `id` bigint NOT NULL, + `alphachannel` tinyint(1) DEFAULT NULL, + `animated` tinyint(1) DEFAULT NULL, + `checksum` varchar(45) DEFAULT NULL, + `height` int DEFAULT NULL, + `imageid` varchar(45) DEFAULT NULL, + `url` varchar(255) DEFAULT NULL, + `width` int DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `cover` -- @@ -348,6 +403,30 @@ CREATE TABLE `genre` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `involvedcompany` +-- + +DROP TABLE IF EXISTS `involvedcompany`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `involvedcompany` ( + `id` bigint NOT NULL, + `checksum` varchar(45) DEFAULT NULL, + `company` bigint DEFAULT NULL, + `createdat` datetime DEFAULT NULL, + `developer` tinyint(1) DEFAULT NULL, + `game` bigint DEFAULT NULL, + `porting` tinyint(1) DEFAULT NULL, + `publisher` tinyint(1) DEFAULT NULL, + `supporting` tinyint(1) DEFAULT NULL, + `updatedat` datetime DEFAULT NULL, + `dateAdded` datetime DEFAULT NULL, + `lastUpdated` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `platform` -- From 0e3a3b3ecdd9dc2871ddaa4ee0c3fea2b1a67407 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Fri, 30 Jun 2023 23:17:03 +1000 Subject: [PATCH 45/71] fix: removed duplicate companies --- gaseous-server/wwwroot/pages/game.html | 30 ++++++++++++++++--------- gaseous-server/wwwroot/styles/style.css | 4 ++-- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/gaseous-server/wwwroot/pages/game.html b/gaseous-server/wwwroot/pages/game.html index 6682078..211a88e 100644 --- a/gaseous-server/wwwroot/pages/game.html +++ b/gaseous-server/wwwroot/pages/game.html @@ -128,24 +128,34 @@ var gamePublisherLoaded = false; if (result.involvedCompanies) { ajaxCall('/api/v1/games/' + gameId + '/companies', 'GET', function (result) { + var lstDevelopers = []; + var lstPublishers = []; + for (var i = 0; i < result.length; i++) { var companyLabel = document.createElement('span'); companyLabel.className = 'gamegenrelabel'; companyLabel.innerHTML = result[i].company.name; if (result[i].involvement.developer == true) { - if (gameHeaderDeveloperLabel.innerHTML.length > 0) { - gameHeaderDeveloperLabel += ", "; + if (!lstDevelopers.includes(result[i].company.name)) { + if (gameHeaderDeveloperLabel.innerHTML.length > 0) { + gameHeaderDeveloperLabel += ", "; + } + gameHeaderDeveloperLabel.innerHTML += result[i].company.name; + + gameDeveloperLabel.appendChild(companyLabel); + + lstDevelopers.push(result[i].company.name); + + gameDeveloperLoaded = true; } - gameHeaderDeveloperLabel.innerHTML += result[i].company.name; - - gameDeveloperLabel.appendChild(companyLabel); - - gameDeveloperLoaded = true; } else { if (result[i].involvement.publisher == true) { - gamePublisherLabel.appendChild(companyLabel); - gamePublisherLoaded = true; + if (!lstPublishers.includes(result[i].company.name)) { + lstPublishers.push(result[i].company.name); + gamePublisherLabel.appendChild(companyLabel); + gamePublisherLoaded = true; + } } } } @@ -346,7 +356,7 @@ for (var i = 0; i < gameScreenshots_Items.length; i++) { if (gameScreenshots_Items[i].id == gameScreenshots_Selected.id) { gameScreenshots_Items[i].classList.add('gamescreenshosts_gallery_item_selected'); - gameScreenshots_Selected.scrollIntoView(); + gameScreenshots_Selected.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" }); } else { gameScreenshots_Items[i].classList.remove('gamescreenshosts_gallery_item_selected'); } diff --git a/gaseous-server/wwwroot/styles/style.css b/gaseous-server/wwwroot/styles/style.css index ebf6388..85bf0e0 100644 --- a/gaseous-server/wwwroot/styles/style.css +++ b/gaseous-server/wwwroot/styles/style.css @@ -16,7 +16,7 @@ h3 { border-bottom-style: solid; /*border-bottom-color: #916b01;*/ - border-bottom-width: 3px; + border-bottom-width: 1px; /*border-image: linear-gradient(to right, blue 25%, yellow 25%, yellow 50%,red 50%, red 75%, teal 75%) 5;*/ border-image: linear-gradient(to right, rgba(255,0,0,1) 0%, rgba(251,255,0,1) 16%, rgba(0,255,250,1) 30%, rgba(0,16,255,1) 46%, rgba(250,0,255,1) 62%, rgba(255,0,0,1) 78%, rgba(255,237,0,1) 90%, rgba(20,255,0,1) 100%) 5; @@ -388,7 +388,7 @@ iframe { } .gamescreenshosts_gallery_item:hover { - border-color: lightblue; + border-color: lightgray; } .gamescreenshosts_gallery_item_selected { From 6c2c093ec920da5954ecb6b928fe190911a93a32 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Fri, 30 Jun 2023 23:20:24 +1000 Subject: [PATCH 46/71] fix: removed reference to deleted html object --- gaseous-server/wwwroot/pages/game.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/gaseous-server/wwwroot/pages/game.html b/gaseous-server/wwwroot/pages/game.html index 211a88e..9cb9fd1 100644 --- a/gaseous-server/wwwroot/pages/game.html +++ b/gaseous-server/wwwroot/pages/game.html @@ -121,7 +121,6 @@ // load companies var gameHeaderDeveloperLabel = document.getElementById('gamedeveloper_label'); - var gameHeaderDeveloperLogo = document.getElementById('gamedev_logo'); var gameDeveloperLabel = document.getElementById('gamesummary_developer'); var gamePublisherLabel = document.getElementById('gamesummary_publishers'); var gameDeveloperLoaded = false; @@ -170,7 +169,6 @@ }); } else { gameHeaderDeveloperLabel.setAttribute('style', 'display: none;'); - gameHeaderDeveloperLogo.setAttribute('style', 'display: none;'); gameDeveloperLabel.setAttribute('style', 'display: none;'); gamePublisherLabel.setAttribute('style', 'display: none;'); } From 1b14e697a2b658abc9047d49206ab3552d97c551 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sun, 2 Jul 2023 01:12:26 +1000 Subject: [PATCH 47/71] feat: added initial Docker support (still testing) and refactored the database for case-sensitive hosts --- .DS_Store | Bin 10244 -> 10244 bytes Dockerfile | 16 + Gaseous.sln | 8 + docker-compose.yml | 39 + gaseous-server/Classes/ImportGames.cs | 8 +- gaseous-server/Classes/Metadata/Games.cs | 2 +- gaseous-server/Classes/Metadata/Platforms.cs | 6 +- gaseous-server/Classes/MetadataManagement.cs | 2 +- gaseous-server/Classes/Roms.cs | 8 +- .../Classes/SignatureIngestors/TOSEC.cs | 24 +- .../Controllers/FilterController.cs | 4 +- gaseous-server/Controllers/GamesController.cs | 18 +- .../Controllers/PlatformsController.cs | 2 +- .../Controllers/SignaturesController.cs | 52 +- gaseous-server/Models/Signatures_Status.cs | 2 +- gaseous-server/Program.cs | 8 +- gaseous-server/gaseous-server.csproj | 11 - gaseous-signature-ingestor/Program.cs | 24 +- gaseous-tools/Config.cs | 25 +- gaseous-tools/Database/MySQL/gaseous-1000.sql | 885 +++++++++--------- 20 files changed, 601 insertions(+), 543 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.DS_Store b/.DS_Store index 87c57ab261fc0ddee6e8378a8fa6d29360931046..e163a901e661b55595c6f1dc9a42208b49a05c19 100644 GIT binary patch delta 85 zcmZn(XbIS`UW{@3 + - igdbclientsecret= + gsdb: + container_name: gsdb + image: mysql:8 + restart: unless-stopped + networks: + - gaseous + volumes: + - gsdb:/var/lib/mysql + environment: + - MYSQL_ROOT_PASSWORD=gaseous + - MYSQL_USER=gaseous + - MYSQL_PASSWORD=gaseous +networks: + gaseous: + driver: bridge +volumes: + gs: + gsdb: diff --git a/gaseous-server/Classes/ImportGames.cs b/gaseous-server/Classes/ImportGames.cs index 3ba9ef6..ec90903 100644 --- a/gaseous-server/Classes/ImportGames.cs +++ b/gaseous-server/Classes/ImportGames.cs @@ -61,7 +61,7 @@ namespace gaseous_server.Classes Common.hashObject hash = new Common.hashObject(GameFileImportPath); // check to make sure we don't already have this file imported - sql = "SELECT COUNT(Id) AS count FROM games_roms WHERE md5=@md5 AND sha1=@sha1"; + sql = "SELECT COUNT(Id) AS count FROM Games_Roms WHERE MD5=@md5 AND SHA1=@sha1"; dbDict.Add("md5", hash.md5hash); dbDict.Add("sha1", hash.sha1hash); DataTable importDB = db.ExecuteCMD(sql, dbDict); @@ -304,7 +304,7 @@ namespace gaseous_server.Classes if (UpdateId == 0) { - sql = "INSERT INTO games_roms (platformid, gameid, name, size, crc, md5, sha1, developmentstatus, flags, romtype, romtypemedia, medialabel, path, metadatasource) VALUES (@platformid, @gameid, @name, @size, @crc, @md5, @sha1, @developmentstatus, @flags, @romtype, @romtypemedia, @medialabel, @path, @metadatasource); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; + sql = "INSERT INTO Games_Roms (PlatformId, GameId, Name, Size, CRC, MD5, SHA1, DevelopmentStatus, Flags, RomType, RomTypeMedia, MediaLabel, Path, MetadataSource) VALUES (@platformid, @gameid, @name, @size, @crc, @md5, @sha1, @developmentstatus, @flags, @romtype, @romtypemedia, @medialabel, @path, @metadatasource); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; } Dictionary dbDict = new Dictionary(); dbDict.Add("platformid", Common.ReturnValueIfNull(determinedPlatform.Id, 0)); @@ -402,7 +402,7 @@ namespace gaseous_server.Classes // update the db Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "UPDATE games_roms SET path=@path WHERE id=@id"; + string sql = "UPDATE Games_Roms SET Path=@path WHERE Id=@id"; Dictionary dbDict = new Dictionary(); dbDict.Add("id", RomId); dbDict.Add("path", DestinationPath); @@ -423,7 +423,7 @@ namespace gaseous_server.Classes // move rom files to their new location Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT * FROM games_roms"; + string sql = "SELECT * FROM Games_Roms"; DataTable romDT = db.ExecuteCMD(sql); if (romDT.Rows.Count > 0) diff --git a/gaseous-server/Classes/Metadata/Games.cs b/gaseous-server/Classes/Metadata/Games.cs index 09a6be6..9a11be1 100644 --- a/gaseous-server/Classes/Metadata/Games.cs +++ b/gaseous-server/Classes/Metadata/Games.cs @@ -26,7 +26,7 @@ namespace gaseous_server.Classes.Metadata if (Id == 0) { Game returnValue = new Game(); - if (Storage.GetCacheStatus("game", 0) == Storage.CacheStatus.NotPresent) + if (Storage.GetCacheStatus("Game", 0) == Storage.CacheStatus.NotPresent) { returnValue = new Game { diff --git a/gaseous-server/Classes/Metadata/Platforms.cs b/gaseous-server/Classes/Metadata/Platforms.cs index 4e3fefa..b61969b 100644 --- a/gaseous-server/Classes/Metadata/Platforms.cs +++ b/gaseous-server/Classes/Metadata/Platforms.cs @@ -27,7 +27,7 @@ namespace gaseous_server.Classes.Metadata if (Id == 0) { Platform returnValue = new Platform(); - if (Storage.GetCacheStatus("platform", 0) == Storage.CacheStatus.NotPresent) + if (Storage.GetCacheStatus("Platform", 0) == Storage.CacheStatus.NotPresent) { returnValue = new Platform { @@ -63,11 +63,11 @@ namespace gaseous_server.Classes.Metadata Storage.CacheStatus? cacheStatus = new Storage.CacheStatus(); if (searchUsing == SearchUsing.id) { - cacheStatus = Storage.GetCacheStatus("platform", (long)searchValue); + cacheStatus = Storage.GetCacheStatus("Platform", (long)searchValue); } else { - cacheStatus = Storage.GetCacheStatus("platform", (string)searchValue); + cacheStatus = Storage.GetCacheStatus("Platform", (string)searchValue); } // set up where clause diff --git a/gaseous-server/Classes/MetadataManagement.cs b/gaseous-server/Classes/MetadataManagement.cs index 8190f53..62e66f7 100644 --- a/gaseous-server/Classes/MetadataManagement.cs +++ b/gaseous-server/Classes/MetadataManagement.cs @@ -9,7 +9,7 @@ namespace gaseous_server.Classes public static void RefreshMetadata(bool forceRefresh = false) { Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT id, `name` FROM game;"; + string sql = "SELECT Id, `Name` FROM Game;"; DataTable dt = db.ExecuteCMD(sql); foreach (DataRow dr in dt.Rows) diff --git a/gaseous-server/Classes/Roms.cs b/gaseous-server/Classes/Roms.cs index 919525a..a22b3e4 100644 --- a/gaseous-server/Classes/Roms.cs +++ b/gaseous-server/Classes/Roms.cs @@ -9,7 +9,7 @@ namespace gaseous_server.Classes public static List GetRoms(long GameId) { Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT * FROM games_roms WHERE gameid = @id ORDER BY `name`"; + string sql = "SELECT * FROM Games_Roms WHERE GameId = @id ORDER BY `Name`"; Dictionary dbDict = new Dictionary(); dbDict.Add("id", GameId); DataTable romDT = db.ExecuteCMD(sql, dbDict); @@ -33,7 +33,7 @@ namespace gaseous_server.Classes public static GameRomItem GetRom(long RomId) { Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT * FROM games_roms WHERE id = @id"; + string sql = "SELECT * FROM Games_Roms WHERE Id = @id"; Dictionary dbDict = new Dictionary(); dbDict.Add("id", RomId); DataTable romDT = db.ExecuteCMD(sql, dbDict); @@ -59,7 +59,7 @@ namespace gaseous_server.Classes IGDB.Models.Game game = Classes.Metadata.Games.GetGame(GameId, false, false); Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "UPDATE games_roms SET platformid=@platformid, gameid=@gameid WHERE id = @id"; + string sql = "UPDATE Games_Roms SET PlatformId=@platformid, GameId=@gameid WHERE Id = @id"; Dictionary dbDict = new Dictionary(); dbDict.Add("id", RomId); dbDict.Add("platformid", PlatformId); @@ -80,7 +80,7 @@ namespace gaseous_server.Classes } Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "DELETE FROM games_roms WHERE id = @id"; + string sql = "DELETE FROM Games_Roms WHERE Id = @id"; Dictionary dbDict = new Dictionary(); dbDict.Add("id", RomId); db.ExecuteCMD(sql, dbDict); diff --git a/gaseous-server/Classes/SignatureIngestors/TOSEC.cs b/gaseous-server/Classes/SignatureIngestors/TOSEC.cs index 72de44a..85b4557 100644 --- a/gaseous-server/Classes/SignatureIngestors/TOSEC.cs +++ b/gaseous-server/Classes/SignatureIngestors/TOSEC.cs @@ -32,7 +32,7 @@ namespace gaseous_server.SignatureIngestors.TOSEC // check tosec file md5 Common.hashObject hashObject = new Common.hashObject(tosecXMLFile); - sql = "SELECT * FROM signatures_sources WHERE sourcemd5=@sourcemd5"; + sql = "SELECT * FROM Signatures_Sources WHERE SourceMD5=@sourcemd5"; dbDict = new Dictionary(); dbDict.Add("sourcemd5", hashObject.md5hash); sigDB = db.ExecuteCMD(sql, dbDict); @@ -51,7 +51,7 @@ namespace gaseous_server.SignatureIngestors.TOSEC bool processGames = false; if (tosecObject.SourceMd5 != null) { - sql = "SELECT * FROM signatures_sources WHERE sourcemd5=@sourcemd5"; + sql = "SELECT * FROM Signatures_Sources WHERE SourceMD5=@sourcemd5"; dbDict = new Dictionary(); dbDict.Add("name", Common.ReturnValueIfNull(tosecObject.Name, "")); dbDict.Add("description", Common.ReturnValueIfNull(tosecObject.Description, "")); @@ -69,7 +69,7 @@ namespace gaseous_server.SignatureIngestors.TOSEC if (sigDB.Rows.Count == 0) { // entry not present, insert it - sql = "INSERT INTO signatures_sources (name, description, category, version, author, email, homepage, url, sourcetype, sourcemd5, sourcesha1) VALUES (@name, @description, @category, @version, @author, @email, @homepage, @uri, @sourcetype, @sourcemd5, @sourcesha1)"; + sql = "INSERT INTO Signatures_Sources (Name, Description, Category, Version, Author, Email, Homepage, Url, SourceType, SourceMD5, SourceSHA1) VALUES (@name, @description, @category, @version, @author, @email, @homepage, @uri, @sourcetype, @sourcemd5, @sourcesha1)"; db.ExecuteCMD(sql, dbDict); @@ -101,13 +101,13 @@ namespace gaseous_server.SignatureIngestors.TOSEC int gameSystem = 0; if (gameObject.System != null) { - sql = "SELECT id FROM signatures_platforms WHERE platform=@platform"; + sql = "SELECT Id FROM Signatures_Platforms WHERE Platform=@platform"; sigDB = db.ExecuteCMD(sql, dbDict); if (sigDB.Rows.Count == 0) { // entry not present, insert it - sql = "INSERT INTO signatures_platforms (platform) VALUES (@platform); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; + sql = "INSERT INTO Signatures_Platforms (Platform) VALUES (@platform); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; sigDB = db.ExecuteCMD(sql, dbDict); gameSystem = Convert.ToInt32(sigDB.Rows[0][0]); @@ -123,13 +123,13 @@ namespace gaseous_server.SignatureIngestors.TOSEC int gamePublisher = 0; if (gameObject.Publisher != null) { - sql = "SELECT * FROM signatures_publishers WHERE publisher=@publisher"; + sql = "SELECT * FROM Signatures_Publishers WHERE Publisher=@publisher"; sigDB = db.ExecuteCMD(sql, dbDict); if (sigDB.Rows.Count == 0) { // entry not present, insert it - sql = "INSERT INTO signatures_publishers (publisher) VALUES (@publisher); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; + sql = "INSERT INTO Signatures_Publishers (Publisher) VALUES (@publisher); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; sigDB = db.ExecuteCMD(sql, dbDict); gamePublisher = Convert.ToInt32(sigDB.Rows[0][0]); } @@ -142,14 +142,14 @@ namespace gaseous_server.SignatureIngestors.TOSEC // store game int gameId = 0; - sql = "SELECT * FROM signatures_games WHERE name=@name AND year=@year AND publisherid=@publisher AND systemid=@systemid AND country=@country AND language=@language"; + sql = "SELECT * FROM Signatures_Games WHERE Name=@name AND Year=@year AND Publisherid=@publisher AND Systemid=@systemid AND Country=@country AND Language=@language"; sigDB = db.ExecuteCMD(sql, dbDict); if (sigDB.Rows.Count == 0) { // entry not present, insert it - sql = "INSERT INTO signatures_games " + - "(name, description, year, publisherid, demo, systemid, systemvariant, video, country, language, copyright) VALUES " + + sql = "INSERT INTO Signatures_Games " + + "(Name, Description, Year, PublisherId, Demo, SystemId, SystemVariant, Video, Country, Language, Copyright) VALUES " + "(@name, @description, @year, @publisherid, @demo, @systemid, @systemvariant, @video, @country, @language, @copyright); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; sigDB = db.ExecuteCMD(sql, dbDict); @@ -166,7 +166,7 @@ namespace gaseous_server.SignatureIngestors.TOSEC if (romObject.Md5 != null) { int romId = 0; - sql = "SELECT * FROM signatures_roms WHERE gameid=@gameid AND md5=@md5"; + sql = "SELECT * FROM Signatures_Roms WHERE GameId=@gameid AND MD5=@md5"; dbDict = new Dictionary(); dbDict.Add("gameid", gameId); dbDict.Add("name", Common.ReturnValueIfNull(romObject.Name, "")); @@ -200,7 +200,7 @@ namespace gaseous_server.SignatureIngestors.TOSEC if (sigDB.Rows.Count == 0) { // entry not present, insert it - sql = "INSERT INTO signatures_roms (gameid, name, size, crc, md5, sha1, developmentstatus, flags, romtype, romtypemedia, medialabel, metadatasource) VALUES (@gameid, @name, @size, @crc, @md5, @sha1, @developmentstatus, @flags, @romtype, @romtypemedia, @medialabel, @metadatasource); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; + sql = "INSERT INTO Signatures_Roms (GameId, Name, Size, CRC, MD5, SHA1, DevelopmentStatus, Flags, RomType, RomTypeMedia, MediaLabel, MetadataSource) VALUES (@gameid, @name, @size, @crc, @md5, @sha1, @developmentstatus, @flags, @romtype, @romtypemedia, @medialabel, @metadatasource); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; sigDB = db.ExecuteCMD(sql, dbDict); diff --git a/gaseous-server/Controllers/FilterController.cs b/gaseous-server/Controllers/FilterController.cs index 865510a..1a5b31a 100644 --- a/gaseous-server/Controllers/FilterController.cs +++ b/gaseous-server/Controllers/FilterController.cs @@ -25,7 +25,7 @@ namespace gaseous_server.Controllers // platforms List platforms = new List(); - string sql = "SELECT platform.id, platform.abbreviation, platform.alternativename, platform.`name`, platform.platformlogo, (SELECT COUNT(games_roms.id) AS RomCount FROM games_roms WHERE games_roms.platformid = platform.id) AS RomCount FROM platform HAVING RomCount > 0 ORDER BY `name`"; + string sql = "SELECT Platform.Id, Platform.Abbreviation, Platform.AlternativeName, Platform.`Name`, Platform.PlatformLogo, (SELECT COUNT(Games_Roms.Id) AS RomCount FROM Games_Roms WHERE Games_Roms.PlatformId = Platform.Id) AS RomCount FROM Platform HAVING RomCount > 0 ORDER BY `Name`"; DataTable dbResponse = db.ExecuteCMD(sql); foreach (DataRow dr in dbResponse.Rows) @@ -36,7 +36,7 @@ namespace gaseous_server.Controllers // genres List genres = new List(); - sql = "SELECT DISTINCT t1.id, t1.`name` FROM genre AS t1 JOIN (SELECT * FROM game WHERE (SELECT COUNT(id) FROM games_roms WHERE gameid = game.id) > 0) AS t2 ON JSON_CONTAINS(t2.genres, CAST(t1.id AS char), '$') ORDER BY t1.`name`"; + sql = "SELECT DISTINCT t1.Id, t1.`Name` FROM Genre AS t1 JOIN (SELECT * FROM Game WHERE (SELECT COUNT(Id) FROM Games_Roms WHERE GameId = Game.Id) > 0) AS t2 ON JSON_CONTAINS(t2.Genres, CAST(t1.Id AS char), '$') ORDER BY t1.`Name`"; dbResponse = db.ExecuteCMD(sql); foreach (DataRow dr in dbResponse.Rows) diff --git a/gaseous-server/Controllers/GamesController.cs b/gaseous-server/Controllers/GamesController.cs index 5ec5efc..4946243 100644 --- a/gaseous-server/Controllers/GamesController.cs +++ b/gaseous-server/Controllers/GamesController.cs @@ -36,14 +36,14 @@ namespace gaseous_server.Controllers if (name.Length > 0) { - tempVal = "`name` LIKE @name"; - whereParams.Add("@name", "%" + name + "%"); + tempVal = "`Name` LIKE @Name"; + whereParams.Add("@Name", "%" + name + "%"); havingClauses.Add(tempVal); } if (platform.Length > 0) { - tempVal = "games_roms.platformid IN ("; + tempVal = "Games_Roms.PlatformId IN ("; string[] platformClauseItems = platform.Split(","); for (int i = 0; i < platformClauseItems.Length; i++) { @@ -51,7 +51,7 @@ namespace gaseous_server.Controllers { tempVal += ", "; } - string platformLabel = "@platform" + i; + string platformLabel = "@Platform" + i; tempVal += platformLabel; whereParams.Add(platformLabel, platformClauseItems[i]); } @@ -69,8 +69,8 @@ namespace gaseous_server.Controllers { tempVal += " AND "; } - string genreLabel = "@genre" + i; - tempVal += "JSON_CONTAINS(game.genres, " + genreLabel + ", '$')"; + string genreLabel = "@Genre" + i; + tempVal += "JSON_CONTAINS(Game.Genres, " + genreLabel + ", '$')"; whereParams.Add(genreLabel, genreClauseItems[i]); } tempVal += ")"; @@ -106,14 +106,14 @@ namespace gaseous_server.Controllers } // order by clause - string orderByClause = "ORDER BY `name` ASC"; + string orderByClause = "ORDER BY `Name` ASC"; if (sortdescending == true) { - orderByClause = "ORDER BY `name` DESC"; + orderByClause = "ORDER BY `Name` DESC"; } Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT DISTINCT games_roms.gameid AS ROMGameId, game.id, game.ageratings, game.aggregatedrating, game.aggregatedratingcount, game.alternativenames, game.artworks, game.bundles, game.category, game.collection, game.cover, game.dlcs, game.expansions, game.externalgames, game.firstreleasedate, game.`follows`, game.franchise, game.franchises, game.gameengines, game.gamemodes, game.genres, game.hypes, game.involvedcompanies, game.keywords, game.multiplayermodes, game.`name`, game.parentgame, game.platforms, game.playerperspectives, game.rating, game.ratingcount, game.releasedates, game.screenshots, game.similargames, game.slug, game.standaloneexpansions, game.`status`, game.storyline, game.summary, game.tags, game.themes, game.totalrating, game.totalratingcount, game.versionparent, game.versiontitle, game.videos, game.websites FROM gaseous.games_roms LEFT JOIN game ON game.id = games_roms.gameid " + whereClause + " " + havingClause + " " + orderByClause; + string sql = "SELECT DISTINCT Games_Roms.GameId AS ROMGameId, Game.Id, Game.AgeRatings, Game.AggregatedRating, Game.AggregatedRatingCount, Game.AlternativeNames, Game.Artworks, Game.Bundles, Game.Category, Game.Collection, Game.Cover, Game.Dlcs, Game.Expansions, Game.ExternalGames, Game.FirstReleaseDate, Game.`Follows`, Game.Franchise, Game.Franchises, Game.GameEngines, Game.GameModes, Game.Genres, Game.Hypes, Game.InvolvedCompanies, Game.Keywords, Game.MultiplayerModes, Game.`Name`, Game.ParentGame, Game.Platforms, Game.PlayerPerspectives, Game.Rating, Game.RatingCount, Game.ReleaseDates, Game.Screenshots, Game.SimilarGames, Game.Slug, Game.StandaloneExpansions, Game.`Status`, Game.StoryLine, Game.Summary, Game.Tags, Game.Themes, Game.TotalRating, Game.TotalRatingCount, Game.VersionParent, Game.VersionTitle, Game.Videos, Game.Websites FROM gaseous.Games_Roms LEFT JOIN Game ON Game.Id = Games_Roms.GameId " + whereClause + " " + havingClause + " " + orderByClause; List RetVal = new List(); diff --git a/gaseous-server/Controllers/PlatformsController.cs b/gaseous-server/Controllers/PlatformsController.cs index 650ea7d..7b5fb0c 100644 --- a/gaseous-server/Controllers/PlatformsController.cs +++ b/gaseous-server/Controllers/PlatformsController.cs @@ -24,7 +24,7 @@ namespace gaseous_server.Controllers { Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT * FROM gaseous.platform WHERE id IN (SELECT DISTINCT platformid FROM games_roms) ORDER BY `name` ASC;"; + string sql = "SELECT * FROM gaseous.Platform WHERE Id IN (SELECT DISTINCT PlatformId FROM Games_Roms) ORDER BY `Name` ASC;"; List RetVal = new List(); diff --git a/gaseous-server/Controllers/SignaturesController.cs b/gaseous-server/Controllers/SignaturesController.cs index 4d521dc..5aa2672 100644 --- a/gaseous-server/Controllers/SignaturesController.cs +++ b/gaseous-server/Controllers/SignaturesController.cs @@ -32,10 +32,10 @@ namespace gaseous_server.Controllers { if (md5.Length > 0) { - return _GetSignature("signatures_roms.md5 = @searchstring", md5); + return _GetSignature("Signatures_Roms.md5 = @searchstring", md5); } else { - return _GetSignature("signatures_roms.sha1 = @searchstring", sha1); + return _GetSignature("Signatures_Roms.sha1 = @searchstring", sha1); } } @@ -45,7 +45,7 @@ namespace gaseous_server.Controllers { if (TosecName.Length > 0) { - return _GetSignature("signatures_roms.name = @searchstring", TosecName); + return _GetSignature("Signatures_Roms.name = @searchstring", TosecName); } else { return null; @@ -55,7 +55,7 @@ namespace gaseous_server.Controllers private List _GetSignature(string sqlWhere, string searchString) { Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT \n view_signatures_games.*,\n signatures_roms.id AS romid,\n signatures_roms.name AS romname,\n signatures_roms.size,\n signatures_roms.crc,\n signatures_roms.md5,\n signatures_roms.sha1,\n signatures_roms.developmentstatus,\n signatures_roms.flags,\n signatures_roms.romtype,\n signatures_roms.romtypemedia,\n signatures_roms.medialabel,\n signatures_roms.metadatasource\nFROM\n signatures_roms\n INNER JOIN\n view_signatures_games ON signatures_roms.gameid = view_signatures_games.id WHERE " + sqlWhere; + string sql = "SELECT view_Signatures_Games.*, Signatures_Roms.Id AS romid, Signatures_Roms.Name AS romname, Signatures_Roms.Size, Signatures_Roms.CRC, Signatures_Roms.MD5, Signatures_Roms.SHA1, Signatures_Roms.DevelopmentStatus, Signatures_Roms.Flags, Signatures_Roms.RomType, Signatures_Roms.RomTypeMedia, Signatures_Roms.MediaLabel, Signatures_Roms.MetadataSource FROM Signatures_Roms INNER JOIN view_Signatures_Games ON Signatures_Roms.GameId = view_Signatures_Games.Id WHERE " + sqlWhere; Dictionary dbDict = new Dictionary(); dbDict.Add("searchString", searchString); @@ -69,33 +69,33 @@ namespace gaseous_server.Controllers { Game = new Models.Signatures_Games.GameItem { - Id = (Int32)sigDbRow["id"], - Name = (string)sigDbRow["name"], - Description = (string)sigDbRow["description"], - Year = (string)sigDbRow["year"], - Publisher = (string)sigDbRow["publisher"], - Demo = (Models.Signatures_Games.GameItem.DemoTypes)(int)sigDbRow["demo"], - System = (string)sigDbRow["platform"], - SystemVariant = (string)sigDbRow["systemvariant"], - Video = (string)sigDbRow["video"], - Country = (string)sigDbRow["country"], - Language = (string)sigDbRow["language"], - Copyright = (string)sigDbRow["copyright"] + Id = (Int32)sigDbRow["Id"], + Name = (string)sigDbRow["Name"], + Description = (string)sigDbRow["Description"], + Year = (string)sigDbRow["Year"], + Publisher = (string)sigDbRow["Publisher"], + Demo = (Models.Signatures_Games.GameItem.DemoTypes)(int)sigDbRow["Demo"], + System = (string)sigDbRow["Platform"], + SystemVariant = (string)sigDbRow["SystemVariant"], + Video = (string)sigDbRow["Video"], + Country = (string)sigDbRow["Country"], + Language = (string)sigDbRow["Language"], + Copyright = (string)sigDbRow["Copyright"] }, Rom = new Models.Signatures_Games.RomItem { Id = (Int32)sigDbRow["romid"], Name = (string)sigDbRow["romname"], - Size = (Int64)sigDbRow["size"], - Crc = (string)sigDbRow["crc"], - Md5 = (string)sigDbRow["md5"], - Sha1 = (string)sigDbRow["sha1"], - DevelopmentStatus = (string)sigDbRow["developmentstatus"], - flags = Newtonsoft.Json.JsonConvert.DeserializeObject>((string)sigDbRow["flags"]), - RomType = (Models.Signatures_Games.RomItem.RomTypes)(int)sigDbRow["romtype"], - RomTypeMedia = (string)sigDbRow["romtypemedia"], - MediaLabel = (string)sigDbRow["medialabel"], - SignatureSource = (Models.Signatures_Games.RomItem.SignatureSourceType)(Int32)sigDbRow["metadatasource"] + Size = (Int64)sigDbRow["Size"], + Crc = (string)sigDbRow["CRC"], + Md5 = (string)sigDbRow["MD5"], + Sha1 = (string)sigDbRow["SHA1"], + DevelopmentStatus = (string)sigDbRow["SevelopmentStatus"], + flags = Newtonsoft.Json.JsonConvert.DeserializeObject>((string)sigDbRow["Flags"]), + RomType = (Models.Signatures_Games.RomItem.RomTypes)(int)sigDbRow["RomType"], + RomTypeMedia = (string)sigDbRow["RomTypeMedia"], + MediaLabel = (string)sigDbRow["MediaLabel"], + SignatureSource = (Models.Signatures_Games.RomItem.SignatureSourceType)(Int32)sigDbRow["MetadataSource"] } }; GamesList.Add(gameItem); diff --git a/gaseous-server/Models/Signatures_Status.cs b/gaseous-server/Models/Signatures_Status.cs index 8d2c904..44f9cac 100644 --- a/gaseous-server/Models/Signatures_Status.cs +++ b/gaseous-server/Models/Signatures_Status.cs @@ -15,7 +15,7 @@ namespace gaseous_server.Models public Signatures_Status() { Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "select (select count(*) from signatures_sources) as SourceCount, (select count(*) from signatures_platforms) as PlatformCount, (select count(*) from signatures_games) as GameCount, (select count(*) from signatures_roms) as RomCount;"; + string sql = "select (select count(*) from Signatures_Sources) as SourceCount, (select count(*) from Signatures_Platforms) as PlatformCount, (select count(*) from Signatures_Games) as GameCount, (select count(*) from Signatures_Roms) as RomCount;"; DataTable sigDb = db.ExecuteCMD(sql); if (sigDb.Rows.Count > 0) diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index 7ae0da9..28ed3cd 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -63,13 +63,13 @@ builder.Services.AddHostedService(); var app = builder.Build(); // Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ +//if (app.Environment.IsDevelopment()) +//{ app.UseSwagger(); app.UseSwaggerUI(); -} +//} -app.UseHttpsRedirection(); +//app.UseHttpsRedirection(); app.UseResponseCaching(); diff --git a/gaseous-server/gaseous-server.csproj b/gaseous-server/gaseous-server.csproj index 547584f..8cbfa7d 100644 --- a/gaseous-server/gaseous-server.csproj +++ b/gaseous-server/gaseous-server.csproj @@ -119,14 +119,6 @@ - - - - - - - - @@ -173,7 +165,4 @@ - - - diff --git a/gaseous-signature-ingestor/Program.cs b/gaseous-signature-ingestor/Program.cs index 5108736..3a0b65b 100644 --- a/gaseous-signature-ingestor/Program.cs +++ b/gaseous-signature-ingestor/Program.cs @@ -86,7 +86,7 @@ if (Directory.Exists(tosecXML)) // check tosec file md5 Console.WriteLine(" ==> Checking input file "); Common.hashObject hashObject = new Common.hashObject(tosecXMLFile); - sql = "SELECT * FROM signatures_sources WHERE sourcemd5=@sourcemd5"; + sql = "SELECT * FROM Signatures_Sources WHERE SourceMD5=@sourcemd5"; dbDict = new Dictionary(); dbDict.Add("sourcemd5", hashObject.md5hash); sigDB = db.ExecuteCMD(sql, dbDict); @@ -108,7 +108,7 @@ if (Directory.Exists(tosecXML)) Console.SetCursorPosition(0, Console.CursorTop - 1); Console.WriteLine(" ==> Storing file in database "); - sql = "SELECT * FROM signatures_sources WHERE sourcemd5=@sourcemd5"; + sql = "SELECT * FROM Signatures_Sources WHERE SourceMD5=@sourcemd5"; dbDict = new Dictionary(); dbDict.Add("name", Common.ReturnValueIfNull(tosecObject.Name, "")); dbDict.Add("description", Common.ReturnValueIfNull(tosecObject.Description, "")); @@ -126,7 +126,7 @@ if (Directory.Exists(tosecXML)) if (sigDB.Rows.Count == 0) { // entry not present, insert it - sql = "INSERT INTO signatures_sources (name, description, category, version, author, email, homepage, url, sourcetype, sourcemd5, sourcesha1) VALUES (@name, @description, @category, @version, @author, @email, @homepage, @uri, @sourcetype, @sourcemd5, @sourcesha1)"; + sql = "INSERT INTO Signatures_Sources (Name, Description, Category, Version, Author, Email, Homepage, Url, SourceType, SourceMD5, sourceSHA1) VALUES (@name, @description, @category, @version, @author, @email, @homepage, @uri, @sourcetype, @sourcemd5, @sourcesha1)"; db.ExecuteCMD(sql, dbDict); @@ -167,13 +167,13 @@ if (Directory.Exists(tosecXML)) int gameSystem = 0; if (gameObject.System != null) { - sql = "SELECT id FROM signatures_platforms WHERE platform=@platform"; + sql = "SELECT Id FROM Signatures_Platforms WHERE Platform=@platform"; sigDB = db.ExecuteCMD(sql, dbDict); if (sigDB.Rows.Count == 0) { // entry not present, insert it - sql = "INSERT INTO signatures_platforms (platform) VALUES (@platform); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; + sql = "INSERT INTO Signatures_Platforms (Platform) VALUES (@platform); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; sigDB = db.ExecuteCMD(sql, dbDict); gameSystem = Convert.ToInt32(sigDB.Rows[0][0]); @@ -189,13 +189,13 @@ if (Directory.Exists(tosecXML)) int gamePublisher = 0; if (gameObject.Publisher != null) { - sql = "SELECT * FROM signatures_publishers WHERE publisher=@publisher"; + sql = "SELECT * FROM Signatures_Publishers WHERE Publisher=@publisher"; sigDB = db.ExecuteCMD(sql, dbDict); if (sigDB.Rows.Count == 0) { // entry not present, insert it - sql = "INSERT INTO signatures_publishers (publisher) VALUES (@publisher); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; + sql = "INSERT INTO Signatures_Publishers (Publisher) VALUES (@publisher); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; sigDB = db.ExecuteCMD(sql, dbDict); gamePublisher = Convert.ToInt32(sigDB.Rows[0][0]); } @@ -208,14 +208,14 @@ if (Directory.Exists(tosecXML)) // store game int gameId = 0; - sql = "SELECT * FROM signatures_games WHERE name=@name AND year=@year AND publisherid=@publisher AND systemid=@systemid AND country=@country AND language=@language"; + sql = "SELECT * FROM Signatures_Games WHERE Name=@name AND Year=@year AND PublisherId=@publisher AND SystemId=@systemid AND Country=@country AND Language=@language"; sigDB = db.ExecuteCMD(sql, dbDict); if (sigDB.Rows.Count == 0) { // entry not present, insert it - sql = "INSERT INTO signatures_games " + - "(name, description, year, publisherid, demo, systemid, systemvariant, video, country, language, copyright) VALUES " + + sql = "INSERT INTO Signatures_Games " + + "(Name, Description, Year, PublisherId, Demo, SystemId, SystemVariant, Video, Country, Language, Copyright) VALUES " + "(@name, @description, @year, @publisherid, @demo, @systemid, @systemvariant, @video, @country, @language, @copyright); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; sigDB = db.ExecuteCMD(sql, dbDict); @@ -232,7 +232,7 @@ if (Directory.Exists(tosecXML)) if (romObject.Md5 != null) { int romId = 0; - sql = "SELECT * FROM signatures_roms WHERE gameid=@gameid AND md5=@md5"; + sql = "SELECT * FROM Signatures_Roms WHERE GameId=@gameid AND MD5=@md5"; dbDict = new Dictionary(); dbDict.Add("gameid", gameId); dbDict.Add("name", Common.ReturnValueIfNull(romObject.Name, "")); @@ -265,7 +265,7 @@ if (Directory.Exists(tosecXML)) if (sigDB.Rows.Count == 0) { // entry not present, insert it - sql = "INSERT INTO signatures_roms (gameid, name, size, crc, md5, sha1, developmentstatus, flags, romtype, romtypemedia, medialabel) VALUES (@gameid, @name, @size, @crc, @md5, @sha1, @developmentstatus, @flags, @romtype, @romtypemedia, @medialabel); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; + sql = "INSERT INTO Signatures_Roms (GameId, Name, Size, CRC, MD5, SHA1, DevelopmentStatus, Flags, RomType, RomTypeMedia, MediaLabel) VALUES (@gameid, @name, @size, @crc, @md5, @sha1, @developmentstatus, @flags, @romtype, @romtypemedia, @medialabel); SELECT CAST(LAST_INSERT_ID() AS SIGNED);"; sigDB = db.ExecuteCMD(sql, dbDict); diff --git a/gaseous-tools/Config.cs b/gaseous-tools/Config.cs index a2292dd..241916d 100644 --- a/gaseous-tools/Config.cs +++ b/gaseous-tools/Config.cs @@ -127,6 +127,11 @@ namespace gaseous_tools serializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter()); string configRaw = Newtonsoft.Json.JsonConvert.SerializeObject(_config, serializerSettings); + if (!Directory.Exists(ConfigurationPath)) + { + Directory.CreateDirectory(ConfigurationPath); + } + if (File.Exists(ConfigurationFilePath_Backup)) { File.Delete(ConfigurationFilePath_Backup); @@ -143,18 +148,18 @@ namespace gaseous_tools public static void InitSettings() { Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT * FROM settings"; + string sql = "SELECT * FROM Settings"; DataTable dbResponse = db.ExecuteCMD(sql); foreach (DataRow dataRow in dbResponse.Rows) { - if (AppSettings.ContainsKey((string)dataRow["setting"])) + if (AppSettings.ContainsKey((string)dataRow["Setting"])) { - AppSettings[(string)dataRow["setting"]] = (string)dataRow["value"]; + AppSettings[(string)dataRow["Setting"]] = (string)dataRow["Value"]; } else { - AppSettings.Add((string)dataRow["setting"], (string)dataRow["value"]); + AppSettings.Add((string)dataRow["Setting"], (string)dataRow["Value"]); } } } @@ -168,10 +173,10 @@ namespace gaseous_tools else { Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT * FROM settings WHERE setting = @settingname"; + string sql = "SELECT * FROM Settings WHERE Setting = @SettingName"; Dictionary dbDict = new Dictionary(); - dbDict.Add("settingname", SettingName); - dbDict.Add("value", DefaultValue); + dbDict.Add("SettingName", SettingName); + dbDict.Add("Value", DefaultValue); try { @@ -200,10 +205,10 @@ namespace gaseous_tools public static void SetSetting(string SettingName, string Value) { Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "REPLACE INTO settings (setting, value) VALUES (@settingname, @value)"; + string sql = "REPLACE INTO Settings (Setting, Value) VALUES (@SettingName, @Value)"; Dictionary dbDict = new Dictionary(); - dbDict.Add("settingname", SettingName); - dbDict.Add("value", Value); + dbDict.Add("SettingName", SettingName); + dbDict.Add("Value", Value); Logging.Log(Logging.LogType.Debug, "Database", "Storing setting '" + SettingName + "' to value: '" + Value + "'"); try diff --git a/gaseous-tools/Database/MySQL/gaseous-1000.sql b/gaseous-tools/Database/MySQL/gaseous-1000.sql index ee50687..d81d13f 100644 --- a/gaseous-tools/Database/MySQL/gaseous-1000.sql +++ b/gaseous-tools/Database/MySQL/gaseous-1000.sql @@ -7,7 +7,7 @@ /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!50503 SET NAMES utf8 */; +/*!50503 SET NameS utf8 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; @@ -16,659 +16,660 @@ /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- --- Table structure for table `agerating` +-- Table structure for table `AgeRating` -- -DROP TABLE IF EXISTS `agerating`; +DROP TABLE IF EXISTS `AgeRating`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `agerating` ( - `id` bigint NOT NULL, - `category` int DEFAULT NULL, - `checksum` varchar(45) DEFAULT NULL, - `contentdescriptions` json DEFAULT NULL, - `rating` int DEFAULT NULL, - `ratingcoverurl` varchar(255) DEFAULT NULL, - `synopsis` longtext, +CREATE TABLE `AgeRating` ( + `Id` bigint NOT NULL, + `Category` int DEFAULT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `ContentDescriptions` json DEFAULT NULL, + `Rating` int DEFAULT NULL, + `RatingCoverUrl` varchar(255) DEFAULT NULL, + `Synopsis` longtext, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `ageratingcontentdescription` +-- Table structure for table `AgeRatingContentDescription` -- -DROP TABLE IF EXISTS `ageratingcontentdescription`; +DROP TABLE IF EXISTS `AgeRatingContentDescription`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `ageratingcontentdescription` ( - `id` bigint NOT NULL, - `category` int DEFAULT NULL, - `checksum` varchar(45) DEFAULT NULL, - `description` varchar(255) DEFAULT NULL, +CREATE TABLE `AgeRatingContentDescription` ( + `Id` bigint NOT NULL, + `Category` int DEFAULT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `Description` varchar(255) DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `alternativename` +-- Table structure for table `AlternativeName` -- -DROP TABLE IF EXISTS `alternativename`; +DROP TABLE IF EXISTS `AlternativeName`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `alternativename` ( - `id` bigint NOT NULL, - `checksum` varchar(45) DEFAULT NULL, - `comment` longtext, - `game` bigint DEFAULT NULL, - `name` varchar(255) DEFAULT NULL, +CREATE TABLE `AlternativeName` ( + `Id` bigint NOT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `Comment` longtext, + `Game` bigint DEFAULT NULL, + `Name` varchar(255) DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `artwork` +-- Table structure for table `Artwork` -- -DROP TABLE IF EXISTS `artwork`; +DROP TABLE IF EXISTS `Artwork`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `artwork` ( - `id` bigint NOT NULL, - `alphachannel` tinyint(1) DEFAULT NULL, - `animated` tinyint(1) DEFAULT NULL, - `checksum` varchar(45) DEFAULT NULL, - `game` bigint DEFAULT NULL, - `height` int DEFAULT NULL, - `imageid` varchar(45) DEFAULT NULL, - `url` varchar(255) DEFAULT NULL, - `width` int DEFAULT NULL, +CREATE TABLE `Artwork` ( + `Id` bigint NOT NULL, + `AlphaChannel` tinyint(1) DEFAULT NULL, + `Animated` tinyint(1) DEFAULT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `Game` bigint DEFAULT NULL, + `Height` int DEFAULT NULL, + `ImageId` varchar(45) DEFAULT NULL, + `Url` varchar(255) DEFAULT NULL, + `Width` int DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `collection` +-- Table structure for table `Collection` -- -DROP TABLE IF EXISTS `collection`; +DROP TABLE IF EXISTS `Collection`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `collection` ( - `id` bigint NOT NULL, - `checksum` varchar(45) DEFAULT NULL, - `games` json DEFAULT NULL, - `name` varchar(255) DEFAULT NULL, - `slug` varchar(100) DEFAULT NULL, - `createdAt` datetime DEFAULT NULL, - `updatedAt` datetime DEFAULT NULL, - `url` varchar(255) DEFAULT NULL, +CREATE TABLE `Collection` ( + `Id` bigint NOT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `Games` json DEFAULT NULL, + `Name` varchar(255) DEFAULT NULL, + `Slug` varchar(100) DEFAULT NULL, + `CreatedAt` datetime DEFAULT NULL, + `UpdatedAt` datetime DEFAULT NULL, + `Url` varchar(255) DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `company` +-- Table structure for table `Company` -- -DROP TABLE IF EXISTS `company`; +DROP TABLE IF EXISTS `Company`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `company` ( - `id` bigint NOT NULL, - `changedate` datetime DEFAULT NULL, - `changedatecategory` int DEFAULT NULL, - `changedcompanyid` bigint DEFAULT NULL, - `checksum` varchar(45) DEFAULT NULL, - `country` int DEFAULT NULL, - `createdat` datetime DEFAULT NULL, - `description` longtext, - `developed` json DEFAULT NULL, - `logo` bigint DEFAULT NULL, - `name` varchar(255) DEFAULT NULL, - `parent` bigint DEFAULT NULL, - `published` json DEFAULT NULL, - `slug` varchar(100) DEFAULT NULL, - `startdate` datetime DEFAULT NULL, - `startdatecategory` int DEFAULT NULL, - `updatedat` datetime DEFAULT NULL, - `url` varchar(255) DEFAULT NULL, - `websites` json DEFAULT NULL, +CREATE TABLE `Company` ( + `Id` bigint NOT NULL, + `ChangeDate` datetime DEFAULT NULL, + `ChangeDateCategory` int DEFAULT NULL, + `ChangedCompanyId` bigint DEFAULT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `Country` int DEFAULT NULL, + `CreatedAt` datetime DEFAULT NULL, + `Description` longtext, + `Developed` json DEFAULT NULL, + `Logo` bigint DEFAULT NULL, + `Name` varchar(255) DEFAULT NULL, + `Parent` bigint DEFAULT NULL, + `Published` json DEFAULT NULL, + `Slug` varchar(100) DEFAULT NULL, + `StartDate` datetime DEFAULT NULL, + `StartDateCategory` int DEFAULT NULL, + `UpdatedAt` datetime DEFAULT NULL, + `Url` varchar(255) DEFAULT NULL, + `Websites` json DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `companylogo` +-- Table structure for table `CompanyLogo` -- -DROP TABLE IF EXISTS `companylogo`; +DROP TABLE IF EXISTS `CompanyLogo`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `companylogo` ( - `id` bigint NOT NULL, - `alphachannel` tinyint(1) DEFAULT NULL, - `animated` tinyint(1) DEFAULT NULL, - `checksum` varchar(45) DEFAULT NULL, - `height` int DEFAULT NULL, - `imageid` varchar(45) DEFAULT NULL, - `url` varchar(255) DEFAULT NULL, - `width` int DEFAULT NULL, +CREATE TABLE `CompanyLogo` ( + `Id` bigint NOT NULL, + `AlphaChannel` tinyint(1) DEFAULT NULL, + `Animated` tinyint(1) DEFAULT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `Height` int DEFAULT NULL, + `ImageId` varchar(45) DEFAULT NULL, + `Url` varchar(255) DEFAULT NULL, + `Width` int DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `cover` +-- Table structure for table `Cover` -- -DROP TABLE IF EXISTS `cover`; +DROP TABLE IF EXISTS `Cover`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `cover` ( - `id` bigint NOT NULL, - `alphachannel` tinyint(1) DEFAULT NULL, - `animated` tinyint(1) DEFAULT NULL, - `checksum` varchar(45) DEFAULT NULL, - `game` bigint DEFAULT NULL, - `height` int DEFAULT NULL, - `imageid` varchar(45) DEFAULT NULL, - `url` varchar(255) DEFAULT NULL, - `width` int DEFAULT NULL, +CREATE TABLE `Cover` ( + `Id` bigint NOT NULL, + `AlphaChannel` tinyint(1) DEFAULT NULL, + `Animated` tinyint(1) DEFAULT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `Game` bigint DEFAULT NULL, + `Height` int DEFAULT NULL, + `ImageId` varchar(45) DEFAULT NULL, + `Url` varchar(255) DEFAULT NULL, + `Width` int DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `externalgame` +-- Table structure for table `ExternalGame` -- -DROP TABLE IF EXISTS `externalgame`; +DROP TABLE IF EXISTS `ExternalGame`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `externalgame` ( - `id` bigint NOT NULL, - `category` int DEFAULT NULL, - `checksum` varchar(45) DEFAULT NULL, - `createdat` datetime DEFAULT NULL, - `countries` json DEFAULT NULL, - `game` bigint DEFAULT NULL, - `media` int DEFAULT NULL, - `name` varchar(255) DEFAULT NULL, - `platform` bigint DEFAULT NULL, - `uid` varchar(255) DEFAULT NULL, - `updatedat` datetime DEFAULT NULL, - `url` varchar(255) DEFAULT NULL, - `year` int DEFAULT NULL, +CREATE TABLE `ExternalGame` ( + `Id` bigint NOT NULL, + `Category` int DEFAULT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `CreatedAt` datetime DEFAULT NULL, + `Countries` json DEFAULT NULL, + `Game` bigint DEFAULT NULL, + `Media` int DEFAULT NULL, + `Name` varchar(255) DEFAULT NULL, + `Platform` bigint DEFAULT NULL, + `Uid` varchar(255) DEFAULT NULL, + `UpdatedAt` datetime DEFAULT NULL, + `Url` varchar(255) DEFAULT NULL, + `Year` int DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `franchise` +-- Table structure for table `Franchise` -- -DROP TABLE IF EXISTS `franchise`; +DROP TABLE IF EXISTS `Franchise`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `franchise` ( - `id` bigint NOT NULL, - `checksum` varchar(45) DEFAULT NULL, - `createdat` datetime DEFAULT NULL, - `updatedat` datetime DEFAULT NULL, - `games` json DEFAULT NULL, - `name` varchar(255) DEFAULT NULL, - `slug` varchar(255) DEFAULT NULL, - `url` varchar(255) DEFAULT NULL, +CREATE TABLE `Franchise` ( + `Id` bigint NOT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `CreatedAt` datetime DEFAULT NULL, + `UpdatedAt` datetime DEFAULT NULL, + `Games` json DEFAULT NULL, + `Name` varchar(255) DEFAULT NULL, + `Slug` varchar(255) DEFAULT NULL, + `Url` varchar(255) DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `game` +-- Table structure for table `Game` -- -DROP TABLE IF EXISTS `game`; +DROP TABLE IF EXISTS `Game`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `game` ( - `id` bigint NOT NULL, - `ageratings` json DEFAULT NULL, - `aggregatedrating` double DEFAULT NULL, - `aggregatedratingcount` int DEFAULT NULL, - `alternativenames` json DEFAULT NULL, - `artworks` json DEFAULT NULL, - `bundles` json DEFAULT NULL, - `category` int DEFAULT NULL, - `checksum` varchar(45) DEFAULT NULL, - `collection` bigint DEFAULT NULL, - `cover` bigint DEFAULT NULL, - `createdat` datetime DEFAULT NULL, - `dlcs` json DEFAULT NULL, - `expansions` json DEFAULT NULL, - `externalgames` json DEFAULT NULL, - `firstreleasedate` datetime DEFAULT NULL, - `follows` int DEFAULT NULL, - `franchise` bigint DEFAULT NULL, - `franchises` json DEFAULT NULL, - `gameengines` json DEFAULT NULL, - `gamemodes` json DEFAULT NULL, - `genres` json DEFAULT NULL, - `hypes` int DEFAULT NULL, - `involvedcompanies` json DEFAULT NULL, - `keywords` json DEFAULT NULL, - `multiplayermodes` json DEFAULT NULL, - `name` varchar(255) DEFAULT NULL, - `parentgame` bigint DEFAULT NULL, - `platforms` json DEFAULT NULL, - `playerperspectives` json DEFAULT NULL, - `rating` double DEFAULT NULL, - `ratingcount` int DEFAULT NULL, - `releasedates` json DEFAULT NULL, - `screenshots` json DEFAULT NULL, - `similargames` json DEFAULT NULL, - `slug` varchar(100) DEFAULT NULL, - `standaloneexpansions` json DEFAULT NULL, - `status` int DEFAULT NULL, - `storyline` longtext, - `summary` longtext, - `tags` json DEFAULT NULL, - `themes` json DEFAULT NULL, - `totalrating` double DEFAULT NULL, - `totalratingcount` int DEFAULT NULL, - `updatedat` datetime DEFAULT NULL, - `url` varchar(255) DEFAULT NULL, - `versionparent` bigint DEFAULT NULL, - `versiontitle` varchar(100) DEFAULT NULL, - `videos` json DEFAULT NULL, - `websites` json DEFAULT NULL, +CREATE TABLE `Game` ( + `Id` bigint NOT NULL, + `AgeRatings` json DEFAULT NULL, + `AggregatedRating` double DEFAULT NULL, + `AggregatedRatingCount` int DEFAULT NULL, + `AlternativeNames` json DEFAULT NULL, + `Artworks` json DEFAULT NULL, + `Bundles` json DEFAULT NULL, + `Category` int DEFAULT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `Collection` bigint DEFAULT NULL, + `Cover` bigint DEFAULT NULL, + `CreatedAt` datetime DEFAULT NULL, + `Dlcs` json DEFAULT NULL, + `Expansions` json DEFAULT NULL, + `ExternalGames` json DEFAULT NULL, + `FirstReleaseDate` datetime DEFAULT NULL, + `Follows` int DEFAULT NULL, + `Franchise` bigint DEFAULT NULL, + `Franchises` json DEFAULT NULL, + `GameEngines` json DEFAULT NULL, + `GameModes` json DEFAULT NULL, + `Genres` json DEFAULT NULL, + `Hypes` int DEFAULT NULL, + `InvolvedCompanies` json DEFAULT NULL, + `Keywords` json DEFAULT NULL, + `MultiplayerModes` json DEFAULT NULL, + `Name` varchar(255) DEFAULT NULL, + `ParentGame` bigint DEFAULT NULL, + `Platforms` json DEFAULT NULL, + `PlayerPerspectives` json DEFAULT NULL, + `Rating` double DEFAULT NULL, + `RatingCount` int DEFAULT NULL, + `ReleaseDates` json DEFAULT NULL, + `Screenshots` json DEFAULT NULL, + `SimilarGames` json DEFAULT NULL, + `Slug` varchar(100) DEFAULT NULL, + `StandaloneExpansions` json DEFAULT NULL, + `Status` int DEFAULT NULL, + `StoryLine` longtext, + `Summary` longtext, + `Tags` json DEFAULT NULL, + `Themes` json DEFAULT NULL, + `TotalRating` double DEFAULT NULL, + `TotalRatingCount` int DEFAULT NULL, + `UpdatedAt` datetime DEFAULT NULL, + `Url` varchar(255) DEFAULT NULL, + `VersionParent` bigint DEFAULT NULL, + `VersionTitle` varchar(100) DEFAULT NULL, + `Videos` json DEFAULT NULL, + `Websites` json DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`), - KEY `idx_genres` ((cast(`genres` as unsigned array))), - KEY `idx_alternativenames` ((cast(`alternativenames` as unsigned array))), - KEY `idx_artworks` ((cast(`artworks` as unsigned array))), - KEY `idx_bundles` ((cast(`bundles` as unsigned array))), - KEY `idx_dlcs` ((cast(`dlcs` as unsigned array))), - KEY `idx_expansions` ((cast(`expansions` as unsigned array))), - KEY `idx_externalgames` ((cast(`externalgames` as unsigned array))), - KEY `idx_franchises` ((cast(`franchises` as unsigned array))), - KEY `idx_gameengines` ((cast(`gameengines` as unsigned array))), - KEY `idx_gamemodes` ((cast(`gamemodes` as unsigned array))), - KEY `idx_involvedcompanies` ((cast(`involvedcompanies` as unsigned array))), - KEY `idx_keywords` ((cast(`keywords` as unsigned array))), - KEY `idx_multiplayermodes` ((cast(`multiplayermodes` as unsigned array))), - KEY `idx_platforms` ((cast(`platforms` as unsigned array))), - KEY `idx_playerperspectives` ((cast(`playerperspectives` as unsigned array))), - KEY `idx_releasedates` ((cast(`releasedates` as unsigned array))), - KEY `idx_screenshots` ((cast(`screenshots` as unsigned array))), - KEY `idx_similargames` ((cast(`similargames` as unsigned array))), - KEY `idx_standaloneexpansions` ((cast(`standaloneexpansions` as unsigned array))), - KEY `idx_tags` ((cast(`tags` as unsigned array))), - KEY `idx_themes` ((cast(`themes` as unsigned array))), - KEY `idx_videos` ((cast(`videos` as unsigned array))), - KEY `idx_websites` ((cast(`websites` as unsigned array))) + PRIMARY KEY (`Id`), + UNIQUE KEY `Id_UNIQUE` (`Id`), + KEY `Idx_AgeRatings` ((cast(`AgeRatings` as unsigned array))), + KEY `Idx_Genres` ((cast(`Genres` as unsigned array))), + KEY `Idx_alternativeNames` ((cast(`AlternativeNames` as unsigned array))), + KEY `Idx_artworks` ((cast(`Artworks` as unsigned array))), + KEY `Idx_bundles` ((cast(`Bundles` as unsigned array))), + KEY `Idx_dlcs` ((cast(`Dlcs` as unsigned array))), + KEY `Idx_expansions` ((cast(`Expansions` as unsigned array))), + KEY `Idx_ExternalGames` ((cast(`ExternalGames` as unsigned array))), + KEY `Idx_franchises` ((cast(`Franchises` as unsigned array))), + KEY `Idx_Gameengines` ((cast(`GameEngines` as unsigned array))), + KEY `Idx_Gamemodes` ((cast(`GameModes` as unsigned array))), + KEY `Idx_involvedcompanies` ((cast(`InvolvedCompanies` as unsigned array))), + KEY `Idx_keywords` ((cast(`Keywords` as unsigned array))), + KEY `Idx_multiplayermodes` ((cast(`MultiplayerModes` as unsigned array))), + KEY `Idx_Platforms` ((cast(`Platforms` as unsigned array))), + KEY `Idx_playerperspectives` ((cast(`PlayerPerspectives` as unsigned array))), + KEY `Idx_releasedates` ((cast(`ReleaseDates` as unsigned array))), + KEY `Idx_Screenshots` ((cast(`Screenshots` as unsigned array))), + KEY `Idx_similarGames` ((cast(`SimilarGames` as unsigned array))), + KEY `Idx_standaloneexpansions` ((cast(`StandaloneExpansions` as unsigned array))), + KEY `Idx_tags` ((cast(`Tags` as unsigned array))), + KEY `Idx_themes` ((cast(`Themes` as unsigned array))), + KEY `Idx_vIdeos` ((cast(`Videos` as unsigned array))), + KEY `Idx_websites` ((cast(`Websites` as unsigned array))) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `games_roms` +-- Table structure for table `Games_Roms` -- -DROP TABLE IF EXISTS `games_roms`; +DROP TABLE IF EXISTS `Games_Roms`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `games_roms` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `platformid` bigint DEFAULT NULL, - `gameid` bigint DEFAULT NULL, - `name` varchar(255) DEFAULT NULL, - `size` bigint DEFAULT NULL, - `crc` varchar(20) DEFAULT NULL, - `md5` varchar(100) DEFAULT NULL, - `sha1` varchar(100) DEFAULT NULL, - `developmentstatus` varchar(100) DEFAULT NULL, - `flags` json DEFAULT NULL, - `romtype` int DEFAULT NULL, - `romtypemedia` varchar(100) DEFAULT NULL, - `medialabel` varchar(100) DEFAULT NULL, - `path` longtext, - `metadatasource` int DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`), - INDEX `gameid` (`gameid` ASC) VISIBLE, - INDEX `id_gameid` (`gameid` ASC, `id` ASC) VISIBLE +CREATE TABLE `Games_Roms` ( + `Id` bigint NOT NULL AUTO_INCREMENT, + `PlatformId` bigint DEFAULT NULL, + `GameId` bigint DEFAULT NULL, + `Name` varchar(255) DEFAULT NULL, + `Size` bigint DEFAULT NULL, + `CRC` varchar(20) DEFAULT NULL, + `MD5` varchar(100) DEFAULT NULL, + `SHA1` varchar(100) DEFAULT NULL, + `DevelopmentStatus` varchar(100) DEFAULT NULL, + `Flags` json DEFAULT NULL, + `RomType` int DEFAULT NULL, + `RomTypeMedia` varchar(100) DEFAULT NULL, + `MediaLabel` varchar(100) DEFAULT NULL, + `Path` longtext, + `MetadataSource` int DEFAULT NULL, + PRIMARY KEY (`Id`), + UNIQUE KEY `Id_UNIQUE` (`Id`), + INDEX `GameId` (`GameId` ASC) VISIBLE, + INDEX `Id_GameId` (`GameId` ASC, `Id` ASC) VISIBLE ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `gamevideo` +-- Table structure for table `GameVideo` -- -DROP TABLE IF EXISTS `gamevideo`; +DROP TABLE IF EXISTS `GameVideo`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `gamevideo` ( - `id` bigint NOT NULL, - `checksum` varchar(45) DEFAULT NULL, - `game` bigint DEFAULT NULL, - `name` varchar(100) DEFAULT NULL, - `videoid` varchar(45) DEFAULT NULL, +CREATE TABLE `GameVideo` ( + `Id` bigint NOT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `Game` bigint DEFAULT NULL, + `Name` varchar(100) DEFAULT NULL, + `VideoId` varchar(45) DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `genre` +-- Table structure for table `Genre` -- -DROP TABLE IF EXISTS `genre`; +DROP TABLE IF EXISTS `Genre`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `genre` ( - `id` bigint NOT NULL, - `checksum` varchar(45) DEFAULT NULL, - `createdat` datetime DEFAULT NULL, - `updatedat` datetime DEFAULT NULL, - `name` varchar(255) DEFAULT NULL, - `slug` varchar(100) DEFAULT NULL, - `url` varchar(255) DEFAULT NULL, +CREATE TABLE `Genre` ( + `Id` bigint NOT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `CreatedAt` datetime DEFAULT NULL, + `UpdatedAt` datetime DEFAULT NULL, + `Name` varchar(255) DEFAULT NULL, + `Slug` varchar(100) DEFAULT NULL, + `Url` varchar(255) DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `involvedcompany` +-- Table structure for table `InvolvedCompany` -- -DROP TABLE IF EXISTS `involvedcompany`; +DROP TABLE IF EXISTS `InvolvedCompany`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `involvedcompany` ( - `id` bigint NOT NULL, - `checksum` varchar(45) DEFAULT NULL, - `company` bigint DEFAULT NULL, - `createdat` datetime DEFAULT NULL, - `developer` tinyint(1) DEFAULT NULL, - `game` bigint DEFAULT NULL, - `porting` tinyint(1) DEFAULT NULL, - `publisher` tinyint(1) DEFAULT NULL, - `supporting` tinyint(1) DEFAULT NULL, - `updatedat` datetime DEFAULT NULL, +CREATE TABLE `InvolvedCompany` ( + `Id` bigint NOT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `Company` bigint DEFAULT NULL, + `CreatedAt` datetime DEFAULT NULL, + `Developer` tinyint(1) DEFAULT NULL, + `Game` bigint DEFAULT NULL, + `Porting` tinyint(1) DEFAULT NULL, + `Publisher` tinyint(1) DEFAULT NULL, + `Supporting` tinyint(1) DEFAULT NULL, + `UpdatedAt` datetime DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `platform` +-- Table structure for table `Platform` -- -DROP TABLE IF EXISTS `platform`; +DROP TABLE IF EXISTS `Platform`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `platform` ( - `id` bigint NOT NULL, - `abbreviation` varchar(45) DEFAULT NULL, - `alternativename` varchar(255) DEFAULT NULL, - `category` int DEFAULT NULL, - `checksum` varchar(45) DEFAULT NULL, - `createdat` datetime DEFAULT NULL, - `generation` int DEFAULT NULL, - `name` varchar(45) DEFAULT NULL, - `platformfamily` bigint DEFAULT NULL, - `platformlogo` bigint DEFAULT NULL, - `slug` varchar(45) DEFAULT NULL, - `summary` longtext, - `updatedat` datetime DEFAULT NULL, - `url` varchar(255) DEFAULT NULL, - `versions` json DEFAULT NULL, - `websites` json DEFAULT NULL, +CREATE TABLE `Platform` ( + `Id` bigint NOT NULL, + `Abbreviation` varchar(45) DEFAULT NULL, + `AlternativeName` varchar(255) DEFAULT NULL, + `Category` int DEFAULT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `CreatedAt` datetime DEFAULT NULL, + `Generation` int DEFAULT NULL, + `Name` varchar(45) DEFAULT NULL, + `PlatformFamily` bigint DEFAULT NULL, + `PlatformLogo` bigint DEFAULT NULL, + `Slug` varchar(45) DEFAULT NULL, + `Summary` longtext, + `UpdatedAt` datetime DEFAULT NULL, + `Url` varchar(255) DEFAULT NULL, + `Versions` json DEFAULT NULL, + `Websites` json DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`) + PRIMARY KEY (`Id`), + UNIQUE KEY `Id_UNIQUE` (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `platformlogo` +-- Table structure for table `PlatformLogo` -- -DROP TABLE IF EXISTS `platformlogo`; +DROP TABLE IF EXISTS `PlatformLogo`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `platformlogo` ( - `id` bigint NOT NULL, - `alphachannel` tinyint(1) DEFAULT NULL, - `animated` tinyint(1) DEFAULT NULL, - `checksum` varchar(45) DEFAULT NULL, - `height` int DEFAULT NULL, - `imageid` varchar(45) DEFAULT NULL, - `url` varchar(255) DEFAULT NULL, - `width` int DEFAULT NULL, +CREATE TABLE `PlatformLogo` ( + `Id` bigint NOT NULL, + `AlphaChannel` tinyint(1) DEFAULT NULL, + `Animated` tinyint(1) DEFAULT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `Height` int DEFAULT NULL, + `ImageId` varchar(45) DEFAULT NULL, + `Url` varchar(255) DEFAULT NULL, + `Width` int DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `platformversion` +-- Table structure for table `Platformversion` -- -DROP TABLE IF EXISTS `platformversion`; +DROP TABLE IF EXISTS `PlatformVersion`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `platformversion` ( - `id` bigint NOT NULL, - `checksum` varchar(45) DEFAULT NULL, - `companies` json DEFAULT NULL, - `connectivity` longtext, - `cpu` longtext, - `graphics` longtext, - `mainmanufacturer` bigint DEFAULT NULL, - `media` longtext, - `memory` longtext, - `name` longtext, - `os` longtext, - `output` longtext, - `platformlogo` int DEFAULT NULL, - `platformversionreleasedates` json DEFAULT NULL, - `resolutions` longtext, - `slug` longtext, - `sound` longtext, - `storage` longtext, - `summary` longtext, - `url` varchar(255) DEFAULT NULL, +CREATE TABLE `PlatformVersion` ( + `Id` bigint NOT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `Companies` json DEFAULT NULL, + `Connectivity` longtext, + `CPU` longtext, + `Graphics` longtext, + `MainManufacturer` bigint DEFAULT NULL, + `Media` longtext, + `Memory` longtext, + `Name` longtext, + `OS` longtext, + `Output` longtext, + `PlatformLogo` int DEFAULT NULL, + `PlatformVersionReleaseDates` json DEFAULT NULL, + `Resolutions` longtext, + `Slug` longtext, + `Sound` longtext, + `Storage` longtext, + `Summary` longtext, + `Url` varchar(255) DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `screenshot` +-- Table structure for table `Screenshot` -- -DROP TABLE IF EXISTS `screenshot`; +DROP TABLE IF EXISTS `Screenshot`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `screenshot` ( - `id` bigint NOT NULL, - `alphachannel` tinyint(1) DEFAULT NULL, - `animated` tinyint(1) DEFAULT NULL, - `checksum` varchar(45) DEFAULT NULL, - `game` bigint DEFAULT NULL, - `height` int DEFAULT NULL, - `imageid` varchar(45) DEFAULT NULL, - `url` varchar(255) DEFAULT NULL, - `width` int DEFAULT NULL, +CREATE TABLE `Screenshot` ( + `Id` bigint NOT NULL, + `AlphaChannel` tinyint(1) DEFAULT NULL, + `Animated` tinyint(1) DEFAULT NULL, + `Checksum` varchar(45) DEFAULT NULL, + `Game` bigint DEFAULT NULL, + `Height` int DEFAULT NULL, + `ImageId` varchar(45) DEFAULT NULL, + `Url` varchar(255) DEFAULT NULL, + `Width` int DEFAULT NULL, `dateAdded` datetime DEFAULT NULL, `lastUpdated` datetime DEFAULT NULL, - PRIMARY KEY (`id`) + PRIMARY KEY (`Id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `settings` +-- Table structure for table `Settings` -- -DROP TABLE IF EXISTS `settings`; +DROP TABLE IF EXISTS `Settings`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `settings` ( - `setting` varchar(45) NOT NULL, - `value` longtext, - PRIMARY KEY (`setting`), - UNIQUE KEY `setting_UNIQUE` (`setting`) +CREATE TABLE `Settings` ( + `Setting` varchar(45) NOT NULL, + `Value` longtext, + PRIMARY KEY (`Setting`), + UNIQUE KEY `Setting_UNIQUE` (`Setting`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `signatures_games` +-- Table structure for table `Signatures_Games` -- -DROP TABLE IF EXISTS `signatures_games`; +DROP TABLE IF EXISTS `Signatures_Games`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `signatures_games` ( - `id` int NOT NULL AUTO_INCREMENT, - `name` varchar(255) DEFAULT NULL, - `description` varchar(255) DEFAULT NULL, - `year` varchar(15) DEFAULT NULL, - `publisherid` int DEFAULT NULL, - `demo` int DEFAULT NULL, - `systemid` int DEFAULT NULL, - `systemvariant` varchar(100) DEFAULT NULL, - `video` varchar(10) DEFAULT NULL, - `country` varchar(5) DEFAULT NULL, - `language` varchar(5) DEFAULT NULL, - `copyright` varchar(15) DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`), - KEY `publisher_idx` (`publisherid`), - KEY `system_idx` (`systemid`), - KEY `ingest_idx` (`name`,`year`,`publisherid`,`systemid`,`country`,`language`) USING BTREE, - CONSTRAINT `publisher` FOREIGN KEY (`publisherid`) REFERENCES `signatures_publishers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT `system` FOREIGN KEY (`systemid`) REFERENCES `signatures_platforms` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +CREATE TABLE `Signatures_Games` ( + `Id` int NOT NULL AUTO_INCREMENT, + `Name` varchar(255) DEFAULT NULL, + `Description` varchar(255) DEFAULT NULL, + `Year` varchar(15) DEFAULT NULL, + `PublisherId` int DEFAULT NULL, + `Demo` int DEFAULT NULL, + `SystemId` int DEFAULT NULL, + `SystemVariant` varchar(100) DEFAULT NULL, + `Video` varchar(10) DEFAULT NULL, + `Country` varchar(5) DEFAULT NULL, + `Language` varchar(5) DEFAULT NULL, + `Copyright` varchar(15) DEFAULT NULL, + PRIMARY KEY (`Id`), + UNIQUE KEY `Id_UNIQUE` (`Id`), + KEY `publisher_Idx` (`PublisherId`), + KEY `system_Idx` (`SystemId`), + KEY `ingest_Idx` (`Name`,`Year`,`PublisherId`,`SystemId`,`Country`,`Language`) USING BTREE, + CONSTRAINT `Publisher` FOREIGN KEY (`PublisherId`) REFERENCES `Signatures_Publishers` (`Id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `System` FOREIGN KEY (`SystemId`) REFERENCES `Signatures_Platforms` (`Id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `signatures_platforms` +-- Table structure for table `Signatures_Platforms` -- -DROP TABLE IF EXISTS `signatures_platforms`; +DROP TABLE IF EXISTS `Signatures_Platforms`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `signatures_platforms` ( - `id` int NOT NULL AUTO_INCREMENT, - `platform` varchar(100) DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `idsignatures_platforms_UNIQUE` (`id`), - KEY `platforms_idx` (`platform`,`id`) USING BTREE +CREATE TABLE `Signatures_Platforms` ( + `Id` int NOT NULL AUTO_INCREMENT, + `Platform` varchar(100) DEFAULT NULL, + PRIMARY KEY (`Id`), + UNIQUE KEY `IdSignatures_Platforms_UNIQUE` (`Id`), + KEY `Platforms_Idx` (`Platform`,`Id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `signatures_publishers` +-- Table structure for table `Signatures_Publishers` -- -DROP TABLE IF EXISTS `signatures_publishers`; +DROP TABLE IF EXISTS `Signatures_Publishers`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `signatures_publishers` ( - `id` int NOT NULL AUTO_INCREMENT, - `publisher` varchar(100) DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`), - KEY `publisher_idx` (`publisher`,`id`) +CREATE TABLE `Signatures_Publishers` ( + `Id` int NOT NULL AUTO_INCREMENT, + `Publisher` varchar(100) DEFAULT NULL, + PRIMARY KEY (`Id`), + UNIQUE KEY `Id_UNIQUE` (`Id`), + KEY `publisher_Idx` (`Publisher`,`Id`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `signatures_roms` +-- Table structure for table `Signatures_Roms` -- -DROP TABLE IF EXISTS `signatures_roms`; +DROP TABLE IF EXISTS `Signatures_Roms`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `signatures_roms` ( - `id` int NOT NULL AUTO_INCREMENT, - `gameid` int DEFAULT NULL, - `name` varchar(255) DEFAULT NULL, - `size` bigint DEFAULT NULL, - `crc` varchar(20) DEFAULT NULL, - `md5` varchar(100) DEFAULT NULL, - `sha1` varchar(100) DEFAULT NULL, - `developmentstatus` varchar(100) DEFAULT NULL, - `flags` json DEFAULT NULL, - `romtype` int DEFAULT NULL, - `romtypemedia` varchar(100) DEFAULT NULL, - `medialabel` varchar(100) DEFAULT NULL, - `metadatasource` int DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`,`gameid`) USING BTREE, - KEY `gameid_idx` (`gameid`), - KEY `md5_idx` (`md5`) USING BTREE, - KEY `sha1_idx` (`sha1`) USING BTREE, - KEY `flags_idx` ((cast(`flags` as char(255) array))), - CONSTRAINT `gameid` FOREIGN KEY (`gameid`) REFERENCES `signatures_games` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +CREATE TABLE `Signatures_Roms` ( + `Id` int NOT NULL AUTO_INCREMENT, + `GameId` int DEFAULT NULL, + `Name` varchar(255) DEFAULT NULL, + `Size` bigint DEFAULT NULL, + `CRC` varchar(20) DEFAULT NULL, + `MD5` varchar(100) DEFAULT NULL, + `SHA1` varchar(100) DEFAULT NULL, + `DevelopmentStatus` varchar(100) DEFAULT NULL, + `Flags` json DEFAULT NULL, + `RomType` int DEFAULT NULL, + `RomTypeMedia` varchar(100) DEFAULT NULL, + `MediaLabel` varchar(100) DEFAULT NULL, + `MetadataSource` int DEFAULT NULL, + PRIMARY KEY (`Id`), + UNIQUE KEY `Id_UNIQUE` (`Id`,`GameId`) USING BTREE, + KEY `GameId_Idx` (`GameId`), + KEY `md5_Idx` (`MD5`) USING BTREE, + KEY `sha1_Idx` (`SHA1`) USING BTREE, + KEY `flags_Idx` ((cast(`Flags` as char(255) array))), + CONSTRAINT `GameId` FOREIGN KEY (`GameId`) REFERENCES `Signatures_Games` (`Id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Table structure for table `signatures_sources` +-- Table structure for table `Signatures_Sources` -- -DROP TABLE IF EXISTS `signatures_sources`; +DROP TABLE IF EXISTS `Signatures_Sources`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `signatures_sources` ( - `id` int NOT NULL AUTO_INCREMENT, - `name` varchar(255) DEFAULT NULL, - `description` varchar(255) DEFAULT NULL, - `category` varchar(45) DEFAULT NULL, - `version` varchar(45) DEFAULT NULL, - `author` varchar(255) DEFAULT NULL, - `email` varchar(45) DEFAULT NULL, - `homepage` varchar(45) DEFAULT NULL, - `url` varchar(45) DEFAULT NULL, - `sourcetype` varchar(45) DEFAULT NULL, - `sourcemd5` varchar(45) DEFAULT NULL, - `sourcesha1` varchar(45) DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `id_UNIQUE` (`id`), - KEY `sourcemd5_idx` (`sourcemd5`,`id`) USING BTREE, - KEY `sourcesha1_idx` (`sourcesha1`,`id`) USING BTREE +CREATE TABLE `Signatures_Sources` ( + `Id` int NOT NULL AUTO_INCREMENT, + `Name` varchar(255) DEFAULT NULL, + `Description` varchar(255) DEFAULT NULL, + `Category` varchar(45) DEFAULT NULL, + `Version` varchar(45) DEFAULT NULL, + `Author` varchar(255) DEFAULT NULL, + `Email` varchar(45) DEFAULT NULL, + `Homepage` varchar(45) DEFAULT NULL, + `Url` varchar(45) DEFAULT NULL, + `SourceType` varchar(45) DEFAULT NULL, + `SourceMD5` varchar(45) DEFAULT NULL, + `SourceSHA1` varchar(45) DEFAULT NULL, + PRIMARY KEY (`Id`), + UNIQUE KEY `Id_UNIQUE` (`Id`), + KEY `sourcemd5_Idx` (`SourceMD5`,`Id`) USING BTREE, + KEY `sourcesha1_Idx` (`SourceSHA1`,`Id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -681,27 +682,27 @@ CREATE TABLE `signatures_sources` ( -- -- --- Final view structure for view `view_signatures_games` +-- Final view structure for view `view_Signatures_Games` -- -DROP VIEW IF EXISTS `view_signatures_games`; -CREATE VIEW `view_signatures_games` AS +DROP VIEW IF EXISTS `view_Signatures_Games`; +CREATE VIEW `view_Signatures_Games` AS SELECT - `signatures_games`.`id` AS `id`, - `signatures_games`.`name` AS `name`, - `signatures_games`.`description` AS `description`, - `signatures_games`.`year` AS `year`, - `signatures_games`.`publisherid` AS `publisherid`, - `signatures_publishers`.`publisher` AS `publisher`, - `signatures_games`.`demo` AS `demo`, - `signatures_games`.`systemid` AS `platformid`, - `signatures_platforms`.`platform` AS `platform`, - `signatures_games`.`systemvariant` AS `systemvariant`, - `signatures_games`.`video` AS `video`, - `signatures_games`.`country` AS `country`, - `signatures_games`.`language` AS `language`, - `signatures_games`.`copyright` AS `copyright` + `Signatures_Games`.`Id` AS `Id`, + `Signatures_Games`.`Name` AS `Name`, + `Signatures_Games`.`Description` AS `Description`, + `Signatures_Games`.`Year` AS `Year`, + `Signatures_Games`.`PublisherId` AS `PublisherId`, + `Signatures_Publishers`.`Publisher` AS `Publisher`, + `Signatures_Games`.`Demo` AS `Demo`, + `Signatures_Games`.`SystemId` AS `PlatformId`, + `Signatures_Platforms`.`Platform` AS `Platform`, + `Signatures_Games`.`SystemVariant` AS `SystemVariant`, + `Signatures_Games`.`VIdeo` AS `Video`, + `Signatures_Games`.`Country` AS `Country`, + `Signatures_Games`.`Language` AS `Language`, + `Signatures_Games`.`Copyright` AS `Copyright` FROM - ((`signatures_games` - JOIN `signatures_publishers` ON ((`signatures_games`.`publisherid` = `signatures_publishers`.`id`))) - JOIN `signatures_platforms` ON ((`signatures_games`.`systemid` = `signatures_platforms`.`id`))); \ No newline at end of file + ((`Signatures_Games` + JOIN `Signatures_Publishers` ON ((`Signatures_Games`.`PublisherId` = `Signatures_Publishers`.`Id`))) + JOIN `Signatures_Platforms` ON ((`Signatures_Games`.`SystemId` = `Signatures_Platforms`.`Id`))); \ No newline at end of file From 5eb39f5060a7f6e839adc93719e2421857b28ecf Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sun, 2 Jul 2023 01:20:55 +1000 Subject: [PATCH 48/71] fix: fixed column name typo --- gaseous-server/Controllers/SignaturesController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gaseous-server/Controllers/SignaturesController.cs b/gaseous-server/Controllers/SignaturesController.cs index 5aa2672..f59173a 100644 --- a/gaseous-server/Controllers/SignaturesController.cs +++ b/gaseous-server/Controllers/SignaturesController.cs @@ -90,7 +90,7 @@ namespace gaseous_server.Controllers Crc = (string)sigDbRow["CRC"], Md5 = (string)sigDbRow["MD5"], Sha1 = (string)sigDbRow["SHA1"], - DevelopmentStatus = (string)sigDbRow["SevelopmentStatus"], + DevelopmentStatus = (string)sigDbRow["DevelopmentStatus"], flags = Newtonsoft.Json.JsonConvert.DeserializeObject>((string)sigDbRow["Flags"]), RomType = (Models.Signatures_Games.RomItem.RomTypes)(int)sigDbRow["RomType"], RomTypeMedia = (string)sigDbRow["RomTypeMedia"], From a3dfcf7fd590f8e2cfa7d7c76bf141806256cd7b Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sun, 2 Jul 2023 19:49:28 +1000 Subject: [PATCH 49/71] fix: added support for docker environment variables --- docker-compose.yml | 2 +- gaseous-tools/Config.cs | 84 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b668bc8..0c1445d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,7 @@ services: ports: - 5198:80 volumes: - - /Users/michaelgreen/.gaseous-server:/root/.gaseous-server + - gs:/root/.gaseous-server environment: - dbhost=gsdb - dbuser=root diff --git a/gaseous-tools/Config.cs b/gaseous-tools/Config.cs index 241916d..70d2830 100644 --- a/gaseous-tools/Config.cs +++ b/gaseous-tools/Config.cs @@ -244,9 +244,53 @@ namespace gaseous_tools public class Database { - public string HostName = "localhost"; - public string UserName = "gaseous"; - public string Password = "gaseous"; + private static string _DefaultHostName { + get + { + if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("dbhost"))) + { + return Environment.GetEnvironmentVariable("dbhost"); + } + else + { + return "localhost"; + } + } + } + + private static string _DefaultUserName + { + get + { + if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("dbuser"))) + { + return Environment.GetEnvironmentVariable("dbuser"); + } + else + { + return "gaseous"; + } + } + } + + private static string _DefaultPassword + { + get + { + if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("dbpass"))) + { + return Environment.GetEnvironmentVariable("dbpass"); + } + else + { + return "gaseous"; + } + } + } + + public string HostName = _DefaultHostName; + public string UserName = _DefaultUserName; + public string Password = _DefaultPassword; public string DatabaseName = "gaseous"; public int Port = 3306; @@ -349,8 +393,38 @@ namespace gaseous_tools public class IGDB { - public string ClientId = ""; - public string Secret = ""; + private static string _DefaultIGDBClientId + { + get + { + if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("igdbclientid"))) + { + return Environment.GetEnvironmentVariable("igdbclientid"); + } + else + { + return ""; + } + } + } + + private static string _DefaultIGDBSecret + { + get + { + if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("igdbclientsecret"))) + { + return Environment.GetEnvironmentVariable("igdbclientsecret"); + } + else + { + return ""; + } + } + } + + public string ClientId = _DefaultIGDBClientId; + public string Secret = _DefaultIGDBSecret; } public class Logging From d2fceff52a49086ab4b89ff21be7b6961fae698b Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sun, 2 Jul 2023 21:58:59 +1000 Subject: [PATCH 50/71] doc: updated readme --- README.MD | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/README.MD b/README.MD index c303d5c..8b0ee87 100644 --- a/README.MD +++ b/README.MD @@ -4,6 +4,7 @@ This is the server for the Gaseous system. All your games and metadata are store ## Requirements * MySQL Server 8+ +* Internet Game Database API Key. See: https://api-docs.igdb.com/#account-creation ## Third Party Projects The following projects are used by Gaseous @@ -11,3 +12,50 @@ The following projects are used by Gaseous * https://github.com/JamesNK/Newtonsoft.Json * https://www.nuget.org/packages/MySql.Data/8.0.32.1 * https://github.com/kamranayub/igdb-dotnet + +## Configuration File +When Gaseous-Server is started for the first time, it creates a configuration file at ~/.gaseous-server/config.json if it doesn't exist. Some values can be filled in using environment variables (such as in the case of using docker). + +### DatabaseConfiguration +| Attribute | Environment Variable | +| --------- | -------------------- | +| HostName | dbhost | +| UserName | dbuser | +| Password | dbpass | + +### IGDBConfiguration +| Attribute | Environment Variable | +| --------- | -------------------- | +| ClientId | igdbclientid | +| Secret. | igdbclientsecret | + +### config.json +```json +{ + "DatabaseConfiguration": { + "HostName": "localhost", + "UserName": "gaseous", + "Password": "gaseous", + "DatabaseName": "gaseous", + "Port": 3306 + }, + "IGDBConfiguration": { + "ClientId": "", + "Secret": "" + }, + "LoggingConfiguration": { + "DebugLogging": false, + "LogFormat": "text" + } +} + +``` + +## Deploy with Docker +Dockerfile and docker-compose.yml files have been provided to make deployment of the server as easy as possible. +1. Clone the repo with "git clone https://github.com/gaseous-project/gaseous-server.git" +2. Change into the gaseous-server directory +3. Switch to the develop branch with "git checkout develop" +4. Open the docker-compose.yml file and edit the igdbclientid and igdbclientsecret to the values retrieved from your IGDB account +5. Run the command "docker-compose up -d" +6. Connect to the host on port 5198 \ No newline at end of file From 26718b3cae699fa4474dd9d04356679b6a8814ad Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Sun, 2 Jul 2023 22:57:26 +1000 Subject: [PATCH 51/71] doc: updated readme document --- README.MD | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/README.MD b/README.MD index 8b0ee87..8a1c1da 100644 --- a/README.MD +++ b/README.MD @@ -58,4 +58,41 @@ Dockerfile and docker-compose.yml files have been provided to make deployment of 3. Switch to the develop branch with "git checkout develop" 4. Open the docker-compose.yml file and edit the igdbclientid and igdbclientsecret to the values retrieved from your IGDB account 5. Run the command "docker-compose up -d" -6. Connect to the host on port 5198 \ No newline at end of file +6. Connect to the host on port 5198 + +## Adding Content +While games can be added to the server without them, it is recommended adding some signature DAT files beforehand to allow for better matching of ROM to game. + +These signature DAT files contain a list of titles with hashes for many of the ROM images that have been found by the community. + +Currently only TOSEC is supported, though more will be added. + +### Adding signature DAT files +1. Download the DAT files from the source website. For example; from https://www.tosecdev.org/downloads/category/56-2023-01-23 +2. Extract the archive +3. Copy the DAT files to ~/.gaseous-server/Data/Signatures/TOSEC/ + +### Adding game image files +1. Ensure your game image file is unzipped, and clearly named. Attempting a search for the game name on https://www.igdb.com can help with file naming. If a hash search is unsuccessful, Gaseous will fall back to attempting to search by the file name. +2. Copy the file to ~/.gaseous-server/Data/Import + +Image to game matching follows the following order of operations, stopping the process at the first match: +### Get the file signature +1. Attempt a hash search +2. Attempt to search the signature database for a rom matching the file name - sometimes the hash can not be matched as a highscore table for example was saved to the image +3. Attempt to parse the file name - clues such as the extension being used to define which platform the file belongs to are used to create a search criteria + +### Create a list of search candidates +Before beginning, remove any version numbers. +1. Add the full name of the image +2. Add the name of the image with any " - " replaced by ": " +3. Add the name of the image with text after a " - " removed +4. Add the name of the image with text after a ": " removed + +### Search IGDB for a game match +Loop through each of the search candidates searching using: +1. "where" - exact match as the search candidate +2. "wherefuzzy" - partial match using wildcards +3. "search" - uses a more flexible search method + +Note: that if more than one result is found, the image will be set as "Unknown" as there is no way for Gaseous to know which title is the correct one. \ No newline at end of file From 48e8682a55d90690c2af7092e04876f7d8ce5aac Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Tue, 4 Jul 2023 09:20:42 +1000 Subject: [PATCH 52/71] feat: added system page to manage background tasks and get basic system information --- .../Controllers/SystemController.cs | 48 +++++ gaseous-server/ProcessQueue.cs | 8 +- gaseous-server/Program.cs | 1 + gaseous-server/wwwroot/images/cog.jpg | Bin 0 -> 90353 bytes gaseous-server/wwwroot/index.html | 12 +- gaseous-server/wwwroot/pages/game.html | 20 -- gaseous-server/wwwroot/pages/system.html | 198 ++++++++++++++++++ gaseous-server/wwwroot/scripts/main.js | 24 +++ gaseous-server/wwwroot/styles/style.css | 27 +++ gaseous-tools/Common.cs | 20 +- 10 files changed, 331 insertions(+), 27 deletions(-) create mode 100644 gaseous-server/Controllers/SystemController.cs create mode 100644 gaseous-server/wwwroot/images/cog.jpg create mode 100644 gaseous-server/wwwroot/pages/system.html diff --git a/gaseous-server/Controllers/SystemController.cs b/gaseous-server/Controllers/SystemController.cs new file mode 100644 index 0000000..e4f285d --- /dev/null +++ b/gaseous-server/Controllers/SystemController.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using gaseous_tools; +using Microsoft.AspNetCore.Mvc; + +namespace gaseous_server.Controllers +{ + [ApiController] + [Route("api/v1/[controller]")] + public class SystemController : Controller + { + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public Dictionary GetSystemStatus() + { + Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + + Dictionary ReturnValue = new Dictionary(); + + // disk size + List> Disks = new List>(); + //Disks.Add(GetDisk(gaseous_tools.Config.ConfigurationPath)); + Disks.Add(GetDisk(gaseous_tools.Config.LibraryConfiguration.LibraryRootDirectory)); + ReturnValue.Add("Paths", Disks); + + // database size + string sql = "SELECT table_schema, SUM(data_length + index_length) FROM information_schema.tables WHERE table_schema = '" + Config.DatabaseConfiguration.DatabaseName + "'"; + DataTable dbResponse = db.ExecuteCMD(sql); + ReturnValue.Add("DatabaseSize", dbResponse.Rows[0][1]); + + return ReturnValue; + } + + private Dictionary GetDisk(string Path) + { + Dictionary DiskValues = new Dictionary(); + DiskValues.Add("LibraryPath", Path); + DiskValues.Add("SpaceUsed", gaseous_tools.Common.DirSize(new DirectoryInfo(Path))); + DiskValues.Add("SpaceAvailable", new DriveInfo(Path).AvailableFreeSpace); + DiskValues.Add("TotalSpace", new DriveInfo(Path).TotalSize); + + return DiskValues; + } + } +} \ No newline at end of file diff --git a/gaseous-server/ProcessQueue.cs b/gaseous-server/ProcessQueue.cs index 4057ced..8158105 100644 --- a/gaseous-server/ProcessQueue.cs +++ b/gaseous-server/ProcessQueue.cs @@ -74,6 +74,11 @@ namespace gaseous_server Logging.Log(Logging.LogType.Information, "Timered Event", "Starting Metadata Refresher"); Classes.MetadataManagement.RefreshMetadata(true); break; + + case QueueItemType.OrganiseLibrary: + Logging.Log(Logging.LogType.Information, "Timered Event", "Starting Library Organiser"); + Classes.ImportGame.OrganiseLibrary(); + break; } } catch (Exception ex) @@ -101,7 +106,8 @@ namespace gaseous_server NotConfigured, SignatureIngestor, TitleIngestor, - MetadataRefresh + MetadataRefresh, + OrganiseLibrary } public enum QueueItemState diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index 28ed3cd..51612db 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -94,6 +94,7 @@ gaseous_server.Classes.Metadata.Platforms.GetPlatform(0); ProcessQueue.QueueItems.Add(new ProcessQueue.QueueItem(ProcessQueue.QueueItemType.SignatureIngestor, 60)); ProcessQueue.QueueItems.Add(new ProcessQueue.QueueItem(ProcessQueue.QueueItemType.TitleIngestor, 1)); ProcessQueue.QueueItems.Add(new ProcessQueue.QueueItem(ProcessQueue.QueueItemType.MetadataRefresh, 360)); +ProcessQueue.QueueItems.Add(new ProcessQueue.QueueItem(ProcessQueue.QueueItemType.OrganiseLibrary, 1440)); // start the app app.Run(); diff --git a/gaseous-server/wwwroot/images/cog.jpg b/gaseous-server/wwwroot/images/cog.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4b0b01e30191a0c1825555ffd25f027109127e7a GIT binary patch literal 90353 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 + @@ -14,12 +15,15 @@ Gaseous Games - - diff --git a/gaseous-server/wwwroot/styles/style.css b/gaseous-server/wwwroot/styles/style.css index 9915c52..449efc9 100644 --- a/gaseous-server/wwwroot/styles/style.css +++ b/gaseous-server/wwwroot/styles/style.css @@ -49,6 +49,14 @@ h3 { width: 700px; /* Could be more or less, depending on screen size */ min-height: 358px; } +.modal-content-sub { + background-color: #383838; + margin: 20% auto; /* 20% from the top and centered */ + padding: 10px; + border: 1px solid #888; + width: 300px; /* Could be more or less, depending on screen size */ + min-height: 110px; +} #modal-heading { margin-block: 5px; border-bottom-style: solid; From 35e5efd565b422b848d91bd2a9d946b93bf3b3a3 Mon Sep 17 00:00:00 2001 From: Michael Green <84688932+michael-j-green@users.noreply.github.com> Date: Tue, 11 Jul 2023 00:49:15 +1000 Subject: [PATCH 71/71] doc: updated readme --- README.MD | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.MD b/README.MD index 8a1c1da..5b4b4e3 100644 --- a/README.MD +++ b/README.MD @@ -55,10 +55,9 @@ When Gaseous-Server is started for the first time, it creates a configuration fi Dockerfile and docker-compose.yml files have been provided to make deployment of the server as easy as possible. 1. Clone the repo with "git clone https://github.com/gaseous-project/gaseous-server.git" 2. Change into the gaseous-server directory -3. Switch to the develop branch with "git checkout develop" -4. Open the docker-compose.yml file and edit the igdbclientid and igdbclientsecret to the values retrieved from your IGDB account -5. Run the command "docker-compose up -d" -6. Connect to the host on port 5198 +3. Open the docker-compose.yml file and edit the igdbclientid and igdbclientsecret to the values retrieved from your IGDB account +4. Run the command "docker-compose up -d" +5. Connect to the host on port 5198 ## Adding Content While games can be added to the server without them, it is recommended adding some signature DAT files beforehand to allow for better matching of ROM to game.