internal async Task StartProcess(HtmlDocument htmlDoc, List <Dictionary <string, string> > records,
                                         string chat_id, string token)
        {
            #region useful variables
            var pushList   = new List <string>();
            var recordList = new List <Dictionary <string, string> >();
            #endregion

            #region data processing
            var apps = htmlDoc.DocumentNode.CssSelect("table tr.app");             //find all free games
            foreach (var each in apps)
            {
                //skip the hidden trap row
                if (each.Attributes.HasKeyIgnoreCase("hidden"))
                {
                    continue;
                }

                var tds   = each.CssSelect("td").ToArray();
                var tdLen = tds.Length;                 //steamDB added an extra column with a intall button

                //start gather free game basic info
                string subID    = tds[1].SelectSingleNode(".//a[@href]").Attributes["href"].Value.Split('/')[2];
                string gameName = tds[1].SelectSingleNode(".//b").InnerText;
                string gameURL  = tds[0].SelectSingleNode(".//a[@href]").Attributes["href"].Value.Split('?')[0];
                string freeType = tdLen == 5 ? tds[2].InnerHtml.ToString() : tds[3].InnerHtml.ToString();                 //steamDB added an extra column with a intall button

                string startTime, endTime;

                if (tdLen == 5)                                                                                                                                                                                                                      //steamDB added an extra column with a intall button
                {
                    startTime = tds[3].Attributes["data-time"] == null ? "None" : DateTime.ParseExact(tds[3].Attributes["data-time"].Value.ToString(), SteamDBDateFormat, System.Globalization.CultureInfo.InvariantCulture).AddHours(8).ToString(); // in case of blank start time or end time
                    endTime   = tds[4].Attributes["data-time"] == null ? "None" : DateTime.ParseExact(tds[4].Attributes["data-time"].Value.ToString(), SteamDBDateFormat, System.Globalization.CultureInfo.InvariantCulture).AddHours(8).ToString();
                }
                else
                {
                    startTime = tds[4].Attributes["data-time"] == null ? "None" : DateTime.ParseExact(tds[4].Attributes["data-time"].Value.ToString(), SteamDBDateFormat, System.Globalization.CultureInfo.InvariantCulture).AddHours(8).ToString();
                    endTime   = tds[5].Attributes["data-time"] == null ? "None" : DateTime.ParseExact(tds[5].Attributes["data-time"].Value.ToString(), SteamDBDateFormat, System.Globalization.CultureInfo.InvariantCulture).AddHours(8).ToString();
                }

                if (freeType != "Weekend")
                {
                    _logger.LogInformation("Found free game: {0}", gameName);
                    //add game info to recordList
                    var tmpDic = new Dictionary <string, string> {
                        { "Name", gameName }, { "SubID", subID }, { "URL", gameURL },
                        { "StartTime", startTime }, { "EndTime", endTime }
                    };
                    recordList.Add(tmpDic);

                    if (!records.Where(x => x["SubID"] == subID).Any())                       //the game is not in the previous record(a new game)
                    //try to get game name on Steam page
                    {
                        var browser = new ScrapingBrowser()
                        {
                            Encoding = Encoding.UTF8
                        };
                        WebPage page   = browser.NavigateToPage(new Uri(gameURL));
                        var     tmpDoc = new HtmlDocument();
                        tmpDoc.LoadHtml(page.Content);
                        var steamName = tmpDoc.DocumentNode.CssSelect("div.apphub_AppName").ToArray();
                        if (steamName.Length > 0)
                        {
                            gameName = steamName[0].InnerText;
                        }

                        StringBuilder pushMessage = new ();
                        pushMessage.AppendFormat("<b>{0}</b>\n\nSub ID: <i>{1}</i>\n链接: <a href=\"{2}\" > {3}</a>\n开始时间: {4}\n结束时间: {5}\n", gameName, subID, gameURL, gameName, startTime, endTime);

                        pushList.Add(pushMessage.ToString());
                        _logger.LogInformation("Added game {0} in push list", gameName);
                    }
                    else
                    {
                        _logger.LogInformation("{0} is found in previous records, stop adding in push list...", gameName);
                    }
                }
            }
            #endregion

            _logger.LogInformation("Writing records...");
            #region write records
            using (var jsonOp = new JsonOP()) {
                if (recordList.Count > 0)
                {
                    jsonOp.WriteData(recordList, recordPath);
                    _logger.LogDebug("Done writing records !");
                }
                else
                {
                    _logger.LogDebug("No records detected, quit writing records...");
                }
            }
            #endregion

            _logger.LogInformation("Sending notification...");
            #region send notifications
            await SendNotification(id : chat_id, token : token, msgs : pushList);

            #endregion
        }
        internal async Task Run()
        {
            _logger.LogInformation(" - Start Job -");
            #region previous records and config file
            var records = new List <Dictionary <string, string> >();
            var config  = new Dictionary <string, string>();
            #endregion

            using (var jsonOp = new JsonOP()) {
                _logger.LogInformation("Loading previous records...");
                #region load previous records
                try {
                    records = jsonOp.LoadData(recordPath);                    // load previous free game info
                } catch (Exception e) {
                    _logger.LogError("Error loading previous records !");
                    _logger.LogError("Error message: { 0}\n", e.Message);
                }
                #endregion
                _logger.LogInformation("Done");

                _logger.LogInformation("Loading configurations...");
                #region load config
                try {
                    config = jsonOp.LoadConfig(path: configPath);
                } catch (Exception e) {
                    _logger.LogError("Error loading config file !");
                    _logger.LogError("Error message: {0}", e.Message);
                }
                #endregion
                _logger.LogInformation("Done");
            }

            _logger.LogInformation("Getting page source...");
            var htmlDoc = new HtmlDocument();
            #region playright varialbles
            using var playwright = await Playwright.CreateAsync();

            await using var browser = await playwright.Webkit.LaunchAsync(new() { Headless = true });

            #endregion

            #region load page
            try {
                var page = await browser.NewPageAsync();

                await page.GotoAsync(SteamDBUrl);

                Thread.Sleep(firstDelay);
                htmlDoc.LoadHtml(await page.InnerHTMLAsync("*"));
                _logger.LogInformation("Done");
            } catch (Exception ex) {
                _logger.LogError("Get source error!");
                _logger.LogError("Error message: {0}", ex.Message);
            } finally {
                await browser.CloseAsync();
            }
            #endregion

            _logger.LogInformation("Start data processing...");
            await StartProcess(htmlDoc : htmlDoc, records : records, chat_id : config["CHAT_ID"], token : config["TOKEN"]);

            _logger.LogInformation("Done");

            _logger.LogInformation(" - End Job -");
        }