mirror of
https://github.com/rommapp/playnite-plugin.git
synced 2026-01-09 06:33:15 +08:00
462 lines
19 KiB
C#
462 lines
19 KiB
C#
using Playnite.SDK;
|
|
using Playnite.SDK.Models;
|
|
using Playnite.SDK.Plugins;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Controls;
|
|
using System.Web;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Collections.Specialized;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using System.Linq;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using RomM.Settings;
|
|
using Playnite.SDK.Events;
|
|
using RomM.Games;
|
|
using RomM.Models.RomM.Platform;
|
|
using RomM.Models.RomM.Rom;
|
|
|
|
|
|
namespace RomM
|
|
{
|
|
public static class HttpClientSingleton
|
|
{
|
|
private static readonly HttpClient httpClient = new HttpClient();
|
|
|
|
static HttpClientSingleton()
|
|
{
|
|
httpClient.DefaultRequestHeaders.Accept.Clear();
|
|
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
|
}
|
|
|
|
public static void ConfigureBasicAuth(string username, string password)
|
|
{
|
|
var base64Credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}"));
|
|
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64Credentials);
|
|
}
|
|
|
|
public static HttpClient Instance
|
|
{
|
|
get { return httpClient; }
|
|
}
|
|
}
|
|
|
|
public static class JsonSerializerSingleton
|
|
{
|
|
public static JsonSerializer Instance { get; } = new JsonSerializer();
|
|
}
|
|
|
|
public class RomM : LibraryPlugin, IRomM
|
|
{
|
|
private const string s_pluginName = "RomM";
|
|
|
|
internal static readonly string Icon = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), @"icon.png");
|
|
internal static readonly Guid PluginId = Guid.Parse("9700aa21-447d-41b4-a989-acd38f407d9f");
|
|
internal static readonly MetadataNameProperty SourceName = new MetadataNameProperty(s_pluginName);
|
|
|
|
public override Guid Id { get; } = PluginId;
|
|
public override string Name { get; } = s_pluginName;
|
|
public override string LibraryIcon { get; } = Icon;
|
|
|
|
public ILogger Logger => LogManager.GetLogger();
|
|
public IPlayniteAPI Playnite { get; private set; }
|
|
public SettingsViewModel Settings { get; private set; }
|
|
|
|
// Implementing Client adds ability to open it via special menu in playnite.
|
|
public override LibraryClient Client { get; } = new RomMClient();
|
|
|
|
public RomM(IPlayniteAPI api) : base(api)
|
|
{
|
|
Playnite = api;
|
|
Properties = new LibraryPluginProperties
|
|
{
|
|
HasSettings = true
|
|
};
|
|
}
|
|
|
|
internal IList<RomMPlatform> FetchPlatforms()
|
|
{
|
|
string apiPlatformsUrl = $"{Settings.RomMHost}/api/platforms";
|
|
try
|
|
{
|
|
// Make the request and get the response
|
|
HttpResponseMessage response = HttpClientSingleton.Instance.GetAsync(apiPlatformsUrl).GetAwaiter().GetResult();
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
// Assuming the response is in JSON format
|
|
string body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
|
return JsonConvert.DeserializeObject<List<RomMPlatform>>(body);
|
|
}
|
|
catch (HttpRequestException e)
|
|
{
|
|
Logger.Error($"Request exception: {e.Message}");
|
|
return new List<RomMPlatform>();
|
|
}
|
|
}
|
|
|
|
internal RomMRom FetchRom(string romId)
|
|
{
|
|
string romUrl = $"{Settings.RomMHost}/api/roms/{romId}";
|
|
try
|
|
{
|
|
// Fetch the rom info from RomM
|
|
HttpResponseMessage response = HttpClientSingleton.Instance.GetAsync(romUrl).GetAwaiter().GetResult();
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
// Assuming the response is in JSON format
|
|
string body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
|
return JsonConvert.DeserializeObject<RomMRom>(body);
|
|
}
|
|
catch (HttpRequestException e)
|
|
{
|
|
Logger.Error($"Request exception: {e.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Playnite url is in the format playnite://romm/<action>/<platform_igdb_id>/<rom_id>
|
|
internal void HandleRommUri(PlayniteUriEventArgs args)
|
|
{
|
|
var action = args.Arguments[0];
|
|
var platformIgdbId = args.Arguments[1];
|
|
var romId = args.Arguments[2];
|
|
|
|
Logger.Debug($"Received Playnite URI: {action}/{platformIgdbId}/{romId}");
|
|
|
|
string romUrl = $"{Settings.RomMHost}/api/roms/{romId}";
|
|
RomMRom rom = FetchRom(romId);
|
|
|
|
if (rom == null)
|
|
{
|
|
Logger.Warn($"Game {romId} not found in RomM.");
|
|
return;
|
|
}
|
|
|
|
foreach (var mapping in SettingsViewModel.Instance.Mappings?.Where(m => m.Enabled))
|
|
{
|
|
if (mapping.Platform.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.Name == gameName);
|
|
|
|
if (game == null)
|
|
{
|
|
Logger.Warn($"Game {gameName} not found in Playnite database.");
|
|
}
|
|
|
|
PlayniteApi.MainView.SwitchToLibraryView();
|
|
PlayniteApi.MainView.SelectGame(game.Id);
|
|
|
|
switch (action)
|
|
{
|
|
case "view":
|
|
// We always open the game in the webview
|
|
return;
|
|
case "play":
|
|
PlayniteApi.StartGame(game.Id);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void OnApplicationStarted(OnApplicationStartedEventArgs args)
|
|
{
|
|
base.OnApplicationStarted(args);
|
|
Settings = new SettingsViewModel(this, this);
|
|
HttpClientSingleton.ConfigureBasicAuth(Settings.RomMUsername, Settings.RomMPassword);
|
|
Playnite.UriHandler.RegisterSource("romm", HandleRommUri);
|
|
}
|
|
|
|
public static async Task<HttpResponseMessage> GetAsync(string baseUrl, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
|
{
|
|
return await HttpClientSingleton.Instance.GetAsync(baseUrl, completionOption);
|
|
}
|
|
|
|
public static async Task<HttpResponseMessage> 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<GameMetadata> GetGames(LibraryGetGamesArgs args)
|
|
{
|
|
if (Playnite.ApplicationInfo.Mode == ApplicationMode.Fullscreen && !Settings.ScanGamesInFullScreen)
|
|
{
|
|
return new List<GameMetadata>();
|
|
}
|
|
|
|
// Return early if host, username or password is not set
|
|
if (string.IsNullOrEmpty(Settings.RomMHost) || string.IsNullOrEmpty(Settings.RomMUsername) || string.IsNullOrEmpty(Settings.RomMPassword))
|
|
{
|
|
Logger.Warn("RomM host, username or password is not set.");
|
|
return new List<GameMetadata>();
|
|
}
|
|
|
|
IList<RomMPlatform> apiPlatforms = FetchPlatforms();
|
|
List<GameMetadata> games = new List<GameMetadata>();
|
|
IEnumerable<EmulatorMapping> enabledMappings = SettingsViewModel.Instance.Mappings?.Where(m => m.Enabled);
|
|
|
|
if (enabledMappings == null || !enabledMappings.Any())
|
|
{
|
|
Logger.Warn("No emulators are configured or enabled in RomM settings. No games will be fetched.");
|
|
return games;
|
|
}
|
|
|
|
foreach (var mapping in enabledMappings)
|
|
{
|
|
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)
|
|
{
|
|
Logger.Warn($"Platform {mapping.PlatformId} not found, skipping.");
|
|
continue;
|
|
}
|
|
|
|
string url = $"{Settings.RomMHost}/api/roms";
|
|
RomMPlatform apiPlatform = apiPlatforms.FirstOrDefault(p => p.IgdbId == mapping.Platform.IgdbId);
|
|
|
|
if (apiPlatform == null)
|
|
{
|
|
Logger.Warn($"Platform {mapping.Platform.Name} with IGDB ID {mapping.Platform.IgdbId} not found in RomM, skipping.");
|
|
continue;
|
|
}
|
|
|
|
Logger.Debug($"Starting to fetch games for {apiPlatform.Name}.");
|
|
|
|
const int pageSize = 72;
|
|
int offset = 0;
|
|
bool hasMoreData = true;
|
|
var allRoms = new List<RomMRom>();
|
|
var responseGameIDs = new HashSet<string>();
|
|
|
|
while (hasMoreData)
|
|
{
|
|
if (args.CancelToken.IsCancellationRequested)
|
|
break;
|
|
|
|
NameValueCollection queryParams = new NameValueCollection
|
|
{
|
|
{ "limit", pageSize.ToString() },
|
|
{ "offset", offset.ToString() },
|
|
{ "platform_id", apiPlatform.Id.ToString() },
|
|
{ "order_by", "name" },
|
|
{ "order_dir", "asc" },
|
|
};
|
|
|
|
try
|
|
{
|
|
// Make the request and get the response
|
|
HttpResponseMessage response = GetAsyncWithParams(url, queryParams).GetAwaiter().GetResult();
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
Logger.Debug($"Parsing response for {apiPlatform.Name} batch {offset / pageSize + 1}.");
|
|
|
|
// Assuming the response is in JSON format
|
|
Stream body = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
|
|
List<RomMRom> roms;
|
|
using (StreamReader reader = new StreamReader(body))
|
|
using (JsonTextReader jsonReader = new JsonTextReader(reader))
|
|
{
|
|
var jsonResponse = JObject.Parse(reader.ReadToEnd());
|
|
roms = jsonResponse["items"].ToObject<List<RomMRom>>();
|
|
}
|
|
|
|
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;
|
|
}
|
|
catch (HttpRequestException e)
|
|
{
|
|
Logger.Error($"Request exception: {e.Message}");
|
|
hasMoreData = false;
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
Logger.Debug($"Finished parsing response for {apiPlatform.Name}.");
|
|
|
|
var rootInstallDir = PlayniteApi.Paths.IsPortable
|
|
? mapping.DestinationPathResolved.Replace(PlayniteApi.Paths.ApplicationPath, ExpandableVariables.PlayniteDirectory)
|
|
: mapping.DestinationPathResolved;
|
|
|
|
// Return a GameMetadata for each item in the response
|
|
foreach (var item in allRoms)
|
|
{
|
|
if (args.CancelToken.IsCancellationRequested)
|
|
break;
|
|
|
|
var gameName = item.Name;
|
|
var fileName = item.FileName;
|
|
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 = $"{Settings.RomMHost}/api/roms/{item.Id}/content/{fileName}",
|
|
IsMulti = item.Multi
|
|
};
|
|
var gameId = info.AsGameId();
|
|
responseGameIDs.Add(gameId);
|
|
|
|
// Check if the game is already installed
|
|
if (Playnite.Database.Games.Any(g => g.GameId == gameId))
|
|
{
|
|
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<GameRom> { new GameRom(gameNameWithTags, pathToGame) },
|
|
InstallDirectory = gameInstallDir,
|
|
IsInstalled = File.Exists(pathToGame),
|
|
GameId = gameId,
|
|
Platforms = new HashSet<MetadataProperty> { new MetadataNameProperty(mapping.Platform.Name ?? "") },
|
|
Regions = new HashSet<MetadataProperty>(item.Regions.Where(r => !string.IsNullOrEmpty(r)).Select(r => new MetadataNameProperty(r.ToString()))),
|
|
InstallSize = item.FileSizeBytes,
|
|
Description = item.Summary,
|
|
Icon = !string.IsNullOrEmpty(urlCover) ? new MetadataFile(urlCover) : null,
|
|
GameActions = new List<GameAction>
|
|
{
|
|
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 = $"{Settings.RomMHost}/rom/{item.Id}",
|
|
IsPlayAction = false
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
Logger.Debug($"Finished adding new games for {apiPlatform.Name}");
|
|
|
|
// Find games in the database that are not in the response
|
|
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;
|
|
|
|
if (responseGameIDs.Contains(game.GameId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Remove from the playnite database
|
|
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;
|
|
}
|
|
|
|
|
|
public override ISettings GetSettings(bool firstRunSettings)
|
|
{
|
|
return Settings;
|
|
}
|
|
|
|
public override UserControl GetSettingsView(bool firstRunSettings)
|
|
{
|
|
return new SettingsView();
|
|
}
|
|
|
|
public override IEnumerable<InstallController> GetInstallActions(GetInstallActionsArgs args)
|
|
{
|
|
if (args.Game.PluginId == Id)
|
|
{
|
|
yield return args.Game.GetRomMGameInfo().GetInstallController(args.Game, this);
|
|
}
|
|
}
|
|
|
|
public override IEnumerable<UninstallController> GetUninstallActions(GetUninstallActionsArgs args)
|
|
{
|
|
if (args.Game.PluginId == Id)
|
|
{
|
|
yield return args.Game.GetRomMGameInfo().GetUninstallController(args.Game, this);
|
|
}
|
|
}
|
|
|
|
public override void OnGameInstalled(OnGameInstalledEventArgs args)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|