public async Task <IEnumerable <Match> > GetMatchesPageAsync(string guid, HashSet <int> tagIds, int pageNumber, bool includeAdditionalInfo, Throttle throttle, ProgressData progressData)
        {
            var nameUnavailableCount = 0;
            var nameUnavailableMax   = 60;
            var retryCount           = 0;
            var retryMax             = 5;

            while (true)
            {
                await throttle.WaitAsync();

                var throttleReleased = false;

                try
                {
                    using (var testsResponse = await _ancestryLoginHelper.AncestryClient.GetAsync($"discoveryui-matchesservice/api/samples/{guid}/matches/list?page={pageNumber}&bookmarkdata={{\"moreMatchesAvailable\":true,\"lastMatchesServicePageIdx\":{pageNumber - 1}}}"))
                    {
                        throttle.Release();
                        throttleReleased = true;

                        testsResponse.EnsureSuccessStatusCode();
                        var matches = await testsResponse.Content.ReadAsAsync <MatchesV2>();

                        var result = matches.MatchGroups.SelectMany(matchGroup => matchGroup.Matches)
                                     .Select(match => ConvertMatch(match, tagIds))
                                     .ToList();

                        // Sometimes Ancestry returns matches with partial data.
                        // If that happens, retry and hope to get full data the next time.
                        if (result.Any(match => match.Name == "name unavailable") && ++nameUnavailableCount < nameUnavailableMax)
                        {
                            await Task.Delay(3000);

                            continue;
                        }

                        progressData.Increment();
                        if (includeAdditionalInfo)
                        {
                            try
                            {
                                await GetAdditionalInfoAsync(guid, result, throttle);
                            }
                            catch
                            {
                                // non-fatal if unable to download trees
                            }

                            if (pageNumber == 1)
                            {
                                try
                                {
                                    await GetParentsAsync(guid, result, throttle);
                                }
                                catch
                                {
                                    // non-fatal if unable to download parents
                                }
                            }
                        }

                        progressData.Increment();
                        return(result);
                    }
                }
                catch (Exception ex)
                {
                    if (++retryCount >= retryMax)
                    {
                        FileUtils.LogException(ex, true);
                        await Task.Delay(ex is UnsupportedMediaTypeException? 30000 : 3000);

                        return(Enumerable.Empty <Match>());
                    }
                    await Task.Delay(ex is UnsupportedMediaTypeException? 30000 : 3000);
                }
                finally
                {
                    if (!throttleReleased)
                    {
                        throttle.Release();
                    }
                }
            }
        }
        private async Task <int> CountMatches(string guid, Func <Match, bool> criteria, int minPage, int maxPage, Throttle throttle, ProgressData progressData)
        {
            IEnumerable <Match> pageMatches = new Match[0];

            // Try to find some page that is at least as high as the highest valid match.
            do
            {
                pageMatches = await GetMatchesPageAsync(guid, new HashSet <int>(), maxPage, false, throttle, progressData);

                if (pageMatches.Any(match => !criteria(match)) || !pageMatches.Any())
                {
                    break;
                }
                maxPage *= 2;
            } while (true);

            // Back down to find the the page that is exactly as high as the highest valid match
            var midPage = minPage;

            while (maxPage > minPage)
            {
                midPage     = (maxPage + minPage) / 2;
                pageMatches = await GetMatchesPageAsync(guid, new HashSet <int>(), midPage, false, throttle, progressData);

                if (pageMatches.Any(match => criteria(match)))
                {
                    if (pageMatches.Any(match => !criteria(match)))
                    {
                        break;
                    }
                    minPage = midPage + 1;
                }
                else
                {
                    maxPage = midPage;
                }
            }

            return((midPage - 1) * MatchesPerPage + pageMatches.Count(match => criteria(match)));
        }