Add m3u support for multi image games (#171)

* Parse media type info

* Completed M3U support
This commit is contained in:
Michael Green
2023-10-26 13:39:34 +11:00
committed by GitHub
parent a4e5835e34
commit c2c2831cda
13 changed files with 963 additions and 73 deletions

View File

@@ -266,7 +266,7 @@ namespace gaseous_server.Classes
gameItem.InclusionStatus.PlatformId = alwaysIncludeItem.PlatformId; gameItem.InclusionStatus.PlatformId = alwaysIncludeItem.PlatformId;
gameItem.InclusionStatus.GameId = alwaysIncludeItem.GameId; gameItem.InclusionStatus.GameId = alwaysIncludeItem.GameId;
gameItem.InclusionStatus.InclusionState = alwaysIncludeItem.InclusionState; gameItem.InclusionStatus.InclusionState = alwaysIncludeItem.InclusionState;
gameItem.Roms = Roms.GetRoms((long)gameItem.Id, (long)platform.Id); gameItem.Roms = Roms.GetRoms((long)gameItem.Id, (long)platform.Id).GameRomItems;
collectionPlatformItem.Games.Add(gameItem); collectionPlatformItem.Games.Add(gameItem);
} }
@@ -286,7 +286,7 @@ namespace gaseous_server.Classes
{ {
CollectionContents.CollectionPlatformItem.CollectionGameItem collectionGameItem = new CollectionContents.CollectionPlatformItem.CollectionGameItem(game); CollectionContents.CollectionPlatformItem.CollectionGameItem collectionGameItem = new CollectionContents.CollectionPlatformItem.CollectionGameItem(game);
List<Roms.GameRomItem> gameRoms = Roms.GetRoms((long)game.Id, (long)platform.Id); List<Roms.GameRomItem> gameRoms = Roms.GetRoms((long)game.Id, (long)platform.Id).GameRomItems;
bool AddGame = false; bool AddGame = false;

View File

@@ -15,6 +15,12 @@ namespace gaseous_server.Classes.Metadata
} }
public class InvalidGameId : Exception
{
public InvalidGameId(long Id) : base("Unable to find Game by id " + Id)
{}
}
private static IGDBClient igdb = new IGDBClient( private static IGDBClient igdb = new IGDBClient(
// Found in Twitch Developer portal for your app // Found in Twitch Developer portal for your app
Config.IGDB.ClientId, Config.IGDB.ClientId,

View File

@@ -0,0 +1,415 @@
using System;
using System.Data;
using gaseous_tools;
using gaseous_signature_parser.models.RomSignatureObject;
using Microsoft.VisualBasic;
using IGDB.Models;
using gaseous_server.Classes.Metadata;
using System.IO.Compression;
namespace gaseous_server.Classes
{
public class RomMediaGroup
{
public class InvalidMediaGroupId : Exception
{
public InvalidMediaGroupId(long Id) : base("Unable to find media group by id " + Id)
{}
}
public static GameRomMediaGroupItem CreateMediaGroup(long GameId, long PlatformId, List<long> RomIds)
{
Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = "INSERT INTO RomMediaGroup (Status, PlatformId, GameId) VALUES (@status, @platformid, @gameid); SELECT CAST(LAST_INSERT_ID() AS SIGNED);";
Dictionary<string, object> dbDict = new Dictionary<string, object>();
dbDict.Add("status", GameRomMediaGroupItem.GroupBuildStatus.WaitingForBuild);
dbDict.Add("gameid", GameId);
dbDict.Add("platformid", PlatformId);
DataTable mgInsert = db.ExecuteCMD(sql, dbDict);
long mgId = (long)mgInsert.Rows[0][0];
foreach (long RomId in RomIds)
{
try
{
Roms.GameRomItem gameRomItem = Roms.GetRom(RomId);
if (gameRomItem.PlatformId == PlatformId)
{
sql = "INSERT INTO RomMediaGroup_Members (GroupId, RomId) VALUES (@groupid, @romid);";
dbDict.Clear();
dbDict.Add("groupid", mgId);
dbDict.Add("romid", RomId);
db.ExecuteCMD(sql, dbDict);
}
else
{
Logging.Log(Logging.LogType.Warning, "Media Group", "Unable to add ROM id " + RomId + " to group. ROM platform is different from group platform.");
}
}
catch (Roms.InvalidRomId irid)
{
Logging.Log(Logging.LogType.Warning, "Media Group", "Unable to add ROM id " + RomId + " to group. ROM doesn't exist", irid);
}
}
StartMediaGroupBuild(mgId);
return GetMediaGroup(mgId);
}
public static GameRomMediaGroupItem GetMediaGroup(long Id)
{
Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = "SELECT * FROM RomMediaGroup WHERE Id=@id;";
Dictionary<string, object> dbDict = new Dictionary<string, object>();
dbDict.Add("id", Id);
DataTable dataTable = db.ExecuteCMD(sql, dbDict);
if (dataTable.Rows.Count == 0)
{
throw new InvalidMediaGroupId(Id);
}
else
{
GameRomMediaGroupItem mediaGroupItem = BuildMediaGroupFromRow(dataTable.Rows[0]);
return mediaGroupItem;
}
}
public static List<GameRomMediaGroupItem> GetMediaGroupsFromGameId(long GameId)
{
Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = "SELECT * FROM RomMediaGroup WHERE GameId=@gameid;";
Dictionary<string, object> dbDict = new Dictionary<string, object>();
dbDict.Add("gameid", GameId);
DataTable dataTable = db.ExecuteCMD(sql, dbDict);
List<GameRomMediaGroupItem> mediaGroupItems = new List<GameRomMediaGroupItem>();
foreach (DataRow row in dataTable.Rows)
{
mediaGroupItems.Add(BuildMediaGroupFromRow(row));
}
mediaGroupItems.Sort((x, y) => x.PlatformName.CompareTo(y.PlatformName));
return mediaGroupItems;
}
public static GameRomMediaGroupItem EditMediaGroup(long Id, List<long> RomIds)
{
GameRomMediaGroupItem mg = GetMediaGroup(Id);
Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = "";
Dictionary<string, object> dbDict = new Dictionary<string, object>();
// delete roms from group
sql = "DELETE FROM RomMediaGroup_Members WHERE GroupId=@groupid;";
dbDict.Clear();
dbDict.Add("groupid", Id);
db.ExecuteCMD(sql, dbDict);
// add roms to group
foreach (long RomId in RomIds)
{
try
{
Roms.GameRomItem gameRomItem = Roms.GetRom(RomId);
if (gameRomItem.PlatformId == mg.PlatformId)
{
sql = "INSERT INTO RomMediaGroup_Members (GroupId, RomId) VALUES (@groupid, @romid);";
dbDict.Clear();
dbDict.Add("groupid", Id);
dbDict.Add("romid", RomId);
db.ExecuteCMD(sql, dbDict);
}
else
{
Logging.Log(Logging.LogType.Warning, "Media Group", "Unable to add ROM id " + RomId + " to group. ROM platform is different from group platform.");
}
}
catch (Roms.InvalidRomId irid)
{
Logging.Log(Logging.LogType.Warning, "Media Group", "Unable to add ROM id " + RomId + " to group. ROM doesn't exist", irid);
}
}
// set group to rebuild
sql = "UPDATE RomMediaGroup SET Status=1 WHERE GroupId=@groupid;";
dbDict.Clear();
dbDict.Add("groupid", Id);
db.ExecuteCMD(sql, dbDict);
string MediaGroupZipPath = Path.Combine(Config.LibraryConfiguration.LibraryMediaGroupDirectory, Id + ".zip");
if (File.Exists(MediaGroupZipPath))
{
File.Delete(MediaGroupZipPath);
}
StartMediaGroupBuild(Id);
// return to caller
return GetMediaGroup(Id);
}
public static void DeleteMediaGroup(long Id)
{
Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = "DELETE FROM RomMediaGroup WHERE Id=@id;";
Dictionary<string, object> dbDict = new Dictionary<string, object>();
dbDict.Add("id", Id);
db.ExecuteCMD(sql, dbDict);
string MediaGroupZipPath = Path.Combine(Config.LibraryConfiguration.LibraryMediaGroupDirectory, Id + ".zip");
if (File.Exists(MediaGroupZipPath))
{
File.Delete(MediaGroupZipPath);
}
}
internal static GameRomMediaGroupItem BuildMediaGroupFromRow(DataRow row)
{
GameRomMediaGroupItem mediaGroupItem = new GameRomMediaGroupItem();
mediaGroupItem.Id = (long)row["Id"];
mediaGroupItem.Status = (GameRomMediaGroupItem.GroupBuildStatus)row["Status"];
mediaGroupItem.PlatformId = (long)row["PlatformId"];
mediaGroupItem.GameId = (long)row["GameId"];
mediaGroupItem.RomIds = new List<long>();
// get members
Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = "SELECT * FROM RomMediaGroup_Members WHERE GroupId=@id;";
Dictionary<string, object> dbDict = new Dictionary<string, object>();
dbDict.Add("id", mediaGroupItem.Id);
DataTable data = db.ExecuteCMD(sql, dbDict);
foreach (DataRow dataRow in data.Rows)
{
mediaGroupItem.RomIds.Add((long)dataRow["RomId"]);
}
return mediaGroupItem;
}
public static void StartMediaGroupBuild(long Id)
{
GameRomMediaGroupItem mediaGroupItem = GetMediaGroup(Id);
if (mediaGroupItem.Status != GameRomMediaGroupItem.GroupBuildStatus.Building)
{
// set collection item to waitingforbuild
Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = "UPDATE RomMediaGroup SET Status=@bs WHERE Id=@id";
Dictionary<string, object> dbDict = new Dictionary<string, object>();
dbDict.Add("id", Id);
dbDict.Add("bs", GameRomMediaGroupItem.GroupBuildStatus.WaitingForBuild);
db.ExecuteCMD(sql, dbDict);
// start background task
ProcessQueue.QueueItem queueItem = new ProcessQueue.QueueItem(ProcessQueue.QueueItemType.MediaGroupCompiler, 1, false, true);
queueItem.Options = Id;
queueItem.ForceExecute();
ProcessQueue.QueueItems.Add(queueItem);
}
}
public static void CompileMediaGroup(long Id)
{
Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
GameRomMediaGroupItem mediaGroupItem = GetMediaGroup(Id);
if (mediaGroupItem.Status == GameRomMediaGroupItem.GroupBuildStatus.WaitingForBuild)
{
Game GameObject = Games.GetGame(mediaGroupItem.GameId, false, false, false);
Platform PlatformObject = Platforms.GetPlatform(mediaGroupItem.PlatformId, false);
Logging.Log(Logging.LogType.Information, "Media Group", "Beginning build of media group: " + GameObject.Name + " for platform " + PlatformObject.Name);
// set starting
string sql = "UPDATE RomMediaGroup SET Status=@bs WHERE Id=@id";
Dictionary<string, object> dbDict = new Dictionary<string, object>();
dbDict.Add("id", mediaGroupItem.Id);
dbDict.Add("bs", GameRomMediaGroupItem.GroupBuildStatus.Building);
db.ExecuteCMD(sql, dbDict);
string ZipFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMediaGroupDirectory, mediaGroupItem.Id + ".zip");
string ZipFileTempPath = Path.Combine(Config.LibraryConfiguration.LibraryTempDirectory, mediaGroupItem.Id.ToString());
try
{
// clean up if needed
if (File.Exists(ZipFilePath))
{
Logging.Log(Logging.LogType.Warning, "Media Group", "Deleting existing build of media group: " + GameObject.Name + " for platform " + PlatformObject.Name);
File.Delete(ZipFilePath);
}
if (Directory.Exists(ZipFileTempPath))
{
Directory.Delete(ZipFileTempPath, true);
}
// gather media group files
Directory.CreateDirectory(ZipFileTempPath);
List<Roms.GameRomItem> romItems = new List<Roms.GameRomItem>();
List<string> M3UFileContents = new List<string>();
foreach (long RomId in mediaGroupItem.RomIds)
{
Roms.GameRomItem rom = Roms.GetRom(RomId);
if (File.Exists(rom.Path))
{
Logging.Log(Logging.LogType.Information, "Media Group", "Copying ROM: " + rom.Name);
File.Copy(rom.Path, Path.Combine(ZipFileTempPath, Path.GetFileName(rom.Path)));
romItems.Add(rom);
}
}
// build m3u
romItems.Sort((a, b) =>
{
var firstCompare = a.MediaDetail.Number.ToString().CompareTo(b.MediaDetail.Number.ToString());
return firstCompare != 0 ? firstCompare : a.MediaDetail.Side.CompareTo(b.MediaDetail.Side);
}
);
foreach (Roms.GameRomItem romItem in romItems)
{
string M3UFileContent = "";
M3UFileContent += romItem.Name;
if (romItem.MediaLabel != null)
{
if (romItem.MediaLabel.Length > 0)
{
M3UFileContent += "|" + romItem.MediaLabel;
}
}
M3UFileContents.Add(M3UFileContent);
}
File.WriteAllText(Path.Combine(ZipFileTempPath, GameObject.Name + ".m3u"), String.Join(Environment.NewLine, M3UFileContents));
// compress to zip
Logging.Log(Logging.LogType.Information, "Media Group", "Compressing media group");
if (!Directory.Exists(Config.LibraryConfiguration.LibraryMediaGroupDirectory))
{
Directory.CreateDirectory(Config.LibraryConfiguration.LibraryMediaGroupDirectory);
}
ZipFile.CreateFromDirectory(ZipFileTempPath, ZipFilePath, CompressionLevel.SmallestSize, false);
// clean up
if (Directory.Exists(ZipFileTempPath))
{
Logging.Log(Logging.LogType.Information, "Media Group", "Cleaning up");
Directory.Delete(ZipFileTempPath, true);
}
// set completed
dbDict["bs"] = GameRomMediaGroupItem.GroupBuildStatus.Completed;
db.ExecuteCMD(sql, dbDict);
}
catch (Exception ex)
{
// clean up
if (Directory.Exists(ZipFileTempPath))
{
Directory.Delete(ZipFileTempPath, true);
}
if (File.Exists(ZipFilePath))
{
File.Delete(ZipFilePath);
}
// set failed
dbDict["bs"] = GameRomMediaGroupItem.GroupBuildStatus.Failed;
db.ExecuteCMD(sql, dbDict);
Logging.Log(Logging.LogType.Critical, "Media Group", "Media Group building has failed", ex);
}
}
}
public class GameRomMediaGroupItem
{
public long Id { get; set; }
public long GameId { get; set; }
public long PlatformId { get; set; }
public string PlatformName {
get
{
try
{
return Platforms.GetPlatform(PlatformId, false).Name;
}
catch
{
return "Unknown";
}
}
}
public List<long> RomIds { get; set; }
private GroupBuildStatus _Status { get; set; }
public GroupBuildStatus Status {
get
{
if (_Status == GroupBuildStatus.Completed)
{
if (File.Exists(MediaGroupZipPath))
{
return GroupBuildStatus.Completed;
}
else
{
return GroupBuildStatus.NoStatus;
}
}
else
{
return _Status;
}
}
set
{
_Status = value;
}
}
public long? Size {
get
{
if (Status == GroupBuildStatus.Completed)
{
if (File.Exists(MediaGroupZipPath))
{
FileInfo fi = new FileInfo(MediaGroupZipPath);
return fi.Length;
}
else
{
return 0;
}
}
else
{
return 0;
}
}
}
internal string MediaGroupZipPath
{
get
{
return Path.Combine(Config.LibraryConfiguration.LibraryMediaGroupDirectory, Id + ".zip");
}
}
public enum GroupBuildStatus
{
NoStatus = 0,
WaitingForBuild = 1,
Building = 2,
Completed = 3,
Failed = 4
}
}
}
}

View File

@@ -1,13 +1,24 @@
using System; using System;
using System.Data; using System.Data;
using gaseous_tools; using gaseous_tools;
using gaseous_signature_parser.models.RomSignatureObject;
using static gaseous_server.Classes.RomMediaGroup;
using gaseous_server.Classes.Metadata;
namespace gaseous_server.Classes namespace gaseous_server.Classes
{ {
public class Roms public class Roms
{ {
public static List<GameRomItem> GetRoms(long GameId, long PlatformId = -1) public class InvalidRomId : Exception
{
public InvalidRomId(long Id) : base("Unable to find ROM by id " + Id)
{}
}
public static GameRomObject GetRoms(long GameId, long PlatformId = -1)
{ {
GameRomObject GameRoms = new GameRomObject();
Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); Database db = new gaseous_tools.Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = ""; string sql = "";
Dictionary<string, object> dbDict = new Dictionary<string, object>(); Dictionary<string, object> dbDict = new Dictionary<string, object>();
@@ -23,17 +34,19 @@ namespace gaseous_server.Classes
if (romDT.Rows.Count > 0) if (romDT.Rows.Count > 0)
{ {
List<GameRomItem> romItems = new List<GameRomItem>();
foreach (DataRow romDR in romDT.Rows) foreach (DataRow romDR in romDT.Rows)
{ {
romItems.Add(BuildRom(romDR)); GameRoms.GameRomItems.Add(BuildRom(romDR));
} }
return romItems; // get rom media groups
GameRoms.MediaGroups = Classes.RomMediaGroup.GetMediaGroupsFromGameId(GameId);
return GameRoms;
} }
else else
{ {
throw new Exception("Unknown Game Id"); throw new Games.InvalidGameId(GameId);
} }
} }
@@ -53,7 +66,7 @@ namespace gaseous_server.Classes
} }
else else
{ {
throw new Exception("Unknown ROM Id"); throw new InvalidRomId(RomId);
} }
} }
@@ -135,6 +148,12 @@ namespace gaseous_server.Classes
return romItem; return romItem;
} }
public class GameRomObject
{
public List<GameRomMediaGroupItem> MediaGroups { get; set; } = new List<GameRomMediaGroupItem>();
public List<GameRomItem> GameRomItems { get; set; } = new List<GameRomItem>();
}
public class GameRomItem public class GameRomItem
{ {
public long Id { get; set; } public long Id { get; set; }
@@ -153,12 +172,112 @@ namespace gaseous_server.Classes
public List<KeyValuePair<string, object>>? Attributes { get; set;} public List<KeyValuePair<string, object>>? Attributes { get; set;}
public int RomType { get; set; } public int RomType { get; set; }
public string? RomTypeMedia { get; set; } public string? RomTypeMedia { get; set; }
public MediaType? MediaDetail {
get
{
if (RomTypeMedia != null)
{
return new MediaType(Source, RomTypeMedia);
}
else
{
return null;
}
}
}
public string? MediaLabel { get; set; } public string? MediaLabel { get; set; }
public string? Path { get; set; } public string? Path { get; set; }
public gaseous_signature_parser.models.RomSignatureObject.RomSignatureObject.Game.Rom.SignatureSourceType Source { get; set; } public RomSignatureObject.Game.Rom.SignatureSourceType Source { get; set; }
public string? SignatureSourceGameTitle { get; set;} public string? SignatureSourceGameTitle { get; set;}
public GameLibrary.LibraryItem Library { get; set; } public GameLibrary.LibraryItem Library { get; set; }
} }
public class MediaType
{
public MediaType(RomSignatureObject.Game.Rom.SignatureSourceType Source, string MediaTypeString)
{
switch (Source)
{
case RomSignatureObject.Game.Rom.SignatureSourceType.TOSEC:
string[] typeString = MediaTypeString.Split(" ");
string inType = "";
foreach (string typeStringVal in typeString)
{
if (inType == "")
{
switch (typeStringVal.ToLower())
{
case "disk":
Media = RomSignatureObject.Game.Rom.RomTypes.Disk;
inType = typeStringVal;
break;
case "disc":
Media = RomSignatureObject.Game.Rom.RomTypes.Disc;
inType = typeStringVal;
break;
case "file":
Media = RomSignatureObject.Game.Rom.RomTypes.File;
inType = typeStringVal;
break;
case "part":
Media = RomSignatureObject.Game.Rom.RomTypes.Part;
inType = typeStringVal;
break;
case "tape":
Media = RomSignatureObject.Game.Rom.RomTypes.Tape;
inType = typeStringVal;
break;
case "of":
inType = typeStringVal;
break;
case "side":
inType = typeStringVal;
break;
}
}
else {
switch (inType.ToLower())
{
case "disk":
case "disc":
case "file":
case "part":
case "tape":
Number = int.Parse(typeStringVal);
break;
case "of":
Count = int.Parse(typeStringVal);
break;
case "side":
Side = typeStringVal;
break;
}
inType = "";
}
}
break;
default:
break;
}
}
public RomSignatureObject.Game.Rom.RomTypes? Media { get; set; }
public int? Number { get; set; }
public int? Count { get; set; }
public string? Side { get; set; }
}
} }
} }

View File

@@ -739,7 +739,7 @@ namespace gaseous_server.Controllers
[HttpGet] [HttpGet]
[Route("{GameId}/roms")] [Route("{GameId}/roms")]
[ProducesResponseType(typeof(List<Classes.Roms.GameRomItem>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(Classes.Roms.GameRomObject), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
//[ResponseCache(CacheProfileName = "5Minute")] //[ResponseCache(CacheProfileName = "5Minute")]
public ActionResult GameRom(long GameId) public ActionResult GameRom(long GameId)
@@ -748,9 +748,7 @@ namespace gaseous_server.Controllers
{ {
Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false); Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false);
List<Classes.Roms.GameRomItem> roms = Classes.Roms.GetRoms(GameId); return Ok(Classes.Roms.GetRoms(GameId));
return Ok(roms);
} }
catch catch
{ {
@@ -909,6 +907,158 @@ namespace gaseous_server.Controllers
} }
} }
[HttpGet]
[Route("{GameId}/romgroup/{RomGroupId}")]
[ProducesResponseType(typeof(Classes.RomMediaGroup.GameRomMediaGroupItem), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
//[ResponseCache(CacheProfileName = "5Minute")]
public ActionResult GameRomGroup(long GameId, long RomGroupId)
{
try
{
Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false);
Classes.RomMediaGroup.GameRomMediaGroupItem rom = Classes.RomMediaGroup.GetMediaGroup(RomGroupId);
if (rom.GameId == GameId)
{
return Ok(rom);
}
else
{
return NotFound();
}
}
catch
{
return NotFound();
}
}
[HttpPost]
[Route("{GameId}/romgroup")]
[ProducesResponseType(typeof(Classes.RomMediaGroup.GameRomMediaGroupItem), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult NewGameRomGroup(long GameId, long PlatformId, [FromBody] List<long> RomIds)
{
try
{
Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false);
try
{
Classes.RomMediaGroup.GameRomMediaGroupItem rom = Classes.RomMediaGroup.CreateMediaGroup(GameId, PlatformId, RomIds);
return Ok(rom);
}
catch
{
return NotFound();
}
}
catch
{
return NotFound();
}
}
[HttpPatch]
[Route("{GameId}/romgroup/{RomId}")]
[ProducesResponseType(typeof(Classes.RomMediaGroup.GameRomMediaGroupItem), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult GameRomGroupMembers(long GameId, long RomGroupId, [FromBody] List<long> RomIds)
{
try
{
Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false);
Classes.RomMediaGroup.GameRomMediaGroupItem rom = Classes.RomMediaGroup.GetMediaGroup(RomGroupId);
if (rom.GameId == GameId)
{
rom = Classes.RomMediaGroup.EditMediaGroup(RomGroupId, RomIds);
return Ok(rom);
}
else
{
return NotFound();
}
}
catch
{
return NotFound();
}
}
[HttpDelete]
[Route("{GameId}/romgroup/{RomGroupId}")]
[ProducesResponseType(typeof(Classes.RomMediaGroup.GameRomMediaGroupItem), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult GameRomGroupDelete(long GameId, long RomGroupId)
{
try
{
Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false);
Classes.RomMediaGroup.GameRomMediaGroupItem rom = Classes.RomMediaGroup.GetMediaGroup(RomGroupId);
if (rom.GameId == GameId)
{
Classes.RomMediaGroup.DeleteMediaGroup(RomGroupId);
return Ok(rom);
}
else
{
return NotFound();
}
}
catch
{
return NotFound();
}
}
[HttpGet]
[HttpHead]
[Route("{GameId}/romgroup/{RomGroupId}/file")]
[Route("{GameId}/romgroup/{RomGroupId}/{filename}")]
[ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult GameRomGroupFile(long GameId, long RomGroupId, string filename = "")
{
try
{
IGDB.Models.Game gameObject = Classes.Metadata.Games.GetGame(GameId, false, false, false);
Classes.RomMediaGroup.GameRomMediaGroupItem rom = Classes.RomMediaGroup.GetMediaGroup(RomGroupId);
if (rom.GameId != GameId)
{
return NotFound();
}
string romFilePath = Path.Combine(Config.LibraryConfiguration.LibraryMediaGroupDirectory, RomGroupId + ".zip");
if (System.IO.File.Exists(romFilePath))
{
FileStream content = new FileStream(romFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
string returnFileName = "";
if (filename == "")
{
returnFileName = gameObject.Name + ".zip";
}
else
{
returnFileName = filename;
}
FileStreamResult response = File(content, "application/octet-stream", returnFileName);
return response;
}
else
{
return NotFound();
}
}
catch
{
return NotFound();
}
}
[HttpGet] [HttpGet]
[Route("search")] [Route("search")]
[ProducesResponseType(typeof(List<Game>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(List<Game>), StatusCodes.Status200OK)]

View File

@@ -4,6 +4,7 @@ using System.Data;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using gaseous_signature_parser.models.RomSignatureObject;
using gaseous_tools; using gaseous_tools;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -92,7 +93,7 @@ namespace gaseous_server.Controllers
Sha1 = ((string)sigDbRow["SHA1"]).ToLower(), Sha1 = ((string)sigDbRow["SHA1"]).ToLower(),
DevelopmentStatus = (string)sigDbRow["DevelopmentStatus"], DevelopmentStatus = (string)sigDbRow["DevelopmentStatus"],
Attributes = Newtonsoft.Json.JsonConvert.DeserializeObject<List<KeyValuePair<string, object>>>((string)Common.ReturnValueIfNull(sigDbRow["Attributes"], "[]")), Attributes = Newtonsoft.Json.JsonConvert.DeserializeObject<List<KeyValuePair<string, object>>>((string)Common.ReturnValueIfNull(sigDbRow["Attributes"], "[]")),
RomType = (Models.Signatures_Games.RomItem.RomTypes)(int)sigDbRow["RomType"], RomType = (RomSignatureObject.Game.Rom.RomTypes)(int)sigDbRow["RomType"],
RomTypeMedia = (string)sigDbRow["RomTypeMedia"], RomTypeMedia = (string)sigDbRow["RomTypeMedia"],
MediaLabel = (string)sigDbRow["MediaLabel"], MediaLabel = (string)sigDbRow["MediaLabel"],
SignatureSource = (gaseous_signature_parser.models.RomSignatureObject.RomSignatureObject.Game.Rom.SignatureSourceType)(Int32)sigDbRow["MetadataSource"] SignatureSource = (gaseous_signature_parser.models.RomSignatureObject.RomSignatureObject.Game.Rom.SignatureSourceType)(Int32)sigDbRow["MetadataSource"]

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using gaseous_signature_parser.models.RomSignatureObject;
namespace gaseous_server.Models namespace gaseous_server.Models
{ {
@@ -128,49 +129,11 @@ namespace gaseous_server.Models
public List<KeyValuePair<string, object>> Attributes { get; set; } = new List<KeyValuePair<string, object>>(); public List<KeyValuePair<string, object>> Attributes { get; set; } = new List<KeyValuePair<string, object>>();
public RomTypes RomType { get; set; } public RomSignatureObject.Game.Rom.RomTypes RomType { get; set; }
public string? RomTypeMedia { get; set; } public string? RomTypeMedia { get; set; }
public string? MediaLabel { get; set; } public string? MediaLabel { get; set; }
public gaseous_signature_parser.models.RomSignatureObject.RomSignatureObject.Game.Rom.SignatureSourceType SignatureSource { get; set; } public RomSignatureObject.Game.Rom.SignatureSourceType SignatureSource { get; set; }
public enum RomTypes
{
/// <summary>
/// Media type is unknown
/// </summary>
Unknown = 0,
/// <summary>
/// Optical media
/// </summary>
Disc = 1,
/// <summary>
/// Magnetic media
/// </summary>
Disk = 2,
/// <summary>
/// Individual files
/// </summary>
File = 3,
/// <summary>
/// Individual pars
/// </summary>
Part = 4,
/// <summary>
/// Tape base media
/// </summary>
Tape = 5,
/// <summary>
/// Side of the media
/// </summary>
Side = 6
}
[JsonIgnore] [JsonIgnore]
public int Score public int Score

View File

@@ -159,6 +159,11 @@ namespace gaseous_server
Classes.Collections.CompileCollections((long)Options); Classes.Collections.CompileCollections((long)Options);
break; break;
case QueueItemType.MediaGroupCompiler:
Logging.Log(Logging.LogType.Debug, "Timered Event", "Starting Media Group Compiler");
Classes.RomMediaGroup.CompileMediaGroup((long)Options);
break;
case QueueItemType.BackgroundDatabaseUpgrade: case QueueItemType.BackgroundDatabaseUpgrade:
Logging.Log(Logging.LogType.Debug, "Timered Event", "Starting Background Upgrade"); Logging.Log(Logging.LogType.Debug, "Timered Event", "Starting Background Upgrade");
gaseous_tools.DatabaseMigration.UpgradeScriptBackgroundTasks(); gaseous_tools.DatabaseMigration.UpgradeScriptBackgroundTasks();
@@ -245,6 +250,11 @@ namespace gaseous_server
/// </summary> /// </summary>
CollectionCompiler, CollectionCompiler,
/// <summary>
/// Builds media groups - set the options attribute to the id of the media group to build
/// </summary>
MediaGroupCompiler,
/// <summary> /// <summary>
/// Performs and post database upgrade scripts that can be processed as a background task /// Performs and post database upgrade scripts that can be processed as a background task
/// </summary> /// </summary>

View File

@@ -0,0 +1,27 @@
<p>Are you sure you want to delete this media group?</p>
<p><strong>Warning:</strong> This cannot be undone!</p>
<div style="width: 100%; text-align: center;">
<div style="display: inline-block; margin-right: 20px;">
<button class="redbutton" value="Delete" onclick="deleteCollection();">Delete</button>
</div>
<div style="display: inline-block; margin-left: 20px;">
<button value="Cancel" onclick="closeSubDialog();">Cancel</button>
</div>
</div>
<script type="text/javascript">
function deleteCollection() {
ajaxCall(
'/api/v1/Games/' + gameData.id + '/romgroup/' + subModalVariables,
'DELETE',
function (result) {
loadRoms();
closeSubDialog();
},
function (error) {
loadRoms();
closeSubDialog();
}
);
}
</script>

View File

@@ -66,6 +66,9 @@
<p id="gamesummarytext_label_button_contract" class="text_link" style="display: none;" onclick="document.querySelector('#gamesummarytext_label').classList.add('line-clamp-4'); document.querySelector('#gamesummarytext_label_button_expand').setAttribute('style', ''); document.querySelector('#gamesummarytext_label_button_contract').setAttribute('style', 'display: none;');">Read less...</p> <p id="gamesummarytext_label_button_contract" class="text_link" style="display: none;" onclick="document.querySelector('#gamesummarytext_label').classList.add('line-clamp-4'); document.querySelector('#gamesummarytext_label_button_expand').setAttribute('style', ''); document.querySelector('#gamesummarytext_label_button_contract').setAttribute('style', 'display: none;');">Read less...</p>
</div> </div>
<div id="gamesummarymediagroups">
<h3>Media Groups</h3>
</div>
<div id="gamesummaryroms"> <div id="gamesummaryroms">
<span id="rom_edit" class="romlink" onclick="DisplayROMCheckboxes(true);">Edit</span> <span id="rom_edit" class="romlink" onclick="DisplayROMCheckboxes(true);">Edit</span>
<h3>ROM's/Images</h3> <h3>ROM's/Images</h3>
@@ -76,6 +79,7 @@
<select id="rom_edit_fixplatform" style="width: 150px;"></select> <select id="rom_edit_fixplatform" style="width: 150px;"></select>
<select id="rom_edit_fixgame" style="width: 300px;"></select> <select id="rom_edit_fixgame" style="width: 300px;"></select>
<button id="rom_edit_update" onclick="remapTitles();">Update</button> <button id="rom_edit_update" onclick="remapTitles();">Update</button>
<button id="rom_edit_creategroup" onclick="createMgGroup();" disabled="disabled">Create Media Group</button>
</div> </div>
</div> </div>
</div> </div>
@@ -350,46 +354,56 @@
existingTable.remove(); existingTable.remove();
} }
var existingMgTable = document.getElementById('mediagrouptable');
if (existingMgTable) {
existingMgTable.remove();
}
var gameRoms = document.getElementById('gamesummaryroms'); var gameRoms = document.getElementById('gamesummaryroms');
ajaxCall('/api/v1/Games/' + gameId + '/roms', 'GET', function (result) { ajaxCall('/api/v1/Games/' + gameId + '/roms', 'GET', function (result) {
if (result) { if (result.gameRomItems) {
result.sort((a, b) => a.platform.name.charCodeAt(0) - b.platform.name.charCodeAt(0)); var gameRomItems = result.gameRomItems;
var mediaGroups = result.mediaGroups;
gameRomItems.sort((a, b) => a.platform.name.charCodeAt(0) - b.platform.name.charCodeAt(0));
var newTable = document.createElement('table'); var newTable = document.createElement('table');
newTable.id = 'romtable'; newTable.id = 'romtable';
newTable.className = 'romtable'; newTable.className = 'romtable';
newTable.setAttribute('cellspacing', 0); newTable.setAttribute('cellspacing', 0);
newTable.appendChild(createTableRow(true, [['<input id="rom_mastercheck" type="checkbox" onclick="selectAllChecks();"/>', 'rom_checkbox_box_hidden', 'rom_edit_checkbox'], 'Name', 'Size', 'Media', '', '', ''])); newTable.appendChild(createTableRow(true, [['<input id="rom_mastercheck" type="checkbox" onclick="selectAllChecks(); handleChecks();"/>', 'rom_checkbox_box_hidden', 'rom_edit_checkbox'], 'Name', 'Size', 'Media', '', '', '']));
var lastPlatform = ''; var lastPlatform = '';
for (var i = 0; i < result.length; i++) { for (var i = 0; i < gameRomItems.length; i++) {
if (result[i].platform.name != lastPlatform) { if (gameRomItems[i].platform.name != lastPlatform) {
lastPlatform = result[i].platform.name; lastPlatform = gameRomItems[i].platform.name;
var platformRow = document.createElement('tr'); var platformRow = document.createElement('tr');
var platformHeader = document.createElement('th'); var platformHeader = document.createElement('th');
platformHeader.setAttribute('colspan', 6); platformHeader.setAttribute('colspan', 6);
platformHeader.innerHTML = '<a href="#" onclick="ShowPlatformMappingDialog(' + result[i].platform.id + ');" style="float: right; text-decoration: none;" class="romlink"><img src="/images/map.svg" class="banner_button_image banner_button_image_smaller" alt="Edit platform mapping" title="Edit platform mapping" /></a><a href="#" onclick="ShowCollectionDialog(' + result[i].platform.id + ');" style="float: right; text-decoration: none;" class="romlink"><img src="/images/collections.svg" class="banner_button_image banner_button_image_smaller" alt="Add to collection" title="Add to collection" /></a>' + result[i].platform.name; platformHeader.innerHTML = '<a href="#" onclick="ShowPlatformMappingDialog(' + gameRomItems[i].platform.id + ');" style="float: right; text-decoration: none;" class="romlink"><img src="/images/map.svg" class="banner_button_image banner_button_image_smaller" alt="Edit platform mapping" title="Edit platform mapping" /></a><a href="#" onclick="ShowCollectionDialog(' + gameRomItems[i].platform.id + ');" style="float: right; text-decoration: none;" class="romlink"><img src="/images/collections.svg" class="banner_button_image banner_button_image_smaller" alt="Add to collection" title="Add to collection" /></a>' + gameRomItems[i].platform.name;
platformRow.appendChild(platformHeader); platformRow.appendChild(platformHeader);
newTable.appendChild(platformRow); newTable.appendChild(platformRow);
} }
var launchButton = ''; var launchButton = '';
if (result[i].emulator) { if (result.gameRomItems[i].emulator) {
if (result[i].emulator.type) { if (gameRomItems[i].emulator.type) {
if (result[i].emulator.type.length > 0) { if (gameRomItems[i].emulator.type.length > 0) {
launchButton = '<a href="/index.html?page=emulator&engine=' + result[i].emulator.type + '&core=' + result[i].emulator.core + '&platformid=' + result[i].platform.id + '&gameid=' + gameId + '&rompath=' + encodeURIComponent('/api/v1/Games/' + gameId + '/roms/' + result[i].id + '/' + encodeURIComponent(result[i].name)) + '" class="romstart">Launch</a>'; launchButton = '<a href="/index.html?page=emulator&engine=' + gameRomItems[i].emulator.type + '&core=' + gameRomItems[i].emulator.core + '&platformid=' + gameRomItems[i].platform.id + '&gameid=' + gameId + '&rompath=' + encodeURIComponent('/api/v1/Games/' + gameId + '/roms/' + gameRomItems[i].id + '/' + encodeURIComponent(gameRomItems[i].name)) + '" class="romstart">Launch</a>';
} }
} }
} }
var newRow = [ var newRow = [
['<input type="checkbox" name="rom_checkbox" data-romid="' + result[i].id + '" />', 'rom_checkbox_box_hidden', 'rom_edit_checkbox'], ['<input type="checkbox" name="rom_checkbox" data-gameid="' + gameData.id + '" data-platformid="' + gameRomItems[i].platformId + '" data-romid="' + gameRomItems[i].id + '" onclick="handleChecks();" />', 'rom_checkbox_box_hidden', 'rom_edit_checkbox'],
'<a href="/api/v1/Games/' + gameId + '/roms/' + result[i].id + '/' + encodeURIComponent(result[i].name) + '" class="romlink">' + result[i].name + '</a>', '<a href="/api/v1/Games/' + gameId + '/roms/' + gameRomItems[i].id + '/' + encodeURIComponent(gameRomItems[i].name) + '" class="romlink">' + gameRomItems[i].name + '</a>',
formatBytes(result[i].size, 2), formatBytes(gameRomItems[i].size, 2),
result[i].romTypeMedia, gameRomItems[i].romTypeMedia,
result[i].mediaLabel, gameRomItems[i].mediaLabel,
launchButton, launchButton,
'<div class="properties_button" onclick="showDialog(\'rominfo\', ' + result[i].id + ');">i</div>' '<div class="properties_button" onclick="showDialog(\'rominfo\', ' + gameRomItems[i].id + ');">i</div>'
]; ];
newTable.appendChild(createTableRow(false, newRow, 'romrow', 'romcell')); newTable.appendChild(createTableRow(false, newRow, 'romrow', 'romcell'));
} }
@@ -399,6 +413,96 @@
if (displayCheckboxes == true) { if (displayCheckboxes == true) {
DisplayROMCheckboxes(true); DisplayROMCheckboxes(true);
} }
var mediaGroupDiv = document.getElementById('gamesummarymediagroups');
if (mediaGroups.length == 0) {
mediaGroupDiv.style.display = 'none';
} else {
mediaGroupDiv.style.display = '';
var mgTable = document.createElement('table');
mgTable.id = 'mediagrouptable';
mgTable.className = 'romtable';
mgTable.setAttribute('cellspacing', 0);
mgTable.appendChild(createTableRow(true, ['Platform', 'Images', 'Size', '', '', '']));
lastPlatform = '';
for (var i = 0; i < mediaGroups.length; i++) {
var mediaGroup = mediaGroups[i];
// get rom details including emulator and friendly platform name
var launchButton = '';
for (var r = 0; r < gameRomItems.length; r++) {
var gameRomItem = gameRomItems[r];
if (gameRomItem.platformId == mediaGroup.platformId) {
if (gameRomItem.emulator) {
if (gameRomItem.emulator.type.length > 0) {
launchButton = '<a href="/index.html?page=emulator&engine=' + gameRomItem.emulator.type + '&core=' + gameRomItem.emulator.core + '&platformid=' + gameRomItem.platform.id + '&gameid=' + gameId + '&rompath=' + encodeURIComponent('/api/v1/Games/' + gameId + '/romgroup/' + mediaGroup.id + '/' + gameData.name + '(' + mediaGroup.id + ')' + '.zip') + '" class="romstart">Launch</a>';
break;
}
}
}
}
var statusText = mediaGroup.status;
var downloadLink = '';
var packageSize = '-';
switch (mediaGroup.status) {
case 'NoStatus':
statusText = '-';
break;
case "WaitingForBuild":
statusText = 'Build pending';
break;
case "Building":
statusText = 'Building';
break;
case "Completed":
statusText = 'Available';
downloadLink = '<a href="/api/v1/Games/' + gameId + '/romgroup/' + mediaGroup.id + '/' + gameData.name + '.zip" class="romlink"><img src="/images/download.svg" class="banner_button_image" alt="Download" title="Download" /></a>';
packageSize = formatBytes(mediaGroup.size);
break;
case "Failed":
statusText = 'Build error';
break;
default:
statusText = result[i].buildStatus;
break;
}
var deleteButton = '<a href="#" onclick="showSubDialog(\'mediagroupdelete\', ' + mediaGroup.id + ');" class="romlink"><img src="/images/delete.svg" class="banner_button_image" alt="Delete" title="Delete" /></a>';
var newRow = [
mediaGroup.platformName,
mediaGroup.romIds.length,
packageSize,
statusText,
launchButton,
'<div style="text-align: right;">' + downloadLink + deleteButton + '</div>'
]
mgTable.appendChild(createTableRow(false, newRow, 'romrow', 'romcell'));
var mgRomRow = document.createElement('tr');
var mgRomCell = document.createElement('td');
mgRomCell.setAttribute('colspan', 6);
mgRomCell.className = 'romGroupTitles';
// iterate the group members
var groupMemberNames = [];
for (var r = 0; r < mediaGroup.romIds.length; r++) {
for (var x = 0; x < gameRomItems.length; x++) {
if (mediaGroup.romIds[r] == gameRomItems[x].id) {
groupMemberNames.push(gameRomItems[x].name);
}
}
}
mgRomCell.innerHTML = groupMemberNames.join("<br />");
mgRomRow.appendChild(mgRomCell);
mgTable.appendChild(mgRomRow);
}
mediaGroupDiv.appendChild(mgTable);
}
} else { } else {
gameRoms.setAttribute('style', 'display: none;'); gameRoms.setAttribute('style', 'display: none;');
} }
@@ -518,6 +622,48 @@
} }
} }
function handleChecks() {
var masterCheck = document.getElementById('rom_mastercheck');
var checkboxes = document.getElementsByName('rom_checkbox');
var firstPlatformId = undefined;
var includesDifferentPlatforms = false;
var checkCount = 0;
for (var i = 0; i < checkboxes.length; i++) {
if (checkboxes[i].checked == true) {
checkCount += 1;
if (firstPlatformId == undefined) {
// set our comparison platform
firstPlatformId = checkboxes[i].getAttribute('data-platformid');
} else if (firstPlatformId != checkboxes[i].getAttribute('data-platformid')) {
includesDifferentPlatforms = true;
}
}
}
if (checkCount == checkboxes.length) {
masterCheck.checked = true;
} else {
masterCheck.checked = false;
}
if (firstPlatformId == undefined) {
includesDifferentPlatforms = true;
}
if (checkCount < 2) {
includesDifferentPlatforms = true;
}
var creategroupButton = document.getElementById('rom_edit_creategroup');
if (includesDifferentPlatforms == false) {
creategroupButton.removeAttribute('disabled');
} else {
creategroupButton.setAttribute('disabled', 'disabled');
}
}
$('#rom_edit_fixplatform').select2({ $('#rom_edit_fixplatform').select2({
minimumInputLength: 3, minimumInputLength: 3,
placeholder: "Platform", placeholder: "Platform",
@@ -675,4 +821,31 @@
modalVariables = platformId; modalVariables = platformId;
showSubDialog("collectionaddgame"); showSubDialog("collectionaddgame");
} }
function createMgGroup() {
var checkboxes = document.getElementsByName('rom_checkbox');
var platformId = undefined;
var romIds = [];
for (var i = 0; i < checkboxes.length; i++) {
if (checkboxes[i].checked == true) {
if (platformId == undefined) {
platformId = checkboxes[i].getAttribute('data-platformid');
}
romIds.push(checkboxes[i].getAttribute('data-romid'));
}
}
ajaxCall(
'/api/v1/Games/' + gameId + '/romgroup?PlatformId=' + platformId,
'POST',
function (result) {
loadRoms(false);
},
function (error) {
loadRoms(false);
},
JSON.stringify(romIds)
);
}
</script> </script>

View File

@@ -160,6 +160,7 @@ h3 {
height: 40px; height: 40px;
right: 0px; right: 0px;
align-items: center; align-items: center;
z-index: 50;
} }
#banner_header_label { #banner_header_label {
@@ -201,7 +202,7 @@ h3 {
.filter_panel_box { .filter_panel_box {
position: relative; position: relative;
padding: 10px; padding: 10px;
z-index: -1; z-index: 1;
} }
input[type='text'], input[type='number'] { input[type='text'], input[type='number'] {
@@ -721,6 +722,7 @@ button:hover {
button:disabled { button:disabled {
background-color: #555; background-color: #555;
color: #888;
cursor: not-allowed; cursor: not-allowed;
} }
@@ -957,3 +959,7 @@ button:disabled {
border-color: darkslategray; border-color: darkslategray;
border-style: solid; border-style: solid;
} }
.romGroupTitles {
padding-left: 20px;
}

View File

@@ -410,6 +410,14 @@ namespace gaseous_tools
} }
} }
public string LibraryMediaGroupDirectory
{
get
{
return Path.Combine(LibraryRootDirectory, "Media Groups");
}
}
public string LibraryMetadataDirectory_Platform(Platform platform) public string LibraryMetadataDirectory_Platform(Platform platform)
{ {
string MetadataPath = Path.Combine(LibraryMetadataDirectory, "Platforms", platform.Slug); string MetadataPath = Path.Combine(LibraryMetadataDirectory, "Platforms", platform.Slug);

View File

@@ -40,3 +40,15 @@ CREATE TABLE `Relation_Game_Themes` (
ALTER TABLE `Games_Roms` ALTER TABLE `Games_Roms`
ADD COLUMN `LastMatchAttemptDate` DATETIME NULL AFTER `LibraryId`; ADD COLUMN `LastMatchAttemptDate` DATETIME NULL AFTER `LibraryId`;
CREATE TABLE `RomMediaGroup` (
`Id` BIGINT NOT NULL AUTO_INCREMENT,
`Status` INT NULL,
`PlatformId` BIGINT NULL,
`GameId` BIGINT NULL,
PRIMARY KEY (`Id`));
CREATE TABLE `RomMediaGroup_Members` (
`GroupId` BIGINT NOT NULL,
`RomId` BIGINT NOT NULL,
PRIMARY KEY (`GroupId`, `RomId`));