diff --git a/Source/LinkUtilities.csproj b/Source/LinkUtilities.csproj index 8cf38ebcb..e426fea0c 100644 --- a/Source/LinkUtilities.csproj +++ b/Source/LinkUtilities.csproj @@ -38,6 +38,7 @@ + @@ -59,6 +60,7 @@ + @@ -71,6 +73,7 @@ + diff --git a/Source/Linker/Libraries/LibraryLinkGog.cs b/Source/Linker/Libraries/LibraryLinkGog.cs index e2de6eed0..5546f6643 100644 --- a/Source/Linker/Libraries/LibraryLinkGog.cs +++ b/Source/Linker/Libraries/LibraryLinkGog.cs @@ -10,7 +10,7 @@ namespace LinkUtilities.Linker { /// - /// Adds a link to the gog page of the game, if it is part of the steam library. + /// Adds a link to GOG. /// class LibraryLinkGog : LinkAndLibrary { diff --git a/Source/Linker/Libraries/LibraryLinkItch.cs b/Source/Linker/Libraries/LibraryLinkItch.cs index 0c332a8ba..f11b61643 100644 --- a/Source/Linker/Libraries/LibraryLinkItch.cs +++ b/Source/Linker/Libraries/LibraryLinkItch.cs @@ -10,7 +10,7 @@ namespace LinkUtilities.Linker { /// - /// Adds a link to the itch.io page of the game, if it is part of the steam library. + /// Adds a link to itch.io. /// class LibraryLinkItch : LinkAndLibrary { diff --git a/Source/Linker/Libraries/LibraryLinkSteam.cs b/Source/Linker/Libraries/LibraryLinkSteam.cs index bec12442c..788e561ee 100644 --- a/Source/Linker/Libraries/LibraryLinkSteam.cs +++ b/Source/Linker/Libraries/LibraryLinkSteam.cs @@ -10,7 +10,7 @@ namespace LinkUtilities.Linker { /// - /// Adds a link to the steam page of the game, if it is part of the steam library. + /// Adds a link to Steam. /// class LibraryLinkSteam : LinkAndLibrary { diff --git a/Source/Linker/LinkSources/LinkEpic.cs b/Source/Linker/LinkSources/LinkEpic.cs new file mode 100644 index 000000000..345238511 --- /dev/null +++ b/Source/Linker/LinkSources/LinkEpic.cs @@ -0,0 +1,104 @@ +using LinkUtilities.Helper; +using LinkUtilities.Models; +using LinkUtilities.Models.Epic; +using Playnite.SDK; +using Playnite.SDK.Models; +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; + +namespace LinkUtilities.Linker +{ + class LinkEpic : Link + { + public override string LinkName { get; } = "Epic"; + public override string BaseUrl { get; } = "https://store.epicgames.com/en-US/p/"; + public override string SearchUrl { get; } = "https://www.epicgames.com/graphql?query={Catalog{searchStore(keywords:%22{SearchString}%22,category:%22games/edition%22,effectiveDate:%22[1900-01-01,{DateUntil}]%22,count:100){elements{title%20urlSlug%20seller{name}}}}}"; + + private readonly string CheckUrl = "https://store-content-ipv4.ak.epicgames.com/api/en-US/content/products/"; + + public override string GetGamePath(Game game) + { + // Epic Links need the game name in lowercase without special characters and underscores instead of white spaces. + return game.Name.RemoveDiacritics() + .RemoveSpecialChars() + .CollapseWhitespaces() + .Replace(" ", "-") + .ToLower(); + } + + public override bool AddLink(Game game) + { + // Unfortunately Epic returns the status code forbidden, when trying to check the url, because they want cookies and + // javascipt active. Fortunately we can use the game slug in the store api. If it doesn't return an error, there should also + // be a link with that slug. + string gameSlug = GetGamePath(game); + string url = $"{CheckUrl}{gameSlug}"; + + WebClient client = new WebClient() { Encoding = Encoding.UTF8 }; + client.Headers.Add("Accept", "application/json"); + + try + { + string _ = client.DownloadString(url); + LinkUrl = $"{BaseUrl}{gameSlug}"; + return LinkHelper.AddLink(game, LinkName, LinkUrl, Settings); + } + catch + { + LinkUrl = string.Empty; + return false; + } + } + + public override List SearchLink(string searchTerm) + { + SearchResults.Clear(); + + try + { + WebClient client = new WebClient() { Encoding = Encoding.UTF8 }; + + client.Headers.Add("Accept", "application/json"); + + // We use replace instead of format, because the url already contains several braces. + string url = SearchUrl + .Replace("{SearchString}", searchTerm.UrlEncode()) + .Replace("{DateUntil}", DateTime.Now.AddDays(5).ToString("yyyy-MM-dd")); + + string jsonResult = client.DownloadString(url); + + EpicSearchResult epicSearchResult = Newtonsoft.Json.JsonConvert.DeserializeObject(jsonResult); + + int counter = 0; + + foreach (Element element in epicSearchResult.Data.Catalog.SearchStore.Elements) + { + counter++; + + if (!string.IsNullOrEmpty(element.UrlSlug)) + { + SearchResults.Add(new SearchResult + { + Name = $"{counter}. {element.Title}", + Url = $"{BaseUrl}{element.UrlSlug}", + Description = element.Seller.Name + } + ); + } + } + } + catch (Exception ex) + { + Log.Error(ex, $"Error loading data from {LinkName}"); + } + + return base.SearchLink(searchTerm); + } + + public LinkEpic(LinkUtilitiesSettings settings) : base(settings) + { + } + } +} diff --git a/Source/Linker/LinkSources/LinkHG101.cs b/Source/Linker/LinkSources/LinkHG101.cs index 398d87b31..ae0baf5b6 100644 --- a/Source/Linker/LinkSources/LinkHG101.cs +++ b/Source/Linker/LinkSources/LinkHG101.cs @@ -10,7 +10,7 @@ namespace LinkUtilities.Linker { /// - /// Adds a link to MobyGames. + /// Adds a link to Hardcore Gaming 101. /// class LinkHG101 : Link { diff --git a/Source/Linker/LinkSources/LinkMetacritic.cs b/Source/Linker/LinkSources/LinkMetacritic.cs index 61dd50c33..fc73e8d40 100644 --- a/Source/Linker/LinkSources/LinkMetacritic.cs +++ b/Source/Linker/LinkSources/LinkMetacritic.cs @@ -8,7 +8,7 @@ namespace LinkUtilities.Linker { /// - /// Adds a link to MobyGames. + /// Adds a link to Metacritic. /// class LinkMetacritic : Link { diff --git a/Source/Linker/Links.cs b/Source/Linker/Links.cs index 29cdcde2e..2c26e0e86 100644 --- a/Source/Linker/Links.cs +++ b/Source/Linker/Links.cs @@ -9,6 +9,7 @@ public class Links : List { public Links(LinkUtilitiesSettings settings) { + Add(new LinkEpic(settings)); Add(new LibraryLinkGog(settings)); Add(new LinkHG101(settings)); if (!string.IsNullOrWhiteSpace(settings.ItchApiKey)) diff --git a/Source/Logging.cs b/Source/Logging.cs index 75d3d46c5..02caac8b1 100644 --- a/Source/Logging.cs +++ b/Source/Logging.cs @@ -31,7 +31,7 @@ public static void Error(Exception ex, string Message = "", bool showDialog = fa Message = $"Error on {traceInfos.InitialCaller}()"; } - Message += $"{Message}|{traceInfos.FileName}|{traceInfos.LineNumber}"; + Message += $"|{traceInfos.FileName}|{traceInfos.LineNumber}"; logger.Error(ex, $"{Message}"); diff --git a/Source/Models/EpicSearchResult.cs b/Source/Models/EpicSearchResult.cs new file mode 100644 index 000000000..355a02dca --- /dev/null +++ b/Source/Models/EpicSearchResult.cs @@ -0,0 +1,57 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +// Contains all the classes needed to deserialize the JSON fetched from the Epic graphql api. +namespace LinkUtilities.Models.Epic +{ + public class Catalog + { + [JsonProperty("searchStore")] + public SearchStore SearchStore; + } + + public class Data + { + [JsonProperty("Catalog")] + public Catalog Catalog; + } + + public class Element + { + [JsonProperty("title")] + public string Title; + + [JsonProperty("urlSlug")] + public string UrlSlug; + + [JsonProperty("seller")] + public Seller Seller; + } + + public class Extensions + { + } + + public class EpicSearchResult + { + [JsonProperty("data")] + public Data Data; + + [JsonProperty("extensions")] + public Extensions Extensions; + } + + public class SearchStore + { + [JsonProperty("elements")] + public List Elements; + } + + public class Seller + { + [JsonProperty("name")] + public string Name; + } + + +}