diff --git a/Downloads/VersionSelector.xaml b/Downloads/VersionSelector.xaml index 7b1651f..1a1f557 100644 --- a/Downloads/VersionSelector.xaml +++ b/Downloads/VersionSelector.xaml @@ -11,10 +11,10 @@ - + - + diff --git a/Downloads/VersionSelector.xaml.cs b/Downloads/VersionSelector.xaml.cs index 955f4fc..d99d290 100644 --- a/Downloads/VersionSelector.xaml.cs +++ b/Downloads/VersionSelector.xaml.cs @@ -10,13 +10,13 @@ namespace RomM.VersionSelector public partial class RomMVersionSelector : PluginUserControl { - public ObservableCollection Siblings { get; set; } + public ObservableCollection RomVersions { get; set; } public bool Cancelled { get; set; } = true; - public RomMVersionSelector(List siblings) + public RomMVersionSelector(List romVersions) { - Siblings = new ObservableCollection(siblings); + RomVersions = new ObservableCollection(romVersions); InitializeComponent(); } diff --git a/Games/RomMGameInfo.cs b/Games/RomMGameInfo.cs index cf82e2f..9de9beb 100644 --- a/Games/RomMGameInfo.cs +++ b/Games/RomMGameInfo.cs @@ -7,7 +7,7 @@ using System.IO; using System.Linq; using ProtoBuf; -using Playnite.SDK; +using RomM.Models.RomM.Rom; namespace RomM.Games { @@ -68,7 +68,7 @@ private static T FromGameIdString(string gameId) where T : RomMGameInfo } } - public InstallController GetInstallController(Game game, RomM romm, bool HasSiblings, int SelectedSibling) => new RomMInstallController(game, romm, HasSiblings, SelectedSibling); + public InstallController GetInstallController(Game game, RomM romm, GameInstallInfo GameData) => new RomMInstallController(game, romm, GameData); public UninstallController GetUninstallController(Game game, RomM romm) => new RomMUninstallController(game, romm); diff --git a/Games/RomMImport.cs b/Games/RomMImport.cs new file mode 100644 index 0000000..7670172 --- /dev/null +++ b/Games/RomMImport.cs @@ -0,0 +1,475 @@ +using Newtonsoft.Json; +using Playnite.SDK; +using Playnite.SDK.Models; +using Playnite.SDK.Plugins; +using RomM.Models.RomM.Rom; +using RomM.Settings; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace RomM.Games +{ + internal class RomMImport + { + private readonly RomM _plugin; + LibraryImportGamesArgs _args; + EmulatorMapping _mapping; + List _ROMs; + Dictionary _completionStatusMap; + List _favourites; + + public RomMImport(RomM plugin, LibraryImportGamesArgs args, EmulatorMapping mapping, List roms, List favourites) + { + _plugin = plugin; + _args = args; + _mapping = mapping; + _ROMs = roms; + _completionStatusMap = plugin.Playnite.Database.CompletionStatuses.ToDictionary(cs => cs.Name, cs => cs.Id); + _favourites = favourites; + } + + private RomMFile DetermineFile(RomMRom ROM) + { + if(ROM.Files.Count == 0) + return null; + + if(ROM.Files.Count > 1) + { + List fullpaths = new List(); + foreach (var file in ROM.Files) + { + fullpaths.Add(file.FullPath); + } + + fullpaths = fullpaths.OrderBy(x => x.Count(c => c == '/')).ToList(); + return ROM.Files.Where(x => x.FullPath == fullpaths[0]).FirstOrDefault(); + } + + return ROM.Files.FirstOrDefault(); + } + + // Main library import functions + public List ProcessData() + { + var games = new List(); + List ImportedGamesIDs = new List(); + _plugin.PlayniteApi.Database.Platforms.Add(_mapping.RomMPlatform.Name); + + foreach (var ROM in _ROMs) + { + if (_args.CancelToken.IsCancellationRequested) + break; + + // Some newer platforms don't get a hash value so we will compromise with this + if (string.IsNullOrEmpty(ROM.SHA1)) + { + var tohash = $"{ROM.Name}{ROM.FileSizeBytes}"; + + using (SHA1Managed sha1 = new SHA1Managed()) + { + var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(tohash)); + var sb = new StringBuilder(hash.Length * 2); + + foreach (byte b in hash) + { + sb.Append(b.ToString("x2")); + } + + ROM.SHA1 = sb.ToString(); + } + } + + // Skip game import if the ROM is apart of the exclusion list + if (_plugin.Playnite.Database.ImportExclusions[Playnite.ImportExclusionItem.GetId($"{ROM.Id}:{ROM.SHA1}", _plugin.Id)] != null) + { + _plugin.Logger.Warn($"[Importer] Excluding {ROM.Name} from import."); + continue; + } + + // Skip if ROM has no filename + if (string.IsNullOrEmpty(ROM.FileName)) + { + _plugin.Playnite.Notifications.Add(new NotificationMessage(_plugin.Id.ToString(), $"Filename for ROM ID: {ROM.Id} doesn't exist!\nDoes ROM exist on the servers filesystem?", NotificationType.Error)); + continue; + } + + // Fail-safe incase none of these are set to true + if (!ROM.HasSimpleSingleFile & !ROM.HasNestedSingleFile & !ROM.HasMultipleFiles) + ROM.HasMultipleFiles = true; + + // Migrate old RomMGameInfo id to new romMId:SHA1 id + string gameID = $"{ROM.Id}:{ROM.SHA1}"; + UpdatedOldGameID(ROM); + + // Merging revisions + if (_plugin.Settings.MergeRevisions && ROM.Siblings?.Count > 0) + { + if (CheckForMainSibling(ROM) == MainSibling.Other) + { + var siblinggame = _plugin.Playnite.Database.Games.FirstOrDefault(x => x.GameId == gameID); + if(siblinggame != null) + { + _plugin.Playnite.Database.Games.Remove(siblinggame); + } + continue; + } + + if (ROM.Processed) + { + var siblinggame = _plugin.Playnite.Database.Games.FirstOrDefault(x => x.GameId == gameID); + if (siblinggame != null) + { + _plugin.Playnite.Database.Games.Remove(siblinggame); + } + continue; + } + + } + + // Save Game ROM data to file + SaveGameData(ROM); + + // Skip full import if ROM has already been imported + Guid statusID = new Guid(); + var game = _plugin.Playnite.Database.Games.FirstOrDefault(g => g.GameId == gameID); + if (game != null) + { + // Sync user data + if(_plugin.Settings.KeepRomMSynced) + { + statusID = DetermineCompletionStatus(ROM); + + game.Favorite = _favourites.Exists(f => f == ROM.Id); + + if (statusID != Guid.Empty) + { + game.CompletionStatusId = statusID; + } + _plugin.Playnite.Database.Games.Update(game); + } + + ImportedGamesIDs.Add(gameID); + continue; + } + + // If keep deleted games is enabled and a deleted game gets re-added back to the server under a new romMId, Update playnite entry + if(_plugin.Settings.KeepDeletedGames) + { + if(UpdatedDeletedGame(ROM)) + { + ImportedGamesIDs.Add(gameID); + continue; + } + } + + var importedGame = ImportGame(ROM, statusID); + if (importedGame != null) + { + games.Add(importedGame); + ImportedGamesIDs.Add(gameID); + } + else + { + _plugin.Logger.Error($"[Importer] Failed to import RomM GameID: {ROM.Id}"); + continue; + } + } + _plugin.Logger.Info($"[Importer] Finished adding new games for {_mapping.RomMPlatform.Name}"); + + if (!_plugin.Settings.KeepDeletedGames) + { + RemoveMissingGames(ImportedGamesIDs); + } + + return games; + } + private Game ImportGame(RomMRom ROM, Guid StatusID) + { + var rootInstallDir = _plugin.Playnite.Paths.IsPortable + ? _mapping.DestinationPathResolved.Replace(_plugin.Playnite.Paths.ApplicationPath, ExpandableVariables.PlayniteDirectory) + : _mapping.DestinationPathResolved; + var gameInstallDir = Path.Combine(rootInstallDir, Path.GetFileNameWithoutExtension(ROM.Name)); + var pathToGame = Path.Combine(gameInstallDir, ROM.Name); + + var gameNameWithTags = + $"{ROM.Name}" + + $"{(ROM.Regions.Count > 0 ? $" ({string.Join(", ", ROM.Regions)})" : "")}" + + $"{(!string.IsNullOrEmpty(ROM.Revision) ? $" (Rev {ROM.Revision})" : "")}" + + $"{(ROM.Tags.Count > 0 ? $" ({string.Join(", ", ROM.Tags)})" : "")}"; + + var preferedRatingsBoard = _plugin.Playnite.ApplicationSettings.AgeRatingOrgPriority; + var agerating = ROM.Metadatum.Age_Ratings.Count > 0 ? new HashSet(ROM.Metadatum.Age_Ratings.Where(r => r.Split(':')[0] == preferedRatingsBoard.ToString()).Select(r => new MetadataNameProperty(r.ToString()))) : null; + + var status = _plugin.Playnite.Database.CompletionStatuses.Get(StatusID); + var completionStatusProperty = status != null ? new MetadataNameProperty(status.Name) : null; + + List gameLinks = new List(); + if(ROM.SSId != null) + gameLinks.Add(new Link("Screenscraper", $"https://www.screenscraper.fr/gameinfos.php?gameid={ROM.SSId}")); + if (ROM.HasheousId != null) + gameLinks.Add(new Link("Hasheous", $"https://hasheous.org/index.html?page=dataobjectdetail&type=game&id={ROM.HasheousId}")); + if (ROM.RAId != null) + gameLinks.Add(new Link("RetroAchievements", $"https://retroachievements.org/game/{ROM.RAId}")); + if (ROM.HLTBId != null) + gameLinks.Add(new Link("HowLongToBeat", $"https://howlongtobeat.com/game/{ROM.HLTBId}")); + + var metadata = new GameMetadata + { + Source = _plugin.Source, + GameId = $"{ROM.Id}:{ROM.SHA1}", + + Name = ROM.Name, + Description = ROM.Summary, + + Platforms = new HashSet { new MetadataNameProperty(_mapping.RomMPlatform.Name ?? "") }, + Regions = new HashSet(ROM.Regions.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + Genres = new HashSet(ROM.Metadatum.Genres.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + AgeRatings = agerating, + Series = new HashSet(ROM.Metadatum.Franchises.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + Features = new HashSet(ROM.Metadatum.Gamemodes.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + Categories = new HashSet(ROM.Metadatum.Collections.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + + ReleaseDate = ROM.Metadatum.Release_Date.HasValue ? new ReleaseDate(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(ROM.Metadatum.Release_Date.Value).ToLocalTime()) : new ReleaseDate(), + CommunityScore = (int?)ROM.Metadatum.Average_Rating, + + CoverImage = !string.IsNullOrEmpty(ROM.PathCoverL) ? new MetadataFile($"{_plugin.Settings.RomMHost}{ROM.PathCoverL}") : null, + + Favorite = _favourites.Exists(f => f == ROM.Id), + LastActivity = ROM.RomUser.LastPlayed, + UserScore = ROM.RomUser.Rating * 10, //RomM-Rating is 1-10, Playnite 1-100, so it can unfortunately only by synced one direction without loosing decimals + CompletionStatus = completionStatusProperty, + Links = gameLinks, + Roms = new List { new GameRom(gameNameWithTags, pathToGame) }, + InstallDirectory = gameInstallDir, + IsInstalled = File.Exists(pathToGame), + InstallSize = ROM.FileSizeBytes, + GameActions = new List + { + new GameAction + { + Name = $"Play in {_mapping.Emulator.Name}", + Type = GameActionType.Emulator, + EmulatorId = _mapping.EmulatorId, + EmulatorProfileId = _mapping.EmulatorProfileId, + IsPlayAction = true, + }, + new GameAction + { + Type = GameActionType.URL, + Name = "View in RomM", + Path = _plugin.CombineUrl(_plugin.Settings.RomMHost, $"rom/{ROM.Id}"), + IsPlayAction = false + } + } + }; + + // Import new game + Game game = _plugin.Playnite.Database.ImportGame(metadata, _plugin); + + if (ROM.HasManual) + { + game.Manual = $"{_plugin.Settings.RomMHost}/assets/romm/resources/{ROM.ManualPath}"; + } + + return game; + } + private void RemoveMissingGames(List ImportedGames) + { + var gamesInDatabase = _plugin.Playnite.Database.Games.Where(g => + g.Source != null && g.Source.Name == _plugin.Source.ToString() && + g.Platforms != null && g.Platforms.Any(p => p.Name == _mapping.RomMPlatform.Name) + ); + + _plugin.Logger.Info($"[Importer] Starting to remove not found games for {_mapping.RomMPlatform.Name}."); + + foreach (var game in gamesInDatabase) + { + if (_args.CancelToken.IsCancellationRequested) + break; + + if (ImportedGames.Contains(game.GameId)) + { + continue; + } + + _plugin.Playnite.Database.Games.Remove(game.Id); + _plugin.Logger.Info($"[Importer] Removing {game.Name} - {game.Id} for {_mapping.RomMPlatform.Name}"); + } + + _plugin.Logger.Info($"[Importer] Finished removing not found games for {_mapping.RomMPlatform.Name}"); + } + private bool UpdatedOldGameID(RomMRom ROM) + { + var filename = ROM.HasMultipleFiles ? Path.GetFileName(ROM.FileName) : Path.GetFileName(ROM.Files.Where(f => f.FullPath.Count(c => c == '/') <= 3).FirstOrDefault().FileName); + if (string.IsNullOrWhiteSpace(filename)) + { + _plugin.Logger.Warn($"[Importer] Rom {ROM.Id} returned empty/invalid filename, skipping updating game id."); + return false; + } + var info = new RomMGameInfo + { + MappingId = _mapping.MappingId, + FileName = filename, + DownloadUrl = _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{ROM.Id}/content/{filename}"), + HasMultipleFiles = ROM.HasMultipleFiles + }; + + // Check to see if a game already exists with + var oldgame = _plugin.Playnite.Database.Games.FirstOrDefault(g => g.GameId == info.AsGameId()); + if (oldgame != null) + { + oldgame.GameId = $"{ROM.Id}:{ROM.SHA1}"; + oldgame.PlatformIds = new List { _plugin.Playnite.Database.Platforms.First(x => x.Name == _mapping.RomMPlatform.Name).Id }; + _plugin.Playnite.Database.Games.Update(oldgame); + return true; + } + else + { + return false; + } + } + private bool UpdatedDeletedGame(RomMRom ROM) + { + // Check to see if a game already exists with an old romMId + var oldgame = _plugin.Playnite.Database.Games.FirstOrDefault(g => g.PluginId == _plugin.Id && g.GameId.Split(':')[1] == ROM.SHA1); + if (oldgame != null) + { + oldgame.GameId = $"{ROM.Id}:{ROM.SHA1}"; + _plugin.Playnite.Database.Games.Update(oldgame); + return true; + } + else + { + return false; + } + } + + private MainSibling CheckForMainSibling(RomMRom ROM) + { + //Check to see if ROM is the main sibling + if (ROM.RomUser.IsMainSibling) + return MainSibling.Current; + + //Find if there is a main sibling + foreach (var sibling in ROM.Siblings) + { + var siblingROM = _ROMs.Find(x => x.Id == sibling.Id); + + if (siblingROM.RomUser.IsMainSibling) + { + return MainSibling.Other; + } + } + + return MainSibling.None; + } + private void SaveGameData(RomMRom ROM) + { + string[] versionBreakdown = _plugin.Settings.ServerVersion.Split('.'); + float versionParsed = float.Parse(versionBreakdown[0]) + (float.Parse(versionBreakdown[1]) / 100); + + RomMRomLocal toSave = new RomMRomLocal(); + toSave.Name = ROM.Name; + toSave.SHA1 = ROM.SHA1; + toSave.MappingID = _mapping.MappingId; + toSave.ROMVersions = new List(); + + RomMRevision baseROM = new RomMRevision(); + + // Save base ROM data + baseROM.Id = ROM.Id; + baseROM.HasMultipleFiles = ROM.HasMultipleFiles; + if(!ROM.HasMultipleFiles) + { + var romfile = DetermineFile(ROM); + if (romfile == null) + { + _plugin.Logger.Error("[Importer] Unable to save ROM data as there is no rom file!"); + return; + } + + baseROM.FileName = romfile.FileName; + baseROM.DownloadURL = versionParsed <= 4.7 ? + _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/romsfiles/{romfile.Id}/content/{romfile.FileName}") : + _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{romfile.Id}/files/content/{romfile.FileName}"); + } + else + { + baseROM.FileName = ROM.FileName; + baseROM.DownloadURL = _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{ROM.Id}/content/{ROM.FileName}"); + } + baseROM.IsSelected = false; + toSave.ROMVersions.Add(baseROM); + + // Save sibling data + if (_plugin.Settings.MergeRevisions && ROM.Siblings?.Count > 0) + { + + foreach (var sibling in ROM.Siblings) + { + var siblingROM = _ROMs.Find(x => x.Id == sibling.Id); + if(siblingROM != null) + { + RomMRevision saveSibling = new RomMRevision(); + + saveSibling.Id = siblingROM.Id; + saveSibling.HasMultipleFiles = siblingROM.HasMultipleFiles; + if (!siblingROM.HasMultipleFiles) + { + var romfile = DetermineFile(siblingROM); + if (romfile == null) + { + _plugin.Logger.Error("[Importer] Unable to save sibling ROM data as there is no rom file!"); + continue; + } + + saveSibling.FileName = romfile.FileName; + saveSibling.DownloadURL = versionParsed <= 4.7 ? + _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/romsfiles/{romfile.Id}/content/{romfile.FileName}") : + _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{romfile.Id}/files/content/{romfile.FileName}"); + } + else + { + saveSibling.FileName = siblingROM.FileName; + saveSibling.DownloadURL = _plugin.CombineUrl(_plugin.Settings.RomMHost, $"api/roms/{siblingROM.Id}/content/{siblingROM.FileName}"); + } + saveSibling.IsSelected = false; + _ROMs.First(x => x.Id == sibling.Id).Processed = true; + + toSave.ROMVersions.Add(saveSibling); + } + } + } + + // Write data to file + string json = JsonConvert.SerializeObject(toSave); + File.WriteAllText($"{_plugin.ROMDataPath}{ROM.SHA1}.json", json); + + } + + private Guid DetermineCompletionStatus(RomMRom ROM) + { + string completionStatus; + // Determine status in Playnite. Backlogged and "now playing" take precedent over the status options + if (ROM.RomUser.Backlogged || ROM.RomUser.NowPlaying) + { + completionStatus = ROM.RomUser.NowPlaying ? RomMRomUser.CompletionStatusMap["now_playing"] : RomMRomUser.CompletionStatusMap["backlogged"]; + } + else + { + completionStatus = RomMRomUser.CompletionStatusMap[ROM.RomUser.Status ?? "not_played"]; + } + + _completionStatusMap.TryGetValue(completionStatus, out var statusId); + + var status = _plugin.Playnite.Database.CompletionStatuses.Get(statusId); + var completionStatusProperty = status != null ? new MetadataNameProperty(status.Name) : null; + + return statusId; + } + } +} diff --git a/Games/RomMImportController.cs b/Games/RomMImportController.cs new file mode 100644 index 0000000..d40bcfe --- /dev/null +++ b/Games/RomMImportController.cs @@ -0,0 +1,231 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Playnite.SDK; +using Playnite.SDK.Models; +using Playnite.SDK.Plugins; +using RomM.Models.RomM.Collection; +using RomM.Models.RomM.Platform; +using RomM.Models.RomM.Rom; +using RomM.Settings; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Web; + +namespace RomM.Games +{ + class RomMImportController + { + private readonly RomM _plugin; + public ILogger Logger => LogManager.GetLogger(); + + public RomMImportController(RomM plugin) + { + _plugin = plugin; + } + + public List Import(LibraryImportGamesArgs args) + { + IList apiPlatforms = FetchPlatforms(); + List>> tasks = new List>>(); + List games = new List(); + IEnumerable enabledMappings = SettingsViewModel.Instance.Mappings?.Where(m => m.Enabled); + string url = BuildROMUrl(); + + if (enabledMappings == null || !enabledMappings.Any()) + { + _plugin.Playnite.Notifications.Add(_plugin.Id.ToString(), "No emulators are configured or enabled in RomM settings. No games will be fetched.", NotificationType.Error); + return games; + } + + IList favoritCollections = _plugin.FetchFavorites(); + var favorites = favoritCollections.FirstOrDefault(c => c.IsFavorite)?.RomIds ?? new List(); + + // Pull ROM data for each enabled mapping and add the games to playnite + foreach (var mapping in enabledMappings) + { + if (args.CancelToken.IsCancellationRequested) + break; + + // Check mapping has an Emulator, Profile & Platform assigned to it + if (mapping.Emulator == null || mapping.EmulatorProfile == null || mapping.RomMPlatform == null || mapping.RomMPlatform.Id == -1) + { + Logger.Warn($"[Import Controller] Emulator {mapping.MappingId} is misconfigured, skipping."); + continue; + } + + RomMPlatform apiPlatform = apiPlatforms.FirstOrDefault(p => p.Id == mapping.RomMPlatformId); + if (apiPlatform == null) + { + _plugin.Playnite.Notifications.Add(_plugin.Id.ToString(), $"Platform {mapping.RomMPlatform.Name} with ID {mapping.RomMPlatformId} not found in RomM, skipping.", NotificationType.Error); + continue; + } + + // Pull data from server + // Could be made async, but when testing (4.7.0) found a performance degradation + var romMROMs = DownloadROMData(args, url, apiPlatform); + if(romMROMs == null) + { + Logger.Warn($"[Import Controller] Failed to get ROMs for {apiPlatform.Name}."); + continue; + } + Logger.Info($"[Import Controller] Finished parsing response for {apiPlatform.Name}."); + + // Import games for current mapping + tasks.Add(Task>.Factory.StartNew(() => + { + RomMImport newImport = new RomMImport(_plugin, args, mapping, romMROMs, favorites); + return newImport.ProcessData(); + })); + + } + + Task.WhenAll(tasks).Wait(); + + foreach (var task in tasks) + { + games.AddRange(task.Result); + } + + return games; + } + + private static async Task GetAsyncWithParams(string baseUrl, NameValueCollection queryParams) + { + var uriBuilder = new UriBuilder(baseUrl); + var query = HttpUtility.ParseQueryString(uriBuilder.Query); + + foreach (string key in queryParams) + { + query[key] = queryParams[key]; + } + + uriBuilder.Query = query.ToString(); + + return await HttpClientSingleton.Instance.GetAsync(uriBuilder.Uri); + } + + private string BuildROMUrl() + { + string url = _plugin.CombineUrl(_plugin.Settings.RomMHost, "api/roms"); + + if (_plugin.Settings.SkipMissingFiles) + { + url += "?missing=false&"; + } + + // Exclude genres from import + string excludeGenresString = _plugin.Settings.ExcludeGenres.Trim(' '); + excludeGenresString = excludeGenresString.Trim(';'); + List excludeGenres = excludeGenresString.Split(';').ToList(); + if (!string.IsNullOrEmpty(excludeGenresString)) + { + // Add ? if it hasn't been added already + if (!_plugin.Settings.SkipMissingFiles) + { + url += "?"; + } + + if (excludeGenres.Count > 1) + { + foreach (var genre in excludeGenres) + { + url += $"genres={HttpUtility.UrlEncode(genre)}&"; + } + } + else + { + url += $"genres={HttpUtility.UrlEncode(excludeGenresString)}"; + } + } + url.Trim('&'); + + return url; + } + private IList FetchPlatforms() + { + string apiPlatformsUrl = _plugin.CombineUrl(_plugin.Settings.RomMHost, "api/platforms"); + try + { + HttpResponseMessage response = HttpClientSingleton.Instance.GetAsync(apiPlatformsUrl).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + + string body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + return JsonConvert.DeserializeObject>(body); + } + catch (HttpRequestException e) + { + Logger.Error($"[Import Controller] Request exception: {e.Message}"); + return new List(); + } + } + + private List DownloadROMData(LibraryImportGamesArgs args, string url, RomMPlatform platform) + { + Logger.Info($"[Import Controller] Starting to fetch games for {platform.Name}."); + + const int pageSize = 50; + int offset = 0; + bool hasMoreData = true; + var romData = new List(); + + // Download data from RomM server + while (hasMoreData) + { + if (args.CancelToken.IsCancellationRequested) + break; + + NameValueCollection queryParams = new NameValueCollection + { + { "platform_ids", platform.Id.ToString() }, + { "genres_logic", "none" }, + { "order_by", "name" }, + { "order_dir", "asc" }, + { "limit", pageSize.ToString() }, + { "offset", offset.ToString() }, + }; + + try + { + HttpResponseMessage response = GetAsyncWithParams(url, queryParams).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + + Logger.Info($"[Import Controller] Parsing response for {platform.Name} batch {offset / pageSize + 1}."); + + Stream body = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); + List roms; + using (StreamReader reader = new StreamReader(body)) + { + var jsonResponse = JObject.Parse(reader.ReadToEnd()); + roms = jsonResponse["items"].ToObject>(); + } + + Logger.Info($"[Import Controller] Parsed {roms.Count} roms for batch {offset / pageSize + 1}."); + romData.AddRange(roms); + + if (roms.Count < pageSize) + { + Logger.Info($"[Import Controller] Received less than {pageSize} roms for {platform.Name}, assuming no more games."); + hasMoreData = false; + break; + } + + offset += pageSize; + } + catch (HttpRequestException e) + { + Logger.Error($"[Import Controller] Request exception: {e.Message}"); + hasMoreData = false; + } + } + + return romData; + } + } + +} diff --git a/Games/RomMInstallController.cs b/Games/RomMInstallController.cs index 58c89eb..8cc37ba 100644 --- a/Games/RomMInstallController.cs +++ b/Games/RomMInstallController.cs @@ -12,107 +12,57 @@ namespace RomM.Games { + enum InstallStatus + { + Cancelled = -1 + } + internal class RomMInstallController : InstallController { protected readonly IRomM _romM; public ILogger Logger => LogManager.GetLogger(); - public bool HasSiblings = false; - public int SelectedSibling = -1; + public GameInstallInfo _gameData; - internal RomMInstallController(Game game, IRomM romM, bool hasSiblings, int selectedSibling) : base(game) + internal RomMInstallController(Game game, IRomM romM, GameInstallInfo GameData) : base(game) { Name = "Download"; _romM = romM; - HasSiblings = hasSiblings; - SelectedSibling = selectedSibling; + _gameData = GameData; } public override void Install(InstallActionArgs args) { - var info = Game.GetRomMGameInfo(); - - if (SelectedSibling == -2) + if (_gameData.Id == (int)InstallStatus.Cancelled) { CancelInstall(); return; - } - - // Replace info if a different version of the game is selected - if (HasSiblings && SelectedSibling != -1) - { - List siblingInfos = new List(); - - var version = Game.Version; - if (version == null || !version.StartsWith("RomM:")) - { - _romM.Playnite.Notifications.Add( - Game.GameId, - $"Failed to download {Game.Name}.{Environment.NewLine}{Environment.NewLine}{$"Couldn't find RomMId for {Game.Name}."}", - NotificationType.Error); - - CancelInstall(); - return; - } - - int romMId; - if (!int.TryParse(version.Split(':')[1], out romMId)) - { - _romM.Playnite.Notifications.Add( - Game.GameId, - $"Failed to download {Game.Name}.{Environment.NewLine}{Environment.NewLine}{$"Malformed version string? {version} > {romMId}"}", - NotificationType.Error); - - CancelInstall(); - return; - } - - siblingInfos = JsonConvert.DeserializeObject>(File.ReadAllText($"{_romM.ROMsWithSiblingsPath}{romMId}.json")); - - var selectedSibling = siblingInfos.Find(x => x.Id == SelectedSibling); - if (selectedSibling != null) - { - info.FileName = selectedSibling.FileName; - info.DownloadUrl = selectedSibling.DownloadURL; - // This has to be changed as systems can have single ROM and Multi ROM files. E.g. .chd vs .bin/.cue - info.HasMultipleFiles = selectedSibling.HasMultipleFiles; - } - else - { - _romM.Playnite.Notifications.Add( - Game.GameId, - $"Failed to find selected version of {Game.Name}.{Environment.NewLine}Selected sibling ID {SelectedSibling} was not found. Reimport libary!", - NotificationType.Error); - CancelInstall(); - return; - } - - } + } - var dstPath = info.Mapping?.DestinationPathResolved + var dstPath = _gameData.Mapping?.DestinationPathResolved ?? throw new Exception("Mapped emulator data cannot be found, try removing and re-adding."); // Paths (same as before) - var installDir = Path.Combine(dstPath, Path.GetFileNameWithoutExtension(info.FileName)); + var installDir = Path.Combine(dstPath, Path.GetFileNameWithoutExtension(_gameData.FileName)); // If RomM indicates multiple files, we download as an archive name (zip) into the install folder. // Otherwise we download the single ROM file. - var downloadFilePath = info.HasMultipleFiles - ? Path.Combine(installDir, info.FileName + ".zip") - : Path.Combine(installDir, info.FileName); + var downloadFilePath = _gameData.HasMultipleFiles + ? Path.Combine(installDir, _gameData.FileName + ".zip") + : Path.Combine(installDir, _gameData.FileName); var req = new DownloadRequest { GameId = Game.Id, GameName = Game.Name, - DownloadUrl = info.DownloadUrl, + DownloadUrl = _gameData.DownloadURL, InstallDir = installDir, GamePath = downloadFilePath, Use7z = _romM.Settings.Use7z, PathTo7Z = _romM.Settings.PathTo7z, - HasMultipleFiles = info.HasMultipleFiles, - AutoExtract = info.Mapping != null && info.Mapping.AutoExtract, + HasMultipleFiles = _gameData.HasMultipleFiles, + AutoExtract = _gameData.Mapping != null && _gameData.Mapping.AutoExtract, // Called by queue AFTER download/extract is done BuildRoms = () => @@ -127,11 +77,11 @@ public override void Install(InstallActionArgs args) } // Otherwise, we assume extracted files are in installDir - var supported = GetEmulatorSupportedFileTypes(info); + var supported = GetEmulatorSupportedFileTypes(_gameData); var actualRomFiles = GetRomFiles(installDir, supported); // Prefer .m3u if requested - var useM3u = info.Mapping != null && info.Mapping.UseM3u; + var useM3u = _gameData.Mapping != null && _gameData.Mapping.UseM3U && supported.Any(x => x.ToLower() == "m3u"); if (useM3u) { var m3uFile = actualRomFiles.FirstOrDefault(m => @@ -177,7 +127,7 @@ public override void Install(InstallActionArgs args) { _romM.Playnite.Notifications.Add( Game.GameId, - $"Failed to download {Game.Name}.{Environment.NewLine}{Environment.NewLine}{ex}", + $"Failed to download {Game.Name}.{Environment.NewLine}{Environment.NewLine}{ex.Message}", NotificationType.Error); Game.IsInstalling = false; @@ -223,7 +173,7 @@ private static string[] GetRomFiles(string installDir, List supportedFil }).ToArray(); } - private static List GetEmulatorSupportedFileTypes(RomMGameInfo info) + private static List GetEmulatorSupportedFileTypes(GameInstallInfo info) { if (info.Mapping.EmulatorProfile is CustomEmulatorProfile) { diff --git a/Games/RomMMetadataProvider.cs b/Games/RomMMetadataProvider.cs new file mode 100644 index 0000000..8c608f5 --- /dev/null +++ b/Games/RomMMetadataProvider.cs @@ -0,0 +1,73 @@ +using Playnite.SDK; +using Playnite.SDK.Models; +using RomM.Models.RomM.Rom; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace RomM.Games +{ + public class RomMMetadataProvider : LibraryMetadataProvider + { + private readonly IRomM _romM; + public RomMMetadataProvider(RomM romM) + { + _romM = romM; + } + + public override GameMetadata GetMetadata(Game game) + { + int romMId; + if (!int.TryParse(game.GameId.Split(':')[0], out romMId)) + { + _romM.Logger.Error($"[Metadata] {game.Name} GameID is malformed!"); + return null; + } + + RomMRom romMGame = _romM.FetchRom(romMId.ToString()); + if(romMGame == null) + { + _romM.Logger.Error($"[Metadata] {game.Name} failed to get game!"); + return null; + } + + var preferedRatingsBoard = _romM.Playnite.ApplicationSettings.AgeRatingOrgPriority; + var agerating = romMGame.Metadatum.Age_Ratings.Count > 0 ? new HashSet(romMGame.Metadatum.Age_Ratings.Where(r => r.Split(':')[0] == preferedRatingsBoard.ToString()).Select(r => new MetadataNameProperty(r.ToString()))) : null; + + List gameLinks = new List(); + if (romMGame.SSId != null) + gameLinks.Add(new Link("Screenscraper", $"https://www.screenscraper.fr/gameinfos.php?gameid={romMGame.SSId}")); + if (romMGame.HasheousId != null) + gameLinks.Add(new Link("Hasheous", $"https://hasheous.org/index.html?page=dataobjectdetail&type=game&id={romMGame.HasheousId}")); + if (romMGame.RAId != null) + gameLinks.Add(new Link("RetroAchievements", $"https://retroachievements.org/game/{romMGame.RAId}")); + if (romMGame.HLTBId != null) + gameLinks.Add(new Link("HowLongToBeat", $"https://howlongtobeat.com/game/{romMGame.HLTBId}")); + + var metadata = new GameMetadata + { + Name = romMGame.Name, + Description = romMGame.Summary, + + Regions = new HashSet(romMGame.Regions.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + Genres = new HashSet(romMGame.Metadatum.Genres.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + AgeRatings = agerating, + Series = new HashSet(romMGame.Metadatum.Franchises.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + Features = new HashSet(romMGame.Metadatum.Gamemodes.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + Categories = new HashSet(romMGame.Metadatum.Collections.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), + + ReleaseDate = romMGame.Metadatum.Release_Date.HasValue ? new ReleaseDate(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(romMGame.Metadatum.Release_Date.Value).ToLocalTime()) : new ReleaseDate(), + CommunityScore = (int?)romMGame.Metadatum.Average_Rating, + + CoverImage = !string.IsNullOrEmpty(romMGame.PathCoverL) ? new MetadataFile($"{_romM.Settings.RomMHost}{romMGame.PathCoverL}") : null, + + LastActivity = romMGame.RomUser.LastPlayed, + UserScore = romMGame.RomUser.Rating * 10, //RomM-Rating is 1-10, Playnite 1-100, so it can unfortunately only by synced one direction without loosing decimals + Links = gameLinks, + + }; + + return metadata; + } + } +} diff --git a/IRomm.cs b/IRomm.cs index 2017abb..4b4f4af 100644 --- a/IRomm.cs +++ b/IRomm.cs @@ -1,14 +1,21 @@ using Playnite.SDK; +using Playnite.SDK.Models; +using RomM.Models.RomM.Rom; +using System; namespace RomM { internal interface IRomM { - ILogger Logger { get; } + ILogger Logger { get; } IPlayniteAPI Playnite { get; } - Settings.SettingsViewModel Settings { get; } - string ROMsWithSiblingsPath { get; } + Guid Id { get; } + + Settings.SettingsViewModel Settings { get; } + MetadataProperty Source { get; } Downloads.DownloadQueueController DownloadQueueController { get; } string GetPluginUserDataPath(); - } + RomMRom FetchRom(string romId); + + } } \ No newline at end of file diff --git a/Models/RomM/Platform/RomMPlatform.cs b/Models/RomM/Platform/RomMPlatform.cs index e340380..3e2adcd 100644 --- a/Models/RomM/Platform/RomMPlatform.cs +++ b/Models/RomM/Platform/RomMPlatform.cs @@ -4,8 +4,36 @@ namespace RomM.Models.RomM.Platform { -public class RomMPlatform +public class RomMPlatform : IEquatable { + public bool Equals(RomMPlatform other) + { + if (Object.ReferenceEquals(other, null)) return false; + if (Object.ReferenceEquals(other, this)) return true; + return this.Name == other.Name; + } + + public sealed override bool Equals(object obj) + { + var otherMyItem = obj as RomMPlatform; + if (Object.ReferenceEquals(otherMyItem, null)) return false; + return otherMyItem.Equals(this); + } + + public static bool operator ==(RomMPlatform myItem1, RomMPlatform myItem2) + { + return Object.Equals(myItem1, myItem2); + } + + public static bool operator !=(RomMPlatform myItem1, RomMPlatform myItem2) + { + return !(myItem1 == myItem2); + } + public override int GetHashCode() + { + return this.Id.GetHashCode(); + } + [JsonProperty("id")] public int Id { get; set; } diff --git a/Models/RomM/Rom/RomMRom.cs b/Models/RomM/Rom/RomMRom.cs index 1800401..eb310f2 100644 --- a/Models/RomM/Rom/RomMRom.cs +++ b/Models/RomM/Rom/RomMRom.cs @@ -37,6 +37,9 @@ public class metadatum public class RomMFile { + [JsonProperty("id")] + public int? Id { get; set; } + [JsonProperty("file_name")] public string FileName { get; set; } @@ -47,7 +50,7 @@ public class RomMFile public string FullPath { get; set; } } - public class RomMSibling : ObservableObject + public class RomMSibling { [JsonProperty("id")] public int Id { get; set; } @@ -60,12 +63,6 @@ public class RomMSibling : ObservableObject [JsonProperty("fs_name_no_ext")] public string FileNameNoExt { get; set; } - - // Don't add JsonProperty data not from server - public string FileName { get; set; } - public string DownloadURL { get; set; } - public bool HasMultipleFiles { get; set; } - public bool isSelected { get; set; } = false; } public class RomMRom @@ -82,6 +79,18 @@ public class RomMRom [JsonProperty("moby_id")] public object MobyId { get; set; } + [JsonProperty("ss_id")] + public int? SSId { get; set; } + + [JsonProperty("ra_id")] + public int? RAId { get; set; } + + [JsonProperty("hasheous_id")] + public int? HasheousId { get; set; } + + [JsonProperty("hltb_id")] + public int? HLTBId { get; set; } + [JsonProperty("platform_id")] public int PlatformId { get; set; } @@ -184,6 +193,15 @@ public class RomMRom [JsonProperty("siblings")] public List Siblings { get; set; } + [JsonProperty("sha1_hash")] + public string SHA1 { get; set; } + + [JsonProperty("has_manual")] + public bool HasManual { get; set; } + + [JsonProperty("path_manual")] + public string ManualPath { get; set; } + [JsonProperty("full_path")] public string FullPath { get; set; } @@ -198,5 +216,7 @@ public class RomMRom [JsonProperty("sort_comparator")] public string SortComparator { get; set; } - } + + public bool Processed { get; set; } = false; +} } diff --git a/Models/RomM/Rom/RomMRomLocal.cs b/Models/RomM/Rom/RomMRomLocal.cs new file mode 100644 index 0000000..12ceb07 --- /dev/null +++ b/Models/RomM/Rom/RomMRomLocal.cs @@ -0,0 +1,41 @@ +using RomM.Settings; +using System; +using System.Collections.Generic; + +namespace RomM.Models.RomM.Rom +{ + enum MainSibling + { + None = -1, + Current = 0, + Other = 1 + } + + public struct GameInstallInfo + { + public int Id { get; set; } + public string FileName { get; set; } + public bool HasMultipleFiles { get; set; } + public string DownloadURL { get; set; } + public EmulatorMapping Mapping { get; set; } + } + + public struct RomMRevision + { + public int Id { get; set; } + public string FileName { get; set; } + public bool HasMultipleFiles { get; set; } + public string DownloadURL { get; set; } + public bool IsSelected { get; set; } + } + + public class RomMRomLocal + { + public string Name { get; set; } + public string SHA1 { get; set; } + public Guid MappingID { get; set; } + + public List ROMVersions { get; set; } + + } +} diff --git a/Models/RomM/Rom/RomMRomUser.cs b/Models/RomM/Rom/RomMRomUser.cs index d7786cf..19254a6 100644 --- a/Models/RomM/Rom/RomMRomUser.cs +++ b/Models/RomM/Rom/RomMRomUser.cs @@ -12,6 +12,9 @@ public class RomMRomUser [JsonProperty("user_id")] public int UserId { get; set; } + [JsonProperty("is_main_sibling")] + public bool IsMainSibling { get; set; } + [JsonProperty("last_played")] public DateTime? LastPlayed { get; set; } diff --git a/Models/RomM/RomMHeartbeat.cs b/Models/RomM/RomMHeartbeat.cs new file mode 100644 index 0000000..ba6b2bd --- /dev/null +++ b/Models/RomM/RomMHeartbeat.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RomM.Models.RomM +{ + struct ServerInfo + { + [JsonProperty("VERSION")] + public string Version { get; set; } + [JsonProperty("SHOW_SETUP_WIZARD")] + public bool ShowSetupWizard { get; set; } + } + + class RomMHeartbeat + { + [JsonProperty("SYSTEM")] + public ServerInfo SystemInfo { get; set; } + } +} diff --git a/Models/RomM/RomMUser.cs b/Models/RomM/RomMUser.cs new file mode 100644 index 0000000..57dd55d --- /dev/null +++ b/Models/RomM/RomMUser.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace RomM.Models.RomM +{ + public class RomMUser + { + [JsonProperty("id")] + public int Id { get; set; } + [JsonProperty("username")] + public string Username { get; set; } + [JsonProperty("email")] + public string Email { get; set; } + [JsonProperty("enabled")] + public bool Enabled { get; set; } + [JsonProperty("role")] + public string Role { get; set; } + [JsonProperty("avatar_path")] + public string IconPath { get; set; } + [JsonProperty("last_login")] + public string LastLogin { get; set; } + [JsonProperty("last_active")] + public string LastActive { get; set; } + [JsonProperty("ra_username")] + public string RAUsername { get; set; } + } +} diff --git a/RomM.cs b/RomM.cs index 17d5d93..aa6e3f2 100644 --- a/RomM.cs +++ b/RomM.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Playnite.SDK; using Playnite.SDK.Events; using Playnite.SDK.Models; @@ -8,13 +7,10 @@ using RomM.Downloads; using RomM.VersionSelector; using RomM.Models.RomM.Collection; -using RomM.Models.RomM.Platform; using RomM.Models.RomM.Rom; using RomM.Settings; using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Specialized; using System.IO; using System.Linq; using System.Net.Http; @@ -23,7 +19,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Web; using System.Windows; using System.Windows.Controls; @@ -39,27 +34,14 @@ static HttpClientSingleton() httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); } - public static void ConfigureAuth(SettingsViewModel settings) + public static void ConfigureBasicAuth(string username, string password) { - httpClient.DefaultRequestHeaders.Authorization = BuildAuthHeader(settings); + var base64Credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}")); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64Credentials); } - - public static AuthenticationHeaderValue BuildAuthHeader(SettingsViewModel settings) + public static void ConfigureAPIAuth(string apiToken) { - var token = settings.RomMApiToken?.Trim(); - if (SettingsViewModel.IsValidApiToken(token)) - { - return new AuthenticationHeaderValue("Bearer", token); - } - - if (!string.IsNullOrEmpty(settings.RomMUsername) && !string.IsNullOrEmpty(settings.RomMPassword)) - { - var creds = Convert.ToBase64String( - Encoding.ASCII.GetBytes($"{settings.RomMUsername}:{settings.RomMPassword}")); - return new AuthenticationHeaderValue("Basic", creds); - } - - return null; + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken); } public static HttpClient Instance => httpClient; @@ -84,15 +66,14 @@ public class RomM : LibraryPlugin, IRomM public ILogger Logger => LogManager.GetLogger(); public IPlayniteAPI Playnite { get; private set; } - public SettingsViewModel Settings { get; private set; } - - public string ROMsWithSiblingsPath { get; private set; } + public SettingsViewModel Settings { get; private set; } + public string ROMDataPath { get; private set; } + public MetadataProperty Source { get; private set; } public DownloadQueueController DownloadQueueController { get; private set; } - internal RomMDownloadsSidebarItem DownloadsSidebar { get; private set; } private readonly DownloadQueueViewModel downloadsVm; - + // Implementing Client adds ability to open it via special menu in playnite public override LibraryClient Client { get; } = new RomMClient(); @@ -101,9 +82,10 @@ public RomM(IPlayniteAPI api) : base(api) Playnite = api; Properties = new LibraryPluginProperties { - HasSettings = true + HasSettings = true, + HasCustomizedGameImport = true, }; - ROMsWithSiblingsPath = $"{Playnite.Paths.ExtensionsDataPath}\\{Id}\\ROMsWithSiblings\\"; + ROMDataPath = $"{Playnite.Paths.ExtensionsDataPath}\\{Id}\\Games\\"; // Initialise the download queue downloadsVm = new DownloadQueueViewModel(); @@ -118,93 +100,14 @@ public RomM(IPlayniteAPI api) : base(api) } } - private string CombineUrl(string baseUrl, string relativePath) - { - return $"{baseUrl?.TrimEnd('/')}/{relativePath?.TrimStart('/') ?? ""}"; - } - - internal IList FetchPlatforms() - { - string apiPlatformsUrl = CombineUrl(Settings.RomMHost, "api/platforms"); - try - { - HttpResponseMessage response = HttpClientSingleton.Instance.GetAsync(apiPlatformsUrl).GetAwaiter().GetResult(); - response.EnsureSuccessStatusCode(); - - string body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - return JsonConvert.DeserializeObject>(body); - } - catch (HttpRequestException e) - { - Logger.Error($"Request exception: {e.Message}"); - return new List(); - } - } - - internal IList FetchFavorites() - { - string apiFavoriteUrl = CombineUrl(Settings.RomMHost, "api/collections"); - try - { - // Make the request and get the response - HttpResponseMessage response = HttpClientSingleton.Instance.GetAsync(apiFavoriteUrl).GetAwaiter().GetResult(); - response.EnsureSuccessStatusCode(); - - // Assuming the response is in JSON format - string body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - return JsonConvert.DeserializeObject>(body); - } - catch (HttpRequestException e) - { - Logger.Error($"Request exception: {e.Message}"); - return new List(); - } - } - internal RomMCollection CreateFavorites() + #region Helper functions + public string CombineUrl(string baseUrl, string relativePath) { - string apiCollectionUrl = CombineUrl(Settings.RomMHost, "api/collections?is_favorite=true&is_public=false"); - try - { - var formData = new MultipartFormDataContent(); - formData.Add(new StringContent("Favorites"), "name"); - - HttpResponseMessage postResponse = HttpClientSingleton.Instance.PostAsync(apiCollectionUrl, formData).GetAwaiter().GetResult(); - postResponse.EnsureSuccessStatusCode(); - - string body = postResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - return JsonConvert.DeserializeObject(body); - } - catch (HttpRequestException e) - { - Logger.Error($"Request exception: {e.Message}"); - return null; - } - } - - internal void UpdateFavorites(RomMCollection favoriteCollection, List romIds) - { - if (favoriteCollection == null) - { - Logger.Error($"Can't update favorites, collection is null"); - return; - } - - string apiCollectionUrl = CombineUrl(Settings.RomMHost, "api/collections"); - try - { - var formData = new MultipartFormDataContent(); - formData.Add(new StringContent(JsonConvert.SerializeObject(romIds)), "rom_ids"); - HttpResponseMessage putResponse = HttpClientSingleton.Instance.PutAsync($"{apiCollectionUrl}/{favoriteCollection.Id}", formData).GetAwaiter().GetResult(); - putResponse.EnsureSuccessStatusCode(); - } - catch (HttpRequestException e) - { - Logger.Error($"Request exception: {e.Message}"); - } + return $"{baseUrl?.TrimEnd('/')}/{relativePath?.TrimStart('/') ?? ""}"; } - internal RomMRom FetchRom(string romId) + public RomMRom FetchRom(string romId) { string romUrl = CombineUrl(Settings.RomMHost, $"api/roms/{romId}"); try @@ -241,12 +144,12 @@ internal void HandleRommUri(PlayniteUriEventArgs args) foreach (var mapping in SettingsViewModel.Instance.Mappings?.Where(m => m.Enabled)) { - if (mapping.Platform.IgdbId.ToString() == platformIgdbId) + if (mapping.RomMPlatform.IgdbId.ToString() == platformIgdbId) { var gameName = rom.Name; var game = Playnite.Database.Games.FirstOrDefault(g => g.Source.Name == SourceName.ToString() && - g.Platforms.Any(p => p.Name == mapping.Platform.Name) && + g.Platforms.Any(p => p.Name == mapping.RomMPlatform.Name) && g.Name == gameName); if (game == null) @@ -271,13 +174,34 @@ internal void HandleRommUri(PlayniteUriEventArgs args) } } + // New-style overload (used by DownloadQueueController) + public static Task GetAsync(string url, HttpCompletionOption completionOption, CancellationToken ct) + { + return HttpClientSingleton.Instance.GetAsync(url, completionOption, ct); + } + #endregion + + #region Playnite functions public override void OnApplicationStarted(OnApplicationStartedEventArgs args) { base.OnApplicationStarted(args); + if (!Directory.Exists($"{ROMDataPath}")) + Directory.CreateDirectory($"{ROMDataPath}"); + Settings = new SettingsViewModel(this, this); - HttpClientSingleton.ConfigureAuth(Settings); + + if (Settings.UseBasicAuth && !string.IsNullOrEmpty(Settings.RomMUsername) && !string.IsNullOrEmpty(Settings.RomMPassword)) + { + HttpClientSingleton.ConfigureBasicAuth(Settings.RomMUsername, Settings.RomMPassword); + } + else if(SettingsViewModel.ApiTokenPattern.IsMatch(Settings.RomMClientToken)) + { + HttpClientSingleton.ConfigureAPIAuth(Settings.RomMClientToken); + } + Playnite.UriHandler.RegisterSource("romm", HandleRommUri); + Source = SourceName; // Portable path fix: expand "{PlayniteDir}" to absolute paths in DB on startup if (Playnite.Paths.IsPortable) @@ -318,36 +242,27 @@ public override void OnApplicationStarted(OnApplicationStartedEventArgs args) { if (item.PluginId == PluginId) { - var version = item.Version; - if (version == null || !version.StartsWith("RomM:")) - { - Logger.Warn($"Couldn't find RomMId for {item.Name}."); - continue; - } - - int romMId; - if (!int.TryParse(version.Split(':')[1], out romMId)) + if (item.GameId.Contains(':')) { - Logger.Error($"Malformed version string? {version} > {romMId}"); - continue; + if (File.Exists($"{ROMDataPath}{item.GameId.Split(':')[1]}.json")) + { + File.Delete($"{ROMDataPath}{item.GameId.Split(':')[1]}.json"); + } } - - if (File.Exists($"{ROMsWithSiblingsPath}{romMId}.json")) + else { - File.Delete($"{ROMsWithSiblingsPath}{romMId}.json"); + Logger.Error($"Game {item.Name} id is malformed!"); } - } } } }; } - public override void OnApplicationStopped(OnApplicationStoppedEventArgs args) { base.OnApplicationStopped(args); - + Playnite.Database.Games.ItemUpdated -= OnItemUpdated; // Portable path fix: restore "{PlayniteDir}" tokens before exiting @@ -382,573 +297,262 @@ public override void OnApplicationStopped(OnApplicationStoppedEventArgs args) } } - // Old-style overload (keeps older call sites working) - public static Task GetAsync( - string url, - HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) - { - return HttpClientSingleton.Instance.GetAsync(url, completionOption); - } - - // New-style overload (used by DownloadQueueController) - public static Task GetAsync( - string url, - HttpCompletionOption completionOption, - CancellationToken ct) - { - return HttpClientSingleton.Instance.GetAsync(url, completionOption, ct); - } - - public static async Task GetAsyncWithParams(string baseUrl, NameValueCollection queryParams) - { - var uriBuilder = new UriBuilder(baseUrl); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); - - foreach (string key in queryParams) - { - query[key] = queryParams[key]; - } - - uriBuilder.Query = query.ToString(); - - return await HttpClientSingleton.Instance.GetAsync(uriBuilder.Uri); - } - - public override IEnumerable GetGames(LibraryGetGamesArgs args) + public override IEnumerable ImportGames(LibraryImportGamesArgs args) { if (Playnite.ApplicationInfo.Mode == ApplicationMode.Fullscreen && !Settings.ScanGamesInFullScreen) { - return new List(); + return new List(); } - if (string.IsNullOrEmpty(Settings.RomMHost)) + if(!Settings.TestConnection()) { - Logger.Warn("RomM host is not set."); - return new List(); + return new List(); } - if (!Settings.HasAnyAuth) - { - Logger.Warn("RomM API token (rmm_ + 64 hex chars) or username/password must be set."); - return new List(); - } + return new RomMImportController(this).Import(args); + } - IList apiPlatforms = FetchPlatforms(); - List games = new List(); - IEnumerable enabledMappings = SettingsViewModel.Instance.Mappings?.Where(m => m.Enabled); + public override ISettings GetSettings(bool firstRunSettings) + { + return Settings; + } + public override UserControl GetSettingsView(bool firstRunSettings) + { + return new SettingsView(); + } - if (enabledMappings == null || !enabledMappings.Any()) + public override IEnumerable GetSidebarItems() + { + if (DownloadsSidebar != null) { - Logger.Warn("No emulators are configured or enabled in RomM settings. No games will be fetched."); - return games; + yield return DownloadsSidebar; } + } + public override IEnumerable GetGameMenuItems(GetGameMenuItemsArgs args) + { + List gameMenuItems = new List(); - IList favoritCollections = FetchFavorites(); - var favorites = favoritCollections.FirstOrDefault(c => c.IsFavorite)?.RomIds ?? new List(); - - foreach (var mapping in enabledMappings) + if (args.Games.First().PluginId == PluginId) { - if (args.CancelToken.IsCancellationRequested) - break; - - if (mapping.Emulator == null) - { - Logger.Warn($"Emulator {mapping.EmulatorId} not found, skipping."); - continue; - } - - if (mapping.EmulatorProfile == null) - { - Logger.Warn($"Emulator profile {mapping.EmulatorProfileId} for emulator {mapping.EmulatorId} not found, skipping."); - continue; - } - if (mapping.Platform == null) + if (Settings.MergeRevisions && File.Exists($"{ROMDataPath}{args.Games.First().GameId.Split(':')[1]}.json") && args.Games.First().IsInstalled) { - Logger.Warn($"Platform {mapping.PlatformId} not found, skipping."); - continue; + try + { + string json = File.ReadAllText($"{ROMDataPath}{args.Games.First().GameId.Split(':')[1]}.json"); + var gameData = JsonConvert.DeserializeObject(json); + if(gameData.ROMVersions.Count > 1) + { + gameMenuItems.Add(new GameMenuItem + { + //MenuSection = "@", + Description = "Switch ROM Version!", + Action = (gameMenuItem) => + { + Playnite.InstallGame(args.Games.First().Id); + } + }); + } + } + catch (Exception) + { + Logger.Error($"{args.Games.First().Name} GameID is malformed or json file is corrupted!"); + } } + } + return gameMenuItems; + } - string url = CombineUrl(Settings.RomMHost, "api/roms"); - RomMPlatform apiPlatform = apiPlatforms.FirstOrDefault(p => p.IgdbId == mapping.Platform.IgdbId); + public override IEnumerable GetInstallActions(GetInstallActionsArgs args) + { + if (args.Game.PluginId == Id) + { + string gameID = args.Game.GameId; + GameInstallInfo romData = new GameInstallInfo(); + RomMRomLocal gameData = new RomMRomLocal(); - if (apiPlatform == null) + if (args.Game.GameId.StartsWith("!0")) { - Logger.Warn($"Platform {mapping.Platform.Name} with IGDB ID {mapping.Platform.IgdbId} not found in RomM, skipping."); - continue; + PlayniteApi.Notifications.Add(new NotificationMessage(PluginId.ToString(), "Old ID detected run update game library before installing!", NotificationType.Error)); + romData.Id = (int)InstallStatus.Cancelled; } - - Logger.Debug($"Starting to fetch games for {apiPlatform.Name}."); - - const int pageSize = 72; - int offset = 0; - bool hasMoreData = true; - var allRoms = new List(); - var responseGameIDs = new HashSet(); - - while (hasMoreData) + else { - if (args.CancelToken.IsCancellationRequested) - break; - - NameValueCollection queryParams = new NameValueCollection + // Pull game file from RomM data directory + int romMId; + string romMSHA1 = gameID.Split(':')[1]; + if (!int.TryParse(args.Game.GameId.Split(':')[0], out romMId) || !File.Exists($"{ROMDataPath}{romMSHA1}.json")) { - { "limit", pageSize.ToString() }, - { "offset", offset.ToString() }, - { "platform_ids", apiPlatform.Id.ToString() }, - { "order_by", "name" }, - { "order_dir", "asc" }, - }; + Logger.Error($"{args.Game.Name} GameID is malformed!"); + romData.Id = (int)InstallStatus.Cancelled; + yield return new RomMInstallController(args.Game, this, romData); + } try { - HttpResponseMessage response = GetAsyncWithParams(url, queryParams).GetAwaiter().GetResult(); - response.EnsureSuccessStatusCode(); - - Logger.Debug($"Parsing response for {apiPlatform.Name} batch {offset / pageSize + 1}."); - - Stream body = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); - List roms; - using (StreamReader reader = new StreamReader(body)) - { - var jsonResponse = JObject.Parse(reader.ReadToEnd()); - roms = jsonResponse["items"].ToObject>(); - } - - Logger.Debug($"Parsed {roms.Count} roms for batch {offset / pageSize + 1}."); - allRoms.AddRange(roms); - - if (roms.Count < pageSize) - { - Logger.Debug($"Received less than {pageSize} roms for {apiPlatform.Name}, assuming no more games."); - hasMoreData = false; - break; - } - - offset += pageSize; + string json = File.ReadAllText($"{ROMDataPath}{romMSHA1}.json"); + gameData = JsonConvert.DeserializeObject(json); } - catch (HttpRequestException e) + catch (Exception) { - Logger.Error($"Request exception: {e.Message}"); - hasMoreData = false; + Logger.Error($"{args.Game.Name} GameID is malformed or {romMSHA1} json file is corrupted!"); + romData.Id = (int)InstallStatus.Cancelled; } - } - try - { - Logger.Debug($"Finished parsing response for {apiPlatform.Name}."); - - var rootInstallDir = PlayniteApi.Paths.IsPortable - ? mapping.DestinationPathResolved.Replace(PlayniteApi.Paths.ApplicationPath, ExpandableVariables.PlayniteDirectory) - : mapping.DestinationPathResolved; - - var completionStatusMap = PlayniteApi.Database.CompletionStatuses.ToDictionary(cs => cs.Name, cs => cs.Id); + if (romData.Id == (int)InstallStatus.Cancelled) + yield return new RomMInstallController(args.Game, this, romData); - if (!Directory.Exists(ROMsWithSiblingsPath)) - Directory.CreateDirectory(ROMsWithSiblingsPath); - - List ImportedROMsWithSiblings = new List(); - if (Settings.MergeRevisions) + // Set ROM data to base ROM + romData = new GameInstallInfo { - foreach (string filename in Directory.EnumerateFiles(ROMsWithSiblingsPath, "*.json", SearchOption.TopDirectoryOnly)) - { - int FileId; - if (!int.TryParse(Path.GetFileNameWithoutExtension(filename), out FileId)) - { - continue; - } - - ImportedROMsWithSiblings.Add(FileId); - } - } + Id = gameData.ROMVersions[0].Id, + FileName = gameData.ROMVersions[0].FileName, + HasMultipleFiles = gameData.ROMVersions[0].HasMultipleFiles, + DownloadURL = gameData.ROMVersions[0].DownloadURL, + Mapping = Settings.Mappings.FirstOrDefault(x => x.MappingId == gameData.MappingID) + }; - foreach (var item in allRoms) + // If Siblings are avaiable prompt user with version selection + if (Settings.MergeRevisions && gameData.ROMVersions?.Count > 1) { - if (args.CancelToken.IsCancellationRequested) - break; - - // Check for siblings and if one has already been imported skip - if (Settings.MergeRevisions && item.Siblings.Count > 0) - { - bool foundSibling = false; - - foreach (var sibling in item.Siblings) - { - if (ImportedROMsWithSiblings.Contains(sibling.Id)) - { - foundSibling = true; - break; - } - } - - if (foundSibling) - continue; - } - - var gameName = item.Name; - // Not sure if this a server bug or if my RomM server is borked but some games like Wii U dont have any of these enabled - if (!item.HasSimpleSingleFile & !item.HasNestedSingleFile & !item.HasMultipleFiles) - item.HasMultipleFiles = true; - - // Defensive: never allow path segments from server-provided filename & make sure single ROM files have an extention - var fileName = item.HasMultipleFiles ? Path.GetFileName(item.FileName) : Path.GetFileName(item.Files.Where(f => f.FullPath.Count(c => c == '/') <= 3).FirstOrDefault().FileName); - if (string.IsNullOrWhiteSpace(fileName)) - { - Logger.Warn($"Rom {item.Id} returned empty/invalid filename, skipping."); - continue; - } - - var urlCover = item.UrlCover; - var gameInstallDir = Path.Combine(rootInstallDir, Path.GetFileNameWithoutExtension(fileName)); - var pathToGame = Path.Combine(gameInstallDir, fileName); - - var info = new RomMGameInfo - { - MappingId = mapping.MappingId, - FileName = fileName, - DownloadUrl = CombineUrl(Settings.RomMHost, $"api/roms/{item.Id}/content/{fileName}"), - HasMultipleFiles = item.HasMultipleFiles - }; - - var gameId = info.AsGameId(); - responseGameIDs.Add(gameId); - // Save sibling data so user can select the version they want installed - if (Settings.MergeRevisions && item.Siblings.Count > 0) + RomMVersionSelector VersionSelectorControl = new RomMVersionSelector(gameData.ROMVersions); + var window = Playnite.Dialogs.CreateWindow(new WindowCreationOptions { - List gameInfos = new List(); - - var baseSibling = new RomMSibling - { - Id = item.Id, - Name = item.Name, - FileNameNoTags = item.FileNameNoTags, - FileNameNoExt = item.FileNameNoExt, - FileName = fileName, - HasMultipleFiles = item.HasMultipleFiles, - DownloadURL = CombineUrl(Settings.RomMHost, $"api/roms/{item.Id}/content/{fileName}"), - isSelected = true - }; - gameInfos.Add(baseSibling); - - foreach (var sibling in item.Siblings) - { - var siblingItem = allRoms.Find(x => x.Id == sibling.Id); - - if (siblingItem == null) - { - Logger.Error($"Unable to find sibling data for id:{sibling.Id}"); - continue; - } - - var siblingfileName = ""; - try - { - siblingfileName = siblingItem.HasMultipleFiles ? Path.GetFileName(siblingItem.FileName) : Path.GetFileName(siblingItem.Files.Where(f => f.FullPath.Count(c => c == '/') <= 3).FirstOrDefault().FileName); - } - catch (Exception ex) - { - Logger.Error($"ROM: {item.Id} Error:{ex.ToString()}! Skipping ROM!"); - continue; - } - - if (string.IsNullOrWhiteSpace(siblingfileName)) - { - Logger.Warn($"Rom {siblingItem.Id} returned empty/invalid filename, skipping."); - continue; - } + ShowMinimizeButton = false, + ShowMaximizeButton = false, + ShowCloseButton = false, + }); - sibling.FileName = siblingfileName; - sibling.DownloadURL = CombineUrl(Settings.RomMHost, $"api/roms/{sibling.Id}/content/{siblingfileName}"); - sibling.HasMultipleFiles = siblingItem.HasMultipleFiles; + window.Height = 215; + window.Width = 600; - gameInfos.Add(sibling); - } + window.Title = "Select Version to install!"; + window.ShowInTaskbar = false; + window.ResizeMode = ResizeMode.NoResize; + window.Owner = API.Instance.Dialogs.GetCurrentAppWindow(); + window.WindowStartupLocation = WindowStartupLocation.CenterOwner; + window.Content = VersionSelectorControl; - File.WriteAllText($"{ROMsWithSiblingsPath}{item.Id}.json", JsonConvert.SerializeObject(gameInfos)); - ImportedROMsWithSiblings.Add(item.Id); - } + window.ShowDialog(); - string completionStatus; - // Determine status in Playnite. Backlogged and "now playing" take precedent over the status options - if (item.RomUser.Backlogged || item.RomUser.NowPlaying) + if (VersionSelectorControl.Cancelled) { - completionStatus = item.RomUser.NowPlaying ? RomMRomUser.CompletionStatusMap["now_playing"] : RomMRomUser.CompletionStatusMap["backlogged"]; + romData.Id = (int)InstallStatus.Cancelled; } else { - completionStatus = RomMRomUser.CompletionStatusMap[item.RomUser.Status ?? "not_played"]; - } - - completionStatusMap.TryGetValue(completionStatus, out var statusId); - - var status = PlayniteApi.Database.CompletionStatuses.Get(statusId); - var completionStatusProperty = status != null ? new MetadataNameProperty(status.Name) : null; - - // Check if the game is already installed - var game = Playnite.Database.Games.FirstOrDefault(g => g.GameId == gameId); - if (game != null) - { - // If it is already installed, we sync over metadata like favorite and status - if (Settings.KeepRomMSynced == true) + // Uninstall old ROM before installing new one + if (args.Game.IsInstalled) { - game.Favorite = favorites.Exists(f => f == item.Id); - - if (statusId != Guid.Empty) - { - game.CompletionStatusId = statusId; - } - - // Using the Version-Field for storing the ID instead of "RomMGameInfo" - // Could be useful in the future: https://github.com/JosefNemec/Playnite/issues/801 - game.Version = $"RomM:{item.Id}"; + Playnite.UninstallGame(args.Game.Id); - ignoredGameIds.TryAdd(game.Id, 0); - Playnite.Database.Games.Update(game); + args.Game.IsInstalling = true; + Playnite.Database.Games.Update(args.Game); } - continue; - } - - var gameNameWithTags = - $"{gameName}" + - $"{(item.Regions.Count > 0 ? $" ({string.Join(", ", item.Regions)})" : "")}" + - $"{(!string.IsNullOrEmpty(item.Revision) ? $" (Rev {item.Revision})" : "")}" + - $"{(item.Tags.Count > 0 ? $" ({string.Join(", ", item.Tags)})" : "")}"; - - // Add newly found game - games.Add(new GameMetadata - { - Source = SourceName, - Name = gameName, - Roms = new List { new GameRom(gameNameWithTags, pathToGame) }, - InstallDirectory = gameInstallDir, - IsInstalled = File.Exists(pathToGame), - GameId = gameId, - Platforms = new HashSet { new MetadataNameProperty(mapping.Platform.Name ?? "") }, - Regions = new HashSet(item.Regions.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), - Genres = new HashSet(item.Metadatum.Genres.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), - ReleaseDate = item.Metadatum.Release_Date.HasValue ? new ReleaseDate(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(item.Metadatum.Release_Date.Value).ToLocalTime()) : new ReleaseDate(), - Series = new HashSet(item.Metadatum.Franchises.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), - CommunityScore = (int?)item.Metadatum.Average_Rating, - Features = new HashSet(item.Metadatum.Gamemodes.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), - Categories = new HashSet(item.Metadatum.Collections.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))), - InstallSize = item.FileSizeBytes, - Description = item.Summary, - CoverImage = !string.IsNullOrEmpty(urlCover) ? new MetadataFile(urlCover) : null, - Favorite = favorites.Exists(f => f == item.Id), - LastActivity = item.RomUser.LastPlayed, - UserScore = item.RomUser.Rating * 10, //RomM-Rating is 1-10, Playnite 1-100, so it can unfortunately only by synced one direction without loosing decimals - CompletionStatus = completionStatusProperty, - GameActions = new List - { - new GameAction - { - Name = $"Play in {mapping.Emulator.Name}", - Type = GameActionType.Emulator, - EmulatorId = mapping.EmulatorId, - EmulatorProfileId = mapping.EmulatorProfileId, - IsPlayAction = true, - }, - new GameAction - { - Type = GameActionType.URL, - Name = "View in RomM", - Path = CombineUrl(Settings.RomMHost, $"rom/{item.Id}"), - IsPlayAction = false - } - }, - Version = $"RomM:{item.Id}" - }); - } - - Logger.Debug($"Finished adding new games for {apiPlatform.Name}"); - - var gamesInDatabase = Playnite.Database.Games.Where(g => - g.Source != null && g.Source.Name == SourceName.ToString() && - g.Platforms != null && g.Platforms.Any(p => p.Name == mapping.Platform.Name) - ); - Logger.Debug($"Starting to remove not found games for {apiPlatform.Name}."); - foreach (var game in gamesInDatabase) - { - if (args.CancelToken.IsCancellationRequested) - break; + var selectedrevision = VersionSelectorControl.RomVersions.First(x => x.IsSelected); + romData.Id = selectedrevision.Id; + romData.FileName = selectedrevision.FileName; + romData.HasMultipleFiles = selectedrevision.HasMultipleFiles; + romData.DownloadURL = selectedrevision.DownloadURL; + + gameData.ROMVersions = VersionSelectorControl.RomVersions.ToList(); - if (responseGameIDs.Contains(game.GameId)) - { - continue; + File.WriteAllText($"{ROMDataPath}{romMSHA1}.json", JsonConvert.SerializeObject(gameData)); } - - Playnite.Database.Games.Remove(game.Id); } - - Logger.Debug($"Finished removing not found games for {apiPlatform.Name}"); } - catch (HttpRequestException e) - { - Logger.Error($"Request exception: {e.Message}"); - return games; - } - } - return games; + yield return new RomMInstallController(args.Game, this, romData); + } } - - public override IEnumerable GetSidebarItems() + public override IEnumerable GetUninstallActions(GetUninstallActionsArgs args) { - if (DownloadsSidebar != null) + if (args.Game.PluginId == Id) { - yield return DownloadsSidebar; + yield return new RomMUninstallController(args.Game, this); } } - - public override ISettings GetSettings(bool firstRunSettings) + public override void OnGameInstalled(OnGameInstalledEventArgs args) { - return Settings; + base.OnGameInstalled(args); + + if (args.Game.PluginId == PluginId && Settings.NotifyOnInstallComplete) + { + Playnite.Notifications.Add(args.Game.GameId, $"Download of \"{args.Game.Name}\" is complete", NotificationType.Info); + } } - public override UserControl GetSettingsView(bool firstRunSettings) + public override LibraryMetadataProvider GetMetadataDownloader() { - return new SettingsView(); + return new RomMMetadataProvider(this); } + #endregion - public override IEnumerable GetGameMenuItems(GetGameMenuItemsArgs args) + #region RomM Status Syncing + public IList FetchFavorites() { - List gameMenuItems = new List(); - - if (args.Games.First().PluginId == PluginId) + string apiFavoriteUrl = CombineUrl(Settings.RomMHost, "api/collections"); + try { - var version = args.Games.First().Version; - if (version == null || !version.StartsWith("RomM:")) - { - Logger.Warn($"Couldn't find RomMId for {args.Games.First().Name}."); - return gameMenuItems; - } - - int romMId; - if (!int.TryParse(version.Split(':')[1], out romMId)) - { - Logger.Error($"Malformed version string? {version} > {romMId}"); - return gameMenuItems; - } + // Make the request and get the response + HttpResponseMessage response = HttpClientSingleton.Instance.GetAsync(apiFavoriteUrl).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); - if (Settings.MergeRevisions && File.Exists($"{ROMsWithSiblingsPath}{romMId}.json") && args.Games.First().IsInstalled) - { - gameMenuItems.Add(new GameMenuItem - { - //MenuSection = "@", - Description = "Switch ROM Version!", - Action = (gameMenuItem) => - { - Playnite.InstallGame(args.Games.First().Id); - } - }); - } + // Assuming the response is in JSON format + string body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + return JsonConvert.DeserializeObject>(body); + } + catch (HttpRequestException e) + { + Logger.Error($"Request exception: {e.Message}"); + return new List(); } - return gameMenuItems; } - - public override IEnumerable GetInstallActions(GetInstallActionsArgs args) + internal RomMCollection CreateFavorites() { - if (args.Game.PluginId == Id) + string apiCollectionUrl = CombineUrl(Settings.RomMHost, "api/collections?is_favorite=true&is_public=false"); + try { - bool hasSiblings = false; - int siblingID = -1; - - var version = args.Game.Version; - if (version == null || !version.StartsWith("RomM:")) - { - Logger.Warn($"Couldn't find RomMId for {args.Game.Name}."); - //Set SiblingId to -2 to cancel request - siblingID = -2; - } - - int romMId = -1; - if (siblingID != -2) - { - if (!int.TryParse(version.Split(':')[1], out romMId)) - { - Logger.Error($"Malformed version string? {version} > {romMId}"); - siblingID = -2; - } - } - - // If Siblings are avaiable prompt user with version selection - if (Settings.MergeRevisions && File.Exists($"{ROMsWithSiblingsPath}{romMId}.json") && siblingID != -2) - { - List siblingInfos = new List(); - string json = File.ReadAllText($"{ROMsWithSiblingsPath}{romMId}.json"); - siblingInfos = JsonConvert.DeserializeObject>(json); - - RomMVersionSelector VersionSelectorControl = new RomMVersionSelector(siblingInfos); - - var window = Playnite.Dialogs.CreateWindow(new WindowCreationOptions - { - ShowMinimizeButton = false, - ShowMaximizeButton = false, - ShowCloseButton = false, - }); - - window.Height = 215; - window.Width = 600; - - window.Title = "Select Version to install!"; - window.ShowInTaskbar = false; - window.ResizeMode = ResizeMode.NoResize; - window.Owner = API.Instance.Dialogs.GetCurrentAppWindow(); - window.WindowStartupLocation = WindowStartupLocation.CenterOwner; - window.Content = VersionSelectorControl; - - window.ShowDialog(); - - if (VersionSelectorControl.Cancelled) - { - siblingID = -2; - } - else - { - //Uninstall old ROM before installing new one - if (args.Game.IsInstalled) - { - Playnite.UninstallGame(args.Game.Id); - - args.Game.IsInstalling = true; - Playnite.Database.Games.Update(args.Game); - } - - hasSiblings = true; - siblingID = VersionSelectorControl.Siblings.Where(x => x.isSelected).First().Id; + var formData = new MultipartFormDataContent(); + formData.Add(new StringContent("Favorites"), "name"); - //Write result back to json file - siblingInfos = VersionSelectorControl.Siblings.ToList(); - File.WriteAllText($"{ROMsWithSiblingsPath}{romMId}.json", JsonConvert.SerializeObject(siblingInfos)); - } - } + HttpResponseMessage postResponse = HttpClientSingleton.Instance.PostAsync(apiCollectionUrl, formData).GetAwaiter().GetResult(); + postResponse.EnsureSuccessStatusCode(); - yield return args.Game.GetRomMGameInfo().GetInstallController(args.Game, this, hasSiblings, siblingID); + string body = postResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + return JsonConvert.DeserializeObject(body); } - } - - public override IEnumerable GetUninstallActions(GetUninstallActionsArgs args) - { - if (args.Game.PluginId == Id) + catch (HttpRequestException e) { - yield return args.Game.GetRomMGameInfo().GetUninstallController(args.Game, this); + Logger.Error($"Request exception: {e.Message}"); + return null; } } - - public override void OnGameInstalled(OnGameInstalledEventArgs args) + internal void UpdateFavorites(RomMCollection favoriteCollection, List romIds) { - base.OnGameInstalled(args); + if (favoriteCollection == null) + { + Logger.Error($"Can't update favorites, collection is null"); + return; + } - if (args.Game.PluginId == PluginId && Settings.NotifyOnInstallComplete) + string apiCollectionUrl = CombineUrl(Settings.RomMHost, "api/collections"); + try { - Playnite.Notifications.Add(args.Game.GameId, $"Download of \"{args.Game.Name}\" is complete", NotificationType.Info); + var formData = new MultipartFormDataContent(); + formData.Add(new StringContent(JsonConvert.SerializeObject(romIds)), "rom_ids"); + HttpResponseMessage putResponse = HttpClientSingleton.Instance.PutAsync($"{apiCollectionUrl}/{favoriteCollection.Id}", formData).GetAwaiter().GetResult(); + putResponse.EnsureSuccessStatusCode(); + } + catch (HttpRequestException e) + { + Logger.Error($"Request exception: {e.Message}"); } } - private readonly ConcurrentDictionary ignoredGameIds = new ConcurrentDictionary(); private void OnItemUpdated(object sender, ItemUpdatedEventArgs e) { Task.Run(async () => @@ -972,27 +576,12 @@ private void OnItemUpdated(object sender, ItemUpdatedEventArgs e) if (Settings.KeepRomMSynced == true) { - if (ignoredGameIds.ContainsKey(newGame.Id)) + int romMId; + if(!int.TryParse(newGame.GameId.Split(':')[0], out romMId)) { - // This GameId is marked as an internal update, should be ignored this time - ignoredGameIds.TryRemove(newGame.Id, out _); - continue; - } - - var version = newGame.Version; - if (version == null || !version.StartsWith("RomM:")) - { - Logger.Warn($"Couldn't find RomMId for {update.NewData.Name}."); - continue; + Logger.Error($"{newGame.Name} GameID is malformed!"); } - int romMId; - if (!int.TryParse(version.Split(':')[1], out romMId)) - { - Logger.Error($"Malformed version string? {version} > {romMId}"); - continue; - } - if (oldGame.Favorite != newGame.Favorite) { Logger.Info($"Favorites changed for {romMId}."); @@ -1051,5 +640,6 @@ private void OnItemUpdated(object sender, ItemUpdatedEventArgs e) } }); } + #endregion } -} +} \ No newline at end of file diff --git a/RomM.csproj b/RomM.csproj index 39297f9..160dd3b 100644 --- a/RomM.csproj +++ b/RomM.csproj @@ -54,11 +54,6 @@ - - - PreserveNewest - - MSBuild:Compile @@ -110,6 +105,12 @@ MSBuild:Compile + + PreserveNewest + + + PreserveNewest + SettingsSingleFileGenerator Settings.Designer.cs diff --git a/Settings/EmulatorMapping.cs b/Settings/EmulatorMapping.cs index 620d91a..12fa907 100644 --- a/Settings/EmulatorMapping.cs +++ b/Settings/EmulatorMapping.cs @@ -6,91 +6,260 @@ using System.ComponentModel; using System.Linq; using System.Xml.Serialization; +using RomM.Models.RomM.Platform; +using SharpCompress; namespace RomM.Settings { public class EmulatorMapping : ObservableObject { - public EmulatorMapping() + [JsonIgnore] + private Guid _mappingId; + [JsonIgnore] + private string _mappingName = ""; + [JsonIgnore] + private bool _enabled = true; + [JsonIgnore] + private bool _autoExtract = false; + [JsonIgnore] + private bool _useM3U = false; + [JsonIgnore] + private Emulator _emulator; + [JsonIgnore] + private Guid _emulatorId; + [JsonIgnore] + private EmulatorProfile _emulatorProfile; + [JsonIgnore] + private IEnumerable _availableProfiles; + [JsonIgnore] + public string _emulatorProfileId; + [JsonIgnore] + private RomMPlatform _emulatedPlatform = new RomMPlatform(); + [JsonIgnore] + private IEnumerable _availablePlatforms; + [JsonIgnore] + public int _romMPlatformId = -1; + [JsonIgnore] + private string _destinationPath = ""; + + public EmulatorMapping(List romMPlatforms) { MappingId = Guid.NewGuid(); + AvailablePlatforms = romMPlatforms; } - public Guid MappingId { get; set; } + public Guid MappingId + { + get => _mappingId; + set + { + _mappingId = value; + OnPropertyChanged(); + } + } [DefaultValue(true)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public bool Enabled { get; set; } + public bool Enabled + { + get => _enabled; + set + { + _enabled = value; + OnPropertyChanged(); + } + } [DefaultValue(false)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public bool AutoExtract { get; set; } + public bool AutoExtract + { + get => _autoExtract; + set + { + _autoExtract = value; + OnPropertyChanged(); + } + } [DefaultValue(false)] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] - public bool UseM3u { get; set; } + public bool UseM3U + { + get => _useM3U; + set + { + _useM3U = value; + OnPropertyChanged(); + } + } [JsonIgnore] public Emulator Emulator { - get => AvailableEmulators.FirstOrDefault(e => e.Id == EmulatorId); - set { EmulatorId = value.Id; } + get => _emulator; + set + { + if (value != null) + { + _emulator = value; + _emulatorId = value.Id; + AvailableProfiles = Emulator?.SelectableProfiles; + RomMPlatform = new RomMPlatform(); + MappingName = value.Name; + OnPropertyChanged(); + } + } + } + public Guid EmulatorId + { + get => _emulatorId; + set + { + _emulatorId = value; + Emulator = SettingsViewModel.Instance.PlayniteAPI.Database.Emulators.FirstOrDefault(x => x.Id == _emulatorId); + OnPropertyChanged(); + } } - public Guid EmulatorId { get; set; } [JsonIgnore] public EmulatorProfile EmulatorProfile { - get => Emulator?.SelectableProfiles.FirstOrDefault(p => p.Id == EmulatorProfileId); - set { EmulatorProfileId = value.Id; } - } + get => _emulatorProfile; + set + { + if (value != null) + { + _emulatorProfile = value; + _emulatorProfileId = value.Id; - public string EmulatorProfileId { get; set; } + if (Emulator != null) + { + var name = Emulator.Name; + if (EmulatorProfile != null && EmulatorProfile.Name != "") + name += " - " + EmulatorProfile.Name; + if (RomMPlatform != null && !string.IsNullOrEmpty(RomMPlatform.Name)) + name += " - " + RomMPlatform.Name; + MappingName = name; + } + } + OnPropertyChanged(); + } + } + public string EmulatorProfileId + { + get => _emulatorProfileId; + set + { + _emulatorProfileId = value; + EmulatorProfile = Emulator?.SelectableProfiles.FirstOrDefault(x => x.Id == _emulatorProfileId); + OnPropertyChanged(); + } + } + + // (Deprecated) DON'T USE + [JsonIgnore] + public Platform Platform + { + get => null; + set + { + } + } + // (Deprecated) DON'T USE [JsonIgnore] - public EmulatedPlatform Platform + public string PlatformId { - get => AvailablePlatforms.FirstOrDefault(p => p.Id == PlatformId); - set { PlatformId = value.Id; } + get => ""; + set + { + } } - public string PlatformId { get; set; } - public string DestinationPath { get; set; } - public static IEnumerable AvailableEmulators => SettingsViewModel.Instance.PlayniteAPI.Database.Emulators?.OrderBy(x => x.Name) ?? Enumerable.Empty(); + [JsonIgnore] + public RomMPlatform RomMPlatform + { + get => _emulatedPlatform; + set + { + _emulatedPlatform = value; + _romMPlatformId = -1; + if(value != null) + { + _romMPlatformId = value.Id; + + if(Emulator != null) + { + var name = Emulator.Name; + if (EmulatorProfile != null && EmulatorProfile.Name != "") + name += " - " + EmulatorProfile.Name; + if (RomMPlatform != null && !string.IsNullOrEmpty(RomMPlatform.Name)) + name += " - " + RomMPlatform.Name; + + MappingName = name; + } + + } + OnPropertyChanged(); + } + } + public int RomMPlatformId + { + get => _romMPlatformId; + set + { + _romMPlatformId = value; + OnPropertyChanged(); + } + } [JsonIgnore] - public IEnumerable AvailableProfiles => Emulator?.SelectableProfiles; + public string MappingName + { + get => _mappingName; + set + { + _mappingName = value; + OnPropertyChanged(); + } + } + public string DestinationPath + { + get => _destinationPath; + set + { + _destinationPath = value; + OnPropertyChanged(); + } +} + +[JsonIgnore] + public static IEnumerable AvailableEmulators => SettingsViewModel.Instance.PlayniteAPI.Database.Emulators?.OrderBy(x => x.Name) ?? Enumerable.Empty(); [JsonIgnore] - public IEnumerable AvailablePlatforms + public IEnumerable AvailableProfiles { - get + get => _availableProfiles; + set { - var playnite = SettingsViewModel.Instance.PlayniteAPI; - HashSet validPlatforms; + _availableProfiles = value; + OnPropertyChanged(); + } + } + [JsonIgnore] + public IEnumerable AvailablePlatforms + { + get => _availablePlatforms; + set + { + _availablePlatforms = value; + OnPropertyChanged(); - if (EmulatorProfile is CustomEmulatorProfile) + if (_availablePlatforms != null && RomMPlatformId != -1) { - var customProfile = EmulatorProfile as CustomEmulatorProfile; - validPlatforms = new HashSet(playnite.Database.Platforms.Where(p => customProfile.Platforms.Contains(p.Id)).Select(p => p.SpecificationId)); + RomMPlatform = AvailablePlatforms.FirstOrDefault (x => x.Id == RomMPlatformId); } - else if (EmulatorProfile is BuiltInEmulatorProfile) - { - var builtInProfile = (EmulatorProfile as BuiltInEmulatorProfile); - validPlatforms = new HashSet( - playnite.Emulation.Emulators - .FirstOrDefault(e => e.Id == Emulator.BuiltInConfigId)? - .Profiles - .FirstOrDefault(p => p.Name == builtInProfile.Name)? - .Platforms - ); - } - else - { - validPlatforms = new HashSet(); - } - - return playnite.Emulation.Platforms?.Where(p => validPlatforms.Contains(p.Id)) ?? Enumerable.Empty(); } } @@ -128,11 +297,11 @@ public string EmulatorBasePathResolved public IEnumerable GetDescriptionLines() { - yield return $"{nameof(EmulatorId)}: {EmulatorId}"; + yield return $"{nameof(_emulatorId)}: {_emulatorId}"; yield return $"{nameof(Emulator)}*: {Emulator?.Name ?? ""}"; yield return $"{nameof(EmulatorProfileId)}: {EmulatorProfileId ?? ""}"; yield return $"{nameof(EmulatorProfile)}*: {EmulatorProfile?.Name ?? ""}"; - yield return $"{nameof(PlatformId)}: {PlatformId ?? ""}"; + yield return $"{nameof(PlatformId)}: {PlatformId}"; yield return $"{nameof(Platform)}*: {Platform?.Name ?? ""}"; yield return $"{nameof(DestinationPath)}: {DestinationPath ?? ""}"; yield return $"{nameof(DestinationPathResolved)}*: {DestinationPathResolved ?? ""}"; diff --git a/Settings/Settings.cs b/Settings/Settings.cs index cc6ecaf..da93ea4 100644 --- a/Settings/Settings.cs +++ b/Settings/Settings.cs @@ -1,73 +1,280 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + using Playnite.SDK; using Playnite.SDK.Plugins; + +using RomM.Models.RomM; +using RomM.Models.RomM.Platform; + using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; +using System.Net.Http; +using System.Reflection; using System.Text.RegularExpressions; +using System.Windows.Data; +using System.Windows.Media; +using System.Windows.Media.Imaging; namespace RomM.Settings { public class SettingsViewModel : ObservableObject, ISettings { private readonly Plugin _plugin; - private SettingsViewModel editingClone { get; set; } + [JsonIgnore] internal readonly IPlayniteAPI PlayniteAPI; + [JsonIgnore] internal readonly IRomM RomM; + public static SettingsViewModel Instance { get; private set; } - [JsonIgnore] - internal readonly IPlayniteAPI PlayniteAPI; + #region Backing Variables + + [JsonIgnore] private string _romMHost = ""; + [JsonIgnore] private string _romMServerVersion = "---"; + [JsonIgnore] private string _romMClientToken = ""; + [JsonIgnore] private bool _useBasicAuth = true; + [JsonIgnore] private string _romMUsername = ""; + [JsonIgnore] private string _romMPassword = ""; + [JsonIgnore] private string _defaultprofilepath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"profile.png"); + [JsonIgnore] private string _profilepath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"profile.png"); + [JsonIgnore] private string _romMUser = "----"; + [JsonIgnore] private string _profileType = "----"; + + [JsonIgnore] private string _excludeGenres = ""; + [JsonIgnore] private string _7zPath = ""; + + [JsonIgnore] private List _romMPlatforms = new List(); + + [JsonIgnore] private bool _notify = false; + [JsonIgnore] private string _notifyText = ""; + [JsonIgnore] private string _notifyIcon = ""; + [JsonIgnore] private Color _notfiyColour = Colors.DarkSlateGray; + [JsonIgnore] private Brush _notfiyTextColour = new SolidColorBrush(Colors.LightGray); + + #endregion + + #region Notifcation Bar + [JsonIgnore] + public bool Notify + { + get => _notify; + set + { + _notify = value; + OnPropertyChanged(); + } + } + [JsonIgnore] + public string NotifyText + { + get => _notifyText; + set + { + _notifyText = value; + OnPropertyChanged(); + } + } [JsonIgnore] - internal readonly IRomM RomM; + public string NotifyIcon + { + get => _notifyIcon; + set + { + _notifyIcon = value; + OnPropertyChanged(); + } + } + [JsonIgnore] + public Color NotfiyColour + { + get => _notfiyColour; + set + { + _notfiyColour = value; + OnPropertyChanged(); + } + } + [JsonIgnore] + public Brush NotfiyTextColour + { + get => _notfiyTextColour; + set + { + _notfiyTextColour = value; + OnPropertyChanged(); + } + } + public void UpdateNotifcationBar(string Message, bool IsError = false) + { + if (IsError) + { + NotfiyColour = (Color)ColorConverter.ConvertFromString("#730000"); + NotfiyTextColour = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#ff6b6b")); + NotifyIcon = $" \uE730"; + NotifyText = $" {Message}"; + Notify = true; + } + else + { + NotfiyColour = (Color)ColorConverter.ConvertFromString("#035900"); + NotfiyTextColour = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#91ff8e")); + NotifyIcon = $" \uE73E"; + NotifyText = $" {Message}"; + Notify = true; + } + } - public static SettingsViewModel Instance { get; private set; } + #endregion - // RomM client API tokens are "rmm_" + 64 lowercase hex chars (secrets.token_hex(32) on the server). - private static readonly Regex ApiTokenPattern = new Regex(@"^rmm_[0-9a-f]{64}$", RegexOptions.Compiled); + public string RomMHost + { + get => _romMHost; + set + { + if(value.Length == 0) + { + _romMHost = ""; + } + else + { + _romMHost = value.TrimEnd('/'); + } + OnPropertyChanged(); + } + } + public string RomMClientToken + { + get => _romMClientToken; + set + { + _romMClientToken = value; + OnPropertyChanged(); + } + } + public static readonly Regex ApiTokenPattern = new Regex(@"^rmm_[0-9a-f]{64}$", RegexOptions.Compiled); - public static bool IsValidApiToken(string token) + public bool UseBasicAuth { - return !string.IsNullOrEmpty(token) && ApiTokenPattern.IsMatch(token); + get => _useBasicAuth; + set + { + _useBasicAuth = value; + OnPropertyChanged(); + } + } + public string RomMUsername + { + get => _romMUsername; + set + { + _romMUsername = value; + OnPropertyChanged(); + } + } + public string RomMPassword + { + get => _romMPassword; + set + { + _romMPassword = value; + OnPropertyChanged(); + } + } + public string RomMUser + { + get => _romMUser; + set + { + _romMUser = value; + OnPropertyChanged(); + } } - [JsonIgnore] - public bool HasAnyAuth => - IsValidApiToken(RomMApiToken?.Trim()) || - (!string.IsNullOrEmpty(RomMUsername) && !string.IsNullOrEmpty(RomMPassword)); + [JsonIgnore] public string ClientTokenURL + { + get => $"{RomMHost}/client-api-tokens"; + set { } + } + public string ServerVersion + { + get => _romMServerVersion; + set + { + _romMServerVersion = value; + OnPropertyChanged(); + } + } + public string ProfilePath + { + get => _profilepath; + set + { + _profilepath = value; + OnPropertyChanged(); + } + } + public string RomMProfileType + { + get => _profileType; + set + { + _profileType = value; + OnPropertyChanged(); + } + } + public bool ScanGamesInFullScreen { get; set; } = false; public bool NotifyOnInstallComplete { get; set; } = false; public bool KeepRomMSynced { get; set; } = false; - public string RomMHost { get; set; } = ""; - public string RomMUsername { get; set; } = ""; - public string RomMPassword { get; set; } = ""; - private string _romMApiToken = ""; - public string RomMApiToken + public bool Use7z { get; set; } = false; + public string PathTo7z { - get => _romMApiToken; + get => _7zPath; set { - if (_romMApiToken == value) return; - _romMApiToken = value; + _7zPath = value; + OnPropertyChanged(); + } + } + public bool MergeRevisions { get; set; } = false; + public bool KeepDeletedGames { get; set; } = false; + public string ExcludeGenres + { + get => _excludeGenres; + set + { + _excludeGenres = value; OnPropertyChanged(); - OnPropertyChanged(nameof(HasValidApiToken)); } } + public bool SkipMissingFiles { get; set; } = false; - [JsonIgnore] - public bool HasValidApiToken => IsValidApiToken(RomMApiToken?.Trim()); public ObservableCollection Mappings { get; set; } - public bool Use7z { get; set; } = false; - public string PathTo7z { get; set; } = ""; - public bool MergeRevisions { get; set; } = false; - - public SettingsViewModel() + public List RomMPlatforms { + get => _romMPlatforms; + set + { + if(value != null) + { + _romMPlatforms = value; + OnPropertyChanged(); + + foreach (var mapping in Mappings) + { + mapping.AvailablePlatforms = value; + } + } + } } + public SettingsViewModel(){} + internal SettingsViewModel(Plugin plugin, IRomM romM) { RomM = romM; @@ -78,20 +285,36 @@ internal SettingsViewModel(Plugin plugin, IRomM romM) bool forceSave = false; var savedSettings = plugin.LoadPluginSettings(); - if (savedSettings == null) { + if (savedSettings == null) + { forceSave = true; - } else { - ScanGamesInFullScreen = savedSettings.ScanGamesInFullScreen; - NotifyOnInstallComplete = savedSettings.NotifyOnInstallComplete; + } + else + { RomMHost = savedSettings.RomMHost; + RomMClientToken = savedSettings.RomMClientToken; RomMUsername = savedSettings.RomMUsername; RomMPassword = savedSettings.RomMPassword; - RomMApiToken = savedSettings.RomMApiToken ?? ""; + UseBasicAuth = savedSettings.UseBasicAuth; + + RomMUser = savedSettings.RomMUser; + RomMProfileType = savedSettings.RomMProfileType; + ProfilePath = savedSettings.ProfilePath; + ServerVersion = savedSettings.ServerVersion; + + // ----- These need to stay in this order ----- Mappings = savedSettings.Mappings; + RomMPlatforms = savedSettings.RomMPlatforms; + // -------------------------------------------- + KeepRomMSynced = savedSettings.KeepRomMSynced; + ScanGamesInFullScreen = savedSettings.ScanGamesInFullScreen; + NotifyOnInstallComplete = savedSettings.NotifyOnInstallComplete; Use7z = savedSettings.Use7z; PathTo7z = savedSettings.PathTo7z; MergeRevisions = savedSettings.MergeRevisions; + KeepDeletedGames = savedSettings.KeepDeletedGames; + ExcludeGenres = savedSettings.ExcludeGenres; } if (Mappings == null) @@ -112,6 +335,109 @@ internal SettingsViewModel(Plugin plugin, IRomM romM) } } + public bool TestConnection(bool UpdateNotificationBar = false) + { + Notify = false; + + try + { + if(string.IsNullOrEmpty(RomMHost)) + { + throw new ArgumentException("Host not set!"); + } + if(!Uri.IsWellFormedUriString(RomMHost, UriKind.RelativeOrAbsolute)) + { + throw new ArgumentException("Host is not a valid URL!"); + } + + if(UseBasicAuth) + { + if(string.IsNullOrEmpty(RomMUsername) || string.IsNullOrEmpty(RomMPassword)) + { + throw new ArgumentException("Username/Password not set!"); + } + + HttpClientSingleton.ConfigureBasicAuth(RomMUsername, RomMPassword); + } + else + { + if (string.IsNullOrEmpty(RomMClientToken)) + { + throw new ArgumentException("Client token not set!"); + } + + if(!ApiTokenPattern.IsMatch(RomMClientToken)) + { + throw new ArgumentException("Client token format invaild!"); + } + + HttpClientSingleton.ConfigureAPIAuth(RomMClientToken); + } + + // Check server is present + HttpResponseMessage response = HttpClientSingleton.Instance.GetAsync($"{RomMHost}/api/heartbeat", HttpCompletionOption.ResponseContentRead, new System.Threading.CancellationToken()).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + + Stream body = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); + + using (StreamReader reader = new StreamReader(body)) + { + var jsonResponse = JObject.Parse(reader.ReadToEnd()); + ServerInfo info = jsonResponse["SYSTEM"].ToObject(); + + ServerVersion = info.Version; + } + + // Get user info + response = HttpClientSingleton.Instance.GetAsync($"{RomMHost}/api/users/me", System.Net.Http.HttpCompletionOption.ResponseContentRead, new System.Threading.CancellationToken()).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + + body = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); + RomMUser userinfo; + + using (StreamReader reader = new StreamReader(body)) + { + var jsonResponse = JObject.Parse(reader.ReadToEnd()); + userinfo = jsonResponse.ToObject(); + } + + if (!string.IsNullOrEmpty(userinfo.IconPath)) + { + response = HttpClientSingleton.Instance.GetAsync($"{RomMHost}/api/raw/assets/{userinfo.IconPath}", System.Net.Http.HttpCompletionOption.ResponseContentRead, new System.Threading.CancellationToken()).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + var imagebytes = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); + File.WriteAllBytes($"{PlayniteAPI.Paths.ExtensionsDataPath}\\{RomM.Id.ToString()}\\avatar.png", imagebytes); + ProfilePath = $"{PlayniteAPI.Paths.ExtensionsDataPath}\\{RomM.Id.ToString()}\\avatar.png"; + } + else + { + ProfilePath = _defaultprofilepath; + } + + RomMProfileType = userinfo.Role; + RomMUser = userinfo.Username; + if(UpdateNotificationBar) + UpdateNotifcationBar("Authenticated!"); + } + catch (Exception ex) + { + Notify = true; + ProfilePath = _defaultprofilepath; + RomMUser = "----"; + RomMProfileType = "----"; + ServerVersion = "---"; + LogManager.GetLogger().Error($"Failed to read response! {ex}"); + + if (UpdateNotificationBar) + UpdateNotifcationBar($"Authentication failed: {ex.Message}", true); + + PlayniteAPI.Notifications.Add(new NotificationMessage($"RomMPlugin.Authentication.Failed.{ex.Message}", $"RomM - Authentication failed: {ex.Message}", NotificationType.Error)); + return false; + } + + return true; + } + public void BeginEdit() { // Code executed when settings view is opened and user starts editing values. @@ -130,7 +456,15 @@ public void EndEdit() // Code executed when user decides to confirm changes made since BeginEdit was called. // This method should save settings made to Option1 and Option2. SavePluginSettings(this); - HttpClientSingleton.ConfigureAuth(this); + if (UseBasicAuth) + { + HttpClientSingleton.ConfigureBasicAuth(RomMUsername, RomMPassword); + } + else + { + HttpClientSingleton.ConfigureAPIAuth(RomMClientToken); + } + } private void SavePluginSettings(SettingsViewModel settings) @@ -150,20 +484,17 @@ public bool VerifySettings(out List errors) { var mappingErrors = new List(); - if (!string.IsNullOrWhiteSpace(RomMApiToken) && !IsValidApiToken(RomMApiToken.Trim())) - { - mappingErrors.Add("API Token must start with 'rmm_' followed by 64 lowercase hex characters."); - } - Mappings.Where(m => m.Enabled)?.ForEach(m => { if (string.IsNullOrEmpty(m.DestinationPathResolved)) { mappingErrors.Add($"{m.MappingId}: No destination path specified."); + UpdateNotifcationBar($"{m.MappingId}: No destination path specified.", true); } else if (!Directory.Exists(m.DestinationPathResolved)) { mappingErrors.Add($"{m.MappingId}: Destination path doesn't exist ({m.DestinationPathResolved})."); + UpdateNotifcationBar($"{m.MappingId}: Destination path doesn't exist ({m.DestinationPathResolved}).", true); } }); @@ -171,4 +502,30 @@ public bool VerifySettings(out List errors) return errors.Count == 0; } } + + + // Used to load profile image into cache so it can be changed while the application is running + public class ImageCacheConverter : IValueConverter + { + public object Convert(object value, Type targetType, + object parameter, System.Globalization.CultureInfo culture) + { + + var path = (string)value; + var image = new BitmapImage(); + image.BeginInit(); + image.CacheOption = BitmapCacheOption.OnLoad; + image.UriSource = new Uri(path); + image.EndInit(); + + return image; + + } + + public object ConvertBack(object value, Type targetType, + object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException("Not implemented."); + } + } } diff --git a/Settings/SettingsView.xaml b/Settings/SettingsView.xaml index bdcd95b..56200fa 100644 --- a/Settings/SettingsView.xaml +++ b/Settings/SettingsView.xaml @@ -1,206 +1,280 @@ - + xmlns:draw="clr-namespace:System.Drawing;assembly=System.Drawing" + xmlns:sys="clr-namespace:System;assembly=mscorlib" mc:Ignorable="d" d:DesignHeight="1000" d:DesignWidth="800" Padding="2,0,2,4"> + - - + + + - - - + + + + + diff --git a/Settings/SettingsView.xaml.cs b/Settings/SettingsView.xaml.cs index e81c877..2f6d364 100644 --- a/Settings/SettingsView.xaml.cs +++ b/Settings/SettingsView.xaml.cs @@ -1,9 +1,14 @@ -using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Playnite.SDK; +using RomM.Models.RomM; +using RomM.Models.RomM.Platform; +using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; -using System.Net; using System.Net.Http; -using System.Threading; using System.Windows; using System.Windows.Controls; @@ -18,104 +23,82 @@ public SettingsView() InitializeComponent(); } - private void Click_Delete(object sender, RoutedEventArgs e) + private void Click_TestConnection(object sender, RoutedEventArgs e) { - if (((FrameworkElement)sender).DataContext is EmulatorMapping mapping) + SettingsViewModel.Instance.TestConnection(true); + e.Handled = true; + } + + private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) + { + try { - var res = SettingsViewModel.Instance.PlayniteAPI.Dialogs.ShowMessage(string.Format("Delete this mapping?\r\n\r\n{0}", mapping.GetDescriptionLines().Aggregate((a, b) => $"{a}{Environment.NewLine}{b}")), "Confirm delete", MessageBoxButton.YesNo); - if (res == MessageBoxResult.Yes) + if (e.Uri.Scheme == Uri.UriSchemeHttp || e.Uri.Scheme == Uri.UriSchemeHttps) { - SettingsViewModel.Instance.Mappings.Remove(mapping); + var psi = new ProcessStartInfo + { + FileName = e.Uri.AbsoluteUri, + UseShellExecute = true + }; + Process.Start(psi); } } - } - - private void Click_BrowseDestination(object sender, RoutedEventArgs e) - { - var mapping = ((FrameworkElement)sender).DataContext as EmulatorMapping; - string path; - if ((path = GetSelectedFolderPath()) == null) return; - var playnite = SettingsViewModel.Instance.PlayniteAPI; - if (playnite.Paths.IsPortable) + catch (Exception ex) { - path = path.Replace(playnite.Paths.ApplicationPath, Playnite.SDK.ExpandableVariables.PlayniteDirectory); + System.Diagnostics.Debug.WriteLine($"Failed to open URL: {ex.Message}"); } - - mapping.DestinationPath = path; + e.Handled = true; } - private async void Click_TestConnection(object sender, RoutedEventArgs e) + private async void Click_PullPlatforms(object sender, RoutedEventArgs e) { - var settings = SettingsViewModel.Instance; - var dialogs = settings.PlayniteAPI.Dialogs; + SettingsViewModel.Instance.Notify = false; - var host = settings.RomMHost?.Trim().TrimEnd('/'); - if (string.IsNullOrWhiteSpace(host)) + try { - dialogs.ShowMessage("RomM Host is empty.", "RomM"); - return; - } + HttpResponseMessage response = await HttpClientSingleton.Instance.GetAsync($"{SettingsViewModel.Instance.RomMHost}/api/platforms"); + response.EnsureSuccessStatusCode(); - if (!settings.HasAnyAuth) + string body = await response.Content.ReadAsStringAsync(); + SettingsViewModel.Instance.RomMPlatforms = JsonConvert.DeserializeObject>(body); + SettingsViewModel.Instance.UpdateNotifcationBar("Platforms successfully retrieved!"); + } + catch (Exception ex) { - dialogs.ShowMessage("Provide either a valid API Token or username and password.", "RomM"); - return; + LogManager.GetLogger().Error($"RomM - failed to get platforms: {ex}"); + SettingsViewModel.Instance.UpdateNotifcationBar($"Failed to get platforms: {ex.Message}!", true); } + } - var button = (Button)sender; - var originalContent = button.Content; + private void Click_AddMapping(object sender, RoutedEventArgs e) + { + SettingsViewModel.Instance.Mappings.Add(new EmulatorMapping(SettingsViewModel.Instance.RomMPlatforms)); + } - try + private void Click_Delete(object sender, RoutedEventArgs e) + { + if (((FrameworkElement)sender).DataContext is EmulatorMapping mapping) { - button.IsEnabled = false; - button.Content = "Testing..."; - - using (var req = new HttpRequestMessage(HttpMethod.Get, $"{host}/api/users/me")) + var res = SettingsViewModel.Instance.PlayniteAPI.Dialogs.ShowMessage(string.Format("Delete this mapping?\r\n\r\n{0}", mapping.GetDescriptionLines().Aggregate((a, b) => $"{a}{Environment.NewLine}{b}")), "Confirm delete", MessageBoxButton.YesNo); + if (res == MessageBoxResult.Yes) { - req.Headers.Authorization = HttpClientSingleton.BuildAuthHeader(settings); - using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))) - using (var resp = await HttpClientSingleton.Instance.SendAsync(req, cts.Token)) - { - if (resp.IsSuccessStatusCode) - { - dialogs.ShowMessage($"Connection successful ({(int)resp.StatusCode}).", "RomM"); - } - else if (resp.StatusCode == HttpStatusCode.Unauthorized || resp.StatusCode == HttpStatusCode.Forbidden) - { - dialogs.ShowMessage($"Authentication rejected (HTTP {(int)resp.StatusCode}). Check your API token or username/password.", "RomM"); - } - else - { - dialogs.ShowMessage($"Connection failed: HTTP {(int)resp.StatusCode} {resp.ReasonPhrase}", "RomM"); - } - } + SettingsViewModel.Instance.Mappings.Remove(mapping); } } - catch (OperationCanceledException) - { - dialogs.ShowMessage("Connection timed out after 10 seconds.", "RomM"); - } - catch (HttpRequestException ex) - { - dialogs.ShowMessage($"Connection failed: {ex.Message}", "RomM"); - } - catch (Exception ex) - { - dialogs.ShowMessage($"Unexpected error: {ex.Message}", "RomM"); - } - finally - { - button.IsEnabled = true; - button.Content = originalContent; - } } - private void Click_Browse7zDestination(object sender, RoutedEventArgs e) + private void Click_BrowseDestination(object sender, RoutedEventArgs e) { + var mapping = ((FrameworkElement)sender).DataContext as EmulatorMapping; string path; - if ((path = SettingsViewModel.Instance.PlayniteAPI.Dialogs.SelectFile("7Zip Executable|7z.exe")) == null) return; + if ((path = GetSelectedFolderPath()) == null) return; + var playnite = SettingsViewModel.Instance.PlayniteAPI; + if (playnite.Paths.IsPortable) + { + path = path.Replace(playnite.Paths.ApplicationPath, Playnite.SDK.ExpandableVariables.PlayniteDirectory); + } - SettingsViewModel.Instance.PathTo7z = path; + mapping.DestinationPath = path; } private static string GetSelectedFolderPath() @@ -143,27 +126,15 @@ private void DataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventA private void DataGrid_CurrentCellChanged(object sender, EventArgs e) { - + } - private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) + private void Click_Browse7zDestination(object sender, RoutedEventArgs e) { - try - { - if (e.Uri.Scheme == Uri.UriSchemeHttp || e.Uri.Scheme == Uri.UriSchemeHttps) - { - var psi = new ProcessStartInfo - { - FileName = e.Uri.AbsoluteUri, - UseShellExecute = true - }; - Process.Start(psi); - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"Failed to open URL: {ex.Message}"); - } + string path; + if ((path = SettingsViewModel.Instance.PlayniteAPI.Dialogs.SelectFile("7Zip Executable|7z.exe")) == null) return; + + SettingsViewModel.Instance.PathTo7z = path; e.Handled = true; } } diff --git a/profile.png b/profile.png new file mode 100644 index 0000000..883f997 Binary files /dev/null and b/profile.png differ