diff --git a/ASFFreeGames.Tests/Redlib/RedlibHtmlParserTests.cs b/ASFFreeGames.Tests/Redlib/RedlibHtmlParserTests.cs index 8b8189f..7b43d9c 100644 --- a/ASFFreeGames.Tests/Redlib/RedlibHtmlParserTests.cs +++ b/ASFFreeGames.Tests/Redlib/RedlibHtmlParserTests.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -19,6 +21,8 @@ public async void Test() { Assert.NotEmpty(result); Assert.Equal(25, result.Count); + Assert.Equal(new DateTimeOffset(2024, 6, 1, 23, 43, 40, TimeSpan.Zero), result.Skip(1).FirstOrDefault().Date); + // ReSharper disable once ArgumentsStyleLiteral result = RedlibHtmlParser.ParseGamesFromHtml(html, dedup: true); Assert.NotEmpty(result); diff --git a/ASFFreeGames/FreeGames/Strategies/RedlibListFreeGamesStrategy.cs b/ASFFreeGames/FreeGames/Strategies/RedlibListFreeGamesStrategy.cs index e996d1a..a43df1b 100644 --- a/ASFFreeGames/FreeGames/Strategies/RedlibListFreeGamesStrategy.cs +++ b/ASFFreeGames/FreeGames/Strategies/RedlibListFreeGamesStrategy.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Net.Http; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Core; @@ -76,9 +78,44 @@ public async Task> GetGames([NotNull] ListF } } + /// + /// Tries to get the date from the HTTP headers using reflection. + /// + /// The HTTP response. + /// The date from the HTTP headers, or null if not found. + /// + /// This method is used to work around the trimmed binary issue in the release build. + /// In the release build, the property is trimmed, and the Date + /// property is not available. This method uses reflection to safely try to get the date from the HTTP headers. + /// + public static DateTimeOffset? GetDateFromHeaders([NotNull] HttpResponseMessage response) { + try { + Type headersType = response.Headers.GetType(); + + // Try to get the "Date" property using reflection + PropertyInfo? dateProperty = headersType.GetProperty("Date"); + + if (dateProperty != null) { + // Get the value of the "Date" property + object? dateValue = dateProperty.GetValue(response.Headers); + + // Check if the value is of type DateTimeOffset? + if (dateValue is DateTimeOffset?) { + return (DateTimeOffset?) dateValue; + } + } + } + catch (Exception) { + // ignored + } + + return null; + } + private async Task> DoDownloadUsingInstance(SimpleHttpClient client, Uri uri, CancellationToken cancellationToken) { await DownloadSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); string content; + DateTimeOffset date = default; try { #pragma warning disable CAC001 @@ -101,6 +138,8 @@ private async Task> DoDownloadUsingInstance } else { content = await resp.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + + date = GetDateFromHeaders(resp.Response) ?? date; } } finally { @@ -108,9 +147,15 @@ private async Task> DoDownloadUsingInstance } IReadOnlyCollection entries = RedlibHtmlParser.ParseGamesFromHtml(content); - long now = DateTimeOffset.Now.ToUnixTimeMilliseconds(); // TODO read the date from the response's content + DateTimeOffset now = DateTimeOffset.Now; + + if ((date == default(DateTimeOffset)) || ((now - date).Duration() > TimeSpan.FromDays(1))) { + date = now; + } + + long dateMillis = date.ToUnixTimeMilliseconds(); - return entries.Select(entry => entry.ToRedditGameEntry(now)).ToArray(); + return entries.Select(entry => entry.ToRedditGameEntry(dateMillis)).ToArray(); } private async Task> DownloadUsingInstance(SimpleHttpClient client, Uri uri, uint retry, CancellationToken cancellationToken) { diff --git a/ASFFreeGames/Redlib/Html/ParserIndices.cs b/ASFFreeGames/Redlib/Html/ParserIndices.cs index 0c13b63..bbfe81d 100644 --- a/ASFFreeGames/Redlib/Html/ParserIndices.cs +++ b/ASFFreeGames/Redlib/Html/ParserIndices.cs @@ -1,3 +1,3 @@ namespace Maxisoft.ASF.Redlib.Html; -internal readonly record struct ParserIndices(int StartOfCommandIndex, int EndOfCommandIndex, int StartOfFooterIndex, int HrefStartIndex, int HrefEndIndex); +internal readonly record struct ParserIndices(int StartOfCommandIndex, int EndOfCommandIndex, int StartOfFooterIndex, int HrefStartIndex, int HrefEndIndex, int DateStartIndex, int DateEndIndex); diff --git a/ASFFreeGames/Redlib/Html/RedditHtmlParser.cs b/ASFFreeGames/Redlib/Html/RedditHtmlParser.cs index c628e05..4003369 100644 --- a/ASFFreeGames/Redlib/Html/RedditHtmlParser.cs +++ b/ASFFreeGames/Redlib/Html/RedditHtmlParser.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using ASFFreeGames.ASFExtentions.Games; using Maxisoft.ASF.Reddit; using Maxisoft.Utils.Collections.Dictionaries; @@ -22,7 +23,7 @@ public static IReadOnlyCollection ParseGamesFromHtml(ReadOnlySp try { indices = ParseIndices(html, startIndex); - (int startOfCommandIndex, int endOfCommandIndex, int _, _, _) = indices; + (int startOfCommandIndex, int endOfCommandIndex, int _, _, _, _, _) = indices; ReadOnlySpan command = html[startOfCommandIndex..endOfCommandIndex].Trim(); @@ -39,7 +40,18 @@ public static IReadOnlyCollection ParseGamesFromHtml(ReadOnlySp EGameType flag = ParseGameTypeFlags(html[indices.StartOfCommandIndex..indices.StartOfFooterIndex]); ReadOnlySpan title = ExtractTitle(html, indices); - RedlibGameEntry entry = new(effectiveGameIdentifiers.ToArray(), title.ToString(), flag); + + DateTimeOffset createdDate = default; + + if ((indices.DateStartIndex < indices.DateEndIndex) && (indices.DateEndIndex > 0)) { + ReadOnlySpan dateString = html[indices.DateStartIndex..indices.DateEndIndex].Trim(); + + if (!TryParseCreatedDate(dateString, out createdDate)) { + createdDate = default(DateTimeOffset); + } + } + + RedlibGameEntry entry = new(effectiveGameIdentifiers.ToArray(), title.ToString(), flag, createdDate); try { entries.Add(entry, default(EmptyStruct)); @@ -60,6 +72,32 @@ public static IReadOnlyCollection ParseGamesFromHtml(ReadOnlySp return (IReadOnlyCollection) entries.Keys; } + private static readonly string[] CommonDateFormat = ["MM dd yyyy, HH:mm:ss zzz", "MM dd yyyy, HH:mm:ss zzz", "MMM dd yyyy, HH:mm:ss UTC", "yyyy-MM-ddTHH:mm:ssZ", "yyyy-MM-ddTHH:mm:ss", "yyyy-MM-dd HH:mm:ss zzz", "yyyy-MM-dd HH:mm:ss.fffffff zzz", "yyyy-MM-ddTHH:mm:ss.fffffffzzz", "yyyy-MM-dd HH:mm:ss", "yyyyMMddHHmmss", "yyyyMMddHHmmss.fffffff"]; + + private static bool TryParseCreatedDate(ReadOnlySpan dateString, out DateTimeOffset createdDate) { + // parse date like May 31 2024, 12:28:53 UTC + + if (dateString.IsEmpty) { + createdDate = DateTimeOffset.Now; + + return false; + } + + foreach (string format in CommonDateFormat) { + if (DateTimeOffset.TryParseExact(dateString, format, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowWhiteSpaces, out createdDate)) { + return true; + } + } + + if (DateTimeOffset.TryParse(dateString, DateTimeFormatInfo.InvariantInfo, out createdDate)) { + return true; + } + + createdDate = DateTimeOffset.Now; + + return false; + } + internal static ReadOnlySpan ExtractTitle(ReadOnlySpan html, ParserIndices indices) { Span ranges = stackalloc Range[MaxIdentifierPerEntry]; ReadOnlySpan hrefSpan = html[indices.HrefStartIndex..indices.HrefEndIndex]; @@ -114,6 +152,31 @@ internal static ParserIndices ParseIndices(ReadOnlySpan html, int start) { commentLinkIndex += start; + int createdStartIndex = html[commentLinkIndex..startIndex].IndexOf(" html, int start) { startIndex = html[startIndex..commandEndIndex].IndexOf("!addlicense", StringComparison.OrdinalIgnoreCase) + startIndex; - return new ParserIndices(startIndex, commandEndIndex, infoFooterStartIndex, hrefStartIndex, hrefEndIndex); + return new ParserIndices(startIndex, commandEndIndex, infoFooterStartIndex, hrefStartIndex, hrefEndIndex, createdTitleStartIndex, createdTitleEndIndex); } internal static Span SplitCommandAndGetGameIdentifiers(ReadOnlySpan command, Span gameIdentifiers) { diff --git a/ASFFreeGames/Redlib/RedlibGameEntry.cs b/ASFFreeGames/Redlib/RedlibGameEntry.cs index 2ed3b38..3bb73d2 100644 --- a/ASFFreeGames/Redlib/RedlibGameEntry.cs +++ b/ASFFreeGames/Redlib/RedlibGameEntry.cs @@ -1,13 +1,21 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using ASFFreeGames.ASFExtentions.Games; using Maxisoft.ASF.Reddit; +// ReSharper disable once CheckNamespace namespace Maxisoft.ASF.Redlib; #pragma warning disable CA1819 -public readonly record struct RedlibGameEntry(IReadOnlyCollection GameIdentifiers, string CommentLink, EGameType TypeFlags) { - public RedditGameEntry ToRedditGameEntry(long date = default) => new(string.Join(',', GameIdentifiers), TypeFlags.ToRedditGameEntryKind(), date); +public readonly record struct RedlibGameEntry(IReadOnlyCollection GameIdentifiers, string CommentLink, EGameType TypeFlags, DateTimeOffset Date) { + public RedditGameEntry ToRedditGameEntry(long date = default) { + if ((Date != default(DateTimeOffset)) && (Date != DateTimeOffset.MinValue)) { + date = Date.ToUnixTimeMilliseconds(); + } + + return new RedditGameEntry(string.Join(',', GameIdentifiers), TypeFlags.ToRedditGameEntryKind(), date); + } } #pragma warning restore CA1819 diff --git a/Directory.Build.props b/Directory.Build.props index bf819cf..336818b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ ASFFreeGames - 1.6.2.0 + 1.7.0.0 net8.0