using HtmlAgilityPack; using LANCommander.SDK; using Newtonsoft.Json; using System; using System.Collections; using System.Collections.Generic; using System.Net.Http; using System.Reflection; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; namespace LANCommander.PCGamingWiki { public class PCGamingWikiClient { #region Internal models public class GetPageIdModel { public class Cargoquery { public class Title { public string PageID { get; set; } public string Page { get; set; } [JsonProperty("Steam AppID")] public string SteamAppID { get; set; } } public Title title { get; set; } } public List cargoquery { get; } = new List(); } public class GetPageContentModel { public class Parse { public class Text { [JsonProperty("*")] public string content { get; set; } } public string title { get; set; } public int pageid { get; set; } public Text text { get; set; } } public Parse parse { get; set; } } #endregion private HttpClient Client; public PCGamingWikiClient() { Client = new HttpClient(); Client.BaseAddress = new Uri("https://www.pcgamingwiki.com/"); Client.DefaultRequestHeaders.UserAgent.ParseAdd($"{AssemblyName.GetAssemblyName(Assembly.GetExecutingAssembly().Location).Name}/{Assembly.GetExecutingAssembly().GetName().Version.ToString()}"); } public async Task Search(string keyword) { var results = await Client.GetStringAsync($"w/api.php?action=opensearch&format=json&formatversion=2&search={HttpUtility.UrlEncode(keyword)}&limit=1"); string pattern = @"(https:\/\/www.pcgamingwiki.com.+)"""; RegexOptions options = RegexOptions.Multiline; var match = Regex.Match(results, pattern, options); if (match.Success && match.Groups.Count == 2) { return new Uri(match.Groups[1].Value); } return null; } public Dictionary GetMultiplayerPlayerCounts(Uri url) { if (url == null) return []; HtmlWeb web = new(); HtmlDocument dom = web.Load(url); return GetMultiplayerPlayerCounts(dom); } public async Task> GetMultiplayerPlayerCounts(string keyword) { var data = await SearchAndParseMultiplayerPlayerData(keyword, info => info.ToDictionary(x => x.Key, y => y.Value.PlayerCount)); return data ?? []; } public async Task> GetMultiplayerPlayerInfo(string keyword) { var data = await SearchAndParseMultiplayerPlayerData(keyword, info => info.Values.ToArray()); return data ?? []; } protected async Task SearchAndParseMultiplayerPlayerData(string keyword, Func, T> dataPropagateFunction) where T : class { var pageId = await GetPageIDAsync(keyword); if (pageId != null) { var pageContent = await GetPageContentAsync(pageId); var dom = new HtmlDocument(); dom.LoadHtml(pageContent ?? string.Empty); var data = ParseMultiplayerPlayerInfo(dom); return dataPropagateFunction.Invoke(data); } return null; } protected Dictionary GetMultiplayerPlayerCounts(HtmlDocument dom) { return ParseMultiplayerPlayerInfo(dom).ToDictionary(x => x.Key, y => y.Value.PlayerCount); } protected Dictionary ParseMultiplayerPlayerInfo(HtmlDocument dom) { ArgumentNullException.ThrowIfNull(dom); HtmlNode multiplayerTable = dom.GetElementbyId("table-network-multiplayer"); if (multiplayerTable == null) return []; var results = new Dictionary(StringComparer.OrdinalIgnoreCase); var multiplayerRows = multiplayerTable.SelectNodes(".//tr[contains(@class, 'table-network-multiplayer-body-row')]"); var multiplayerAbbrs = multiplayerTable.SelectNodes(".//abbr"); var multiplayerCounts = multiplayerTable.SelectNodes(".//td[contains(@class, 'table-network-multiplayer-body-players')]"); var multiplayerNotes = multiplayerTable.SelectNodes(".//td[contains(@class, 'table-network-multiplayer-body-notes')]"); foreach (var row in multiplayerRows) { var abbr_node = row.SelectNodes(".//abbr"); var count_node = row.SelectNodes(".//td[contains(@class, 'table-network-multiplayer-body-players')]"); var notes_node = row.SelectNodes(".//td[contains(@class, 'table-network-multiplayer-body-notes')]"); if (abbr_node == null || count_node == null) continue; var type = abbr_node[0].InnerText; var players = count_node[0].InnerText; var notes = null as string; if (notes_node.Count == 1 && notes_node[0]?.ChildNodes.Count > 0) { var notelines = notes_node.FirstOrDefault()?.ChildNodes .Select(x => x.InnerText) .Select(x => x.TrimEnd('.')); notes = string.Join("\n", notelines ?? []); notes = notes.Replace("\n\n", ". "); notes = notes.ReplaceLineEndings(""); notes = HttpUtility.HtmlDecode(notes); } if (type == null) continue; results.TryAdd(type, new MultiplayerInfo { MultiplayerType = type }); if (Int32.TryParse(players, out int playerCount)) { results[type].PlayerCount = playerCount; } if (!string.IsNullOrEmpty(notes)) { results[type].Notes = notes; } } return results; } public async Task GetPageIDAsync(string keyword) { try { string getPageIdUrl = $"https://www.pcgamingwiki.com/w/api.php?action=cargoquery&format=json&tables=Infobox_game&fields=Infobox_game._pageID%3DPageID%2C_pageName%3DPage%2CSteam_AppID&where=Infobox_game._pageName%3D%22{HttpUtility.UrlEncode(keyword)}%22&formatversion=1"; // HttpClient.GetFromJsonAsync<> does not seem to deserialize list properly (at least with the usedmodel) // read json and parse via Newtonsoft lib string getPageIdJson = await Client.GetStringAsync(getPageIdUrl); var pageId = JsonConvert.DeserializeObject(getPageIdJson); if (pageId == null || pageId.cargoquery == null || pageId.cargoquery.Count != 1) return null; var title = pageId.cargoquery[0]?.title; return title?.PageID; } catch (Exception ex) { // TODO: add logging //Logger?.LogError(ex, $"Could not parse page id from PCGamingWiki for retrieving page with keyword {keyword}"); } return null; } public async Task GetPageContentAsync(string pageId) { try { string getPageContentUrl = $"https://www.pcgamingwiki.com/w/api.php?action=parse&format=json&prop=text&pageid={HttpUtility.UrlEncode(pageId)}"; // HttpClient.GetFromJsonAsync<> does not seem to deserialize list properly (at least with the usedmodel) // read json and parse via Newtonsoft lib string getPageContentJson = await Client.GetStringAsync(getPageContentUrl); var pageContent = JsonConvert.DeserializeObject(getPageContentJson); return pageContent?.parse?.text?.content; } catch (Exception ex) { // TODO: add logging //Logger?.LogError(ex, $"Could not parse page content from PCGamingWiki for retrieving page with id {pageId}"); } return null; } } public record MultiplayerInfo() { public required string MultiplayerType { get; set; } public int PlayerCount { get; set; } public string? Notes { get; set; } } }