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;
+ }
+
+
+}