Skip to content

Commit

Permalink
Add Sub ID finding function for free Steam DLCs.
Browse files Browse the repository at this point in the history
  • Loading branch information
azhuge233 committed Feb 12, 2024
1 parent 89357e1 commit 06c491e
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 23 deletions.
42 changes: 41 additions & 1 deletion RedditFreeGamesNotifier/Models/SteamApi/AppDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,47 @@ namespace RedditFreeGamesNotifier.Models.SteamApi {
public class AppDetails {
[JsonPropertyName("success")]
public bool Success { get; set; }

[JsonPropertyName("data")]
public Dictionary<string, object> Data { get; set; }
public Data Data { get; set; }
}

public class Data {
[JsonPropertyName("type")]
public string Type { get; set; }

[JsonPropertyName("name")]
public string Name { get; set; }

[JsonPropertyName("is_free")]
public bool IsFree { get; set; }

[JsonPropertyName("packages")]
public List<int> Packages { get; set; } = [];

[JsonPropertyName("package_groups")]
public List<PackageGroup> PackageGroups { get; set; } = [];
}

public class PackageGroup {
[JsonPropertyName("name")]
public string Name { get; set; }

[JsonPropertyName("title")]
public string Title { get; set; }

[JsonPropertyName("subs")]
public List<Sub> Subs { get; set; } = [];
}

public class Sub {
[JsonPropertyName("packageid")]
public int PackageID { get; set; }

[JsonPropertyName("percent_savings")]
public int PercentSavings { get; set; }

[JsonPropertyName("is_free_license")]
public bool IsFreeLicense { get; set; }
}
}
83 changes: 67 additions & 16 deletions RedditFreeGamesNotifier/Services/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ internal class Parser: IDisposable {

var platform = ParseStrings.SupportedPlatform[dataDomain];
var redditLink = new StringBuilder().Append(ParseStrings.redditUrl).Append(dataPermaLink).ToString();

var isGOGGiveaway = platform == "GOG" && IsGOGGiveaway(dataUrl);

var appId = GetGameId(dataUrl);
var subId = string.Empty;
AppDetails steamAppDetails = null;
#endregion

#region check free game duplication in other sources, old records must NOT included
Expand All @@ -69,7 +73,7 @@ internal class Parser: IDisposable {
}

// found in other subreddits
if (result.Records.Any(record => record.AppId == appId)) {
if (result.Records.Any(record => record.AppId.Split(',').Contains(appId))) {
_logger.LogDebug(ParseStrings.debugSteamIDDuplicationDetected, dataUrl);
continue;
}
Expand All @@ -90,24 +94,29 @@ internal class Parser: IDisposable {
}
#endregion

#region free game validation
#region extra validation and information gathering
//Itchio
if (platform == "Itch.io" && !await IsClaimable(dataUrl)) {
_logger.LogDebug(ParseStrings.debugItchIOCNotClaimable, dataUrl);
continue;
}

if (platform == "Steam") {
steamAppDetails = platform == "Steam" ? await GetSteamAppDetails(appId) : null;
subId = GetSteamSubID(steamAppDetails);
}
#endregion

_logger.LogDebug($"{dataUrl} | {redditLink} | {platform} | {appId}\n");

var newRecord = new FreeGameRecord() {
var newRecord = new FreeGameRecord() {
Url = dataUrl,
RedditUrl = redditLink,
Platform = platform,
AppId = appId,
AppId = string.IsNullOrEmpty(subId) ? appId : $"{appId},{subId}",
IsGOGGiveaway = isGOGGiveaway
};
newRecord.Name = await GetGameName(newRecord, redditTitle);
newRecord.Name = await GetGameName(newRecord, steamAppDetails, redditTitle);

result.Records.Add(newRecord);

Expand Down Expand Up @@ -144,7 +153,27 @@ internal class Parser: IDisposable {
return appIdMatch.Success ? appIdMatch.Value : subIdMatch.Success ? subIdMatch.Value : string.Empty;
}

private async Task<string> GetGameName(FreeGameRecord record, string redditTitle) {
private async Task<AppDetails> GetSteamAppDetails(string appId) {
try {
appId = appId.Split('/')[1];
var appDetailsUrl = ParseStrings.steamApiAppDetailsPrefix + appId;
_logger.LogDebug(ParseStrings.debugGetSteamAppDetails, appDetailsUrl);

var source = await services.GetRequiredService<Scraper>().GetSource(appDetailsUrl);
if(source == null || source == "null") return null;

var json = JsonSerializer.Deserialize<Dictionary<string, AppDetails>>(source);

_logger.LogDebug(ParseStrings.debugSteamApiFailed, appId);
return json[appId];
} catch (Exception ex) {
_logger.LogDebug(ParseStrings.errorGetSteamAppDetails, appId);
_logger.LogDebug(ex.Message);
return null;
}
}

private async Task<string> GetGameName(FreeGameRecord record, AppDetails appDetails, string redditTitle) {
try {
var gameName = redditTitle;

Expand Down Expand Up @@ -175,16 +204,8 @@ internal class Parser: IDisposable {
}

if (record.Platform == "Steam") {
var appId = record.AppId.Split('/')[1];
var appDetailsUrl = ParseStrings.steamApiurlPrefix + appId;
_logger.LogDebug(ParseStrings.debugGetGameNameWithUrl, appDetailsUrl);

var source = await services.GetRequiredService<Scraper>().GetSource(appDetailsUrl);
var json = JsonSerializer.Deserialize<Dictionary<string, AppDetails>>(source);

if (json[appId].Success)
gameName = json[appId].Data[ParseStrings.steamAppDetailGameNameKey].ToString();
else _logger.LogDebug(ParseStrings.debugSteamApiGetNameFailed, appId);
if (appDetails != null && appDetails.Success) gameName = appDetails.Data.Name;
else _logger.LogDebug(ParseStrings.debugGetGameNameAppDetailsFailed, record.AppId);
}

_logger.LogDebug($"Done: {ParseStrings.debugGetGameNameWithUrl}", record.Url);
Expand All @@ -196,6 +217,36 @@ internal class Parser: IDisposable {
}
}

private string GetSteamSubID(AppDetails appDetails) {
try {
_logger.LogDebug(ParseStrings.debugGetSteamSubID);

if (appDetails == null || !appDetails.Success) {
_logger.LogDebug(ParseStrings.debugGetSteamSubIDAppDetailsFailed);
return string.Empty;
}

var data = appDetails.Data;

if (data.Type == "dlc") {
if (data.PackageGroups != null && data.PackageGroups.Count > 0) {
var defaultPackageGroup = appDetails.Data.PackageGroups.First(pg => pg.Name == ParseStrings.steamAppDetailsGameTypeValueDefault);
var freeSubs = defaultPackageGroup.Subs.Where(sub => sub.IsFreeLicense == true).ToList();
var freeSubsIDString = string.Join(",", freeSubs.Select(sub => $"{ParseStrings.subIdPrefix}{sub.PackageID}"));

_logger.LogDebug(ParseStrings.debugGotSteamSubID, freeSubsIDString);
return freeSubsIDString;
}
} else _logger.LogDebug(ParseStrings.debugGetSteamSubIDNotDLC);

_logger.LogDebug(ParseStrings.debugGetSteamSubIDNoSubID);
return string.Empty;
} catch (Exception) {
_logger.LogError($"Error: {ParseStrings.debugGetSteamSubID}");
return string.Empty;
}
}

private static bool IsGOGGiveaway(string url) {
return url.Contains(ParseStrings.gogGiveawayUrlKeyword) || url.TrimEnd('/').EndsWith(ParseStrings.gogGiveawayUrlEndKeyword);
}
Expand Down
27 changes: 21 additions & 6 deletions RedditFreeGamesNotifier/Strings/ParseStrings.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Runtime.CompilerServices;

namespace RedditFreeGamesNotifier.Strings {
namespace RedditFreeGamesNotifier.Strings {
internal class ParseStrings {
internal static readonly Dictionary<string, string> SupportedPlatform = new() {
{ "store.steampowered.com", "Steam" },
Expand All @@ -15,12 +13,18 @@ internal class ParseStrings {
};

internal static readonly string redditUrl = "https://old.reddit.com";
internal static readonly string steamApiurlPrefix = "https://store.steampowered.com/api/appdetails?appids=";
internal static readonly string steamApiAppDetailsPrefix = "https://store.steampowered.com/api/appdetails?filters=basic,packages&appids=";

internal static readonly string appIdRegex = @"app/[0-9]*";
internal static readonly string subIdRegex = @"sub/[0-9]*";
internal static readonly string subIdPrefix = "sub/";

internal static readonly string steamAppDetailsGameTypeKey = "type";
internal static readonly string steamAppDetailsGameNameKey = "name";
internal static readonly string steamAppDetailsPackageGroupsKey = "package_groups";
internal static readonly string steamAppDetailsPackagesKey = "packages";
internal static readonly string steamAppDetailsGameTypeValueDefault = "default";

internal static readonly string steamAppDetailGameNameKey = "name";
internal static readonly string gogGiveawayUrlKeyword = "#giveaway";
internal static readonly string gogGiveawayUrlEndKeyword = "gog.com";

Expand Down Expand Up @@ -52,15 +56,26 @@ internal class ParseStrings {
#region debug strings
internal static readonly string debugParse = "Parse";
internal static readonly string debugParseWithUrl = "Parsing: {0}\n\n";

internal static readonly string debugGetGameName = "GetGameName";
internal static readonly string debugGetGameNameWithUrl = "GetGameName: {0}";
internal static readonly string debugGetGameNameAppDetailsFailed = "Get name name failed since Steam app details failed, appID: {0}";

internal static readonly string debugGetSteamSubID = "Get Steam Sub ID";
internal static readonly string debugGotSteamSubID = "Got Steam Sub ID: {0}";
internal static readonly string debugGetSteamSubIDNotDLC = "Not DLC, skip getting sub ID, returning app ID instead.";
internal static readonly string debugGetSteamSubIDNoSubID = "No sub ID detected, returning empty string.";
internal static readonly string debugGetSteamSubIDAppDetailsFailed = "Get sub ID failed since Steam app details failed.";

internal static readonly string debugGetSteamAppDetails = "GetSteamAppDetails: {0}";

internal static readonly string infoFoundNewGame = "Found new free game: {0}";
internal static readonly string debugFoundInOldRecords = "Found {0} in old records, stop adding to push list";

internal static readonly string debugIsGOGGiveaway = "GOG Giveaway detected: {0}";

internal static readonly string debugSteamApiGetNameFailed = "Steam App Detail API for game ID {0} returned failed success code.";
internal static readonly string debugSteamApiFailed = "Steam App Detail API for game ID {0} returned failed success code.";
internal static readonly string errorGetSteamAppDetails = "Cannot get steam app details, app ID: {0}";
internal static readonly string errorGetGameName = "Cannot fetch game name from: {0}, probably caused by poor internet connection.";

internal static readonly string debugCheckItchIOClaimable = "Checking whether itch.io free game is claimable";
Expand Down

0 comments on commit 06c491e

Please sign in to comment.