static string FormatEntry(Z0rEntry entry)
        {
            var ret = new StringBuilder();

            ret.AppendFormat(CultureInfo.InvariantCulture, "z0r #{0}", entry.Z0rID);

            if (entry.Artist != null || entry.Song != null || entry.Image != null)
            {
                ret.Append(" (");

                if (entry.Artist != null && entry.Song != null)
                {
                    ret.AppendFormat("{0} - {1}", entry.Artist, entry.Song);
                }
                else if (entry.Song != null)
                {
                    ret.AppendFormat("{0}", entry.Song);
                }
                else if (entry.Artist != null)
                {
                    ret.AppendFormat("{0} - ?", entry.Artist);
                }
                else
                {
                    ret.Append("?");
                }

                ret.Append(" // ");
                ret.Append(entry.Image != null ? entry.Image : "?");
                ret.Append(")");
            }

            return(ret.ToString());
        }
        void LoadFromPage(long page)
        {
            var pageUri = new Uri(string.Format(CultureInfo.InvariantCulture, Z0rIndexUriFormat, page));

            byte[] indexPageBytes;
            using (var client = new HttpClient())
                using (var request = new HttpRequestMessage(HttpMethod.Get, pageUri))
                {
                    client.Timeout = TimeSpan.FromSeconds(LinkInfoConfig.TimeoutSeconds);
                    request.Headers.UserAgent.TryParseAdd(LinkInfoConfig.FakeUserAgent);

                    using (var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).SyncWait())
                    {
                        indexPageBytes = response.Content.ReadAsByteArrayAsync().SyncWait();
                    }
                }
            string indexPageString = EncodingGuesser.GuessEncodingAndDecode(indexPageBytes, null);

            var htmlDoc = new HtmlDocument();

            htmlDoc.LoadHtml(indexPageString);

            HtmlNode indexTable = htmlDoc.GetElementbyId("zebra");
            IEnumerable <HtmlNode> foundRows = indexTable
                                               .SelectNodes(".//tr")
                                               .OfType <HtmlNode>();

            foreach (HtmlNode foundRow in foundRows)
            {
                List <HtmlNode> cells = foundRow
                                        .ChildNodes
                                        .OfType <HtmlNode>()
                                        .Where(n => n.Name == "th")
                                        .ToList();
                if (cells.Count != 5)
                {
                    continue;
                }

                string idString = TrimmedInnerTextOrNull(cells[0]);

                long id;
                if (!long.TryParse(idString, NumberStyles.None, CultureInfo.InvariantCulture, out id))
                {
                    continue;
                }

                string artist = TrimmedInnerTextOrNull(cells[1]);
                string song   = TrimmedInnerTextOrNull(cells[2]);
                string image  = TrimmedInnerTextOrNull(cells[3]);
                string tag    = TrimmedInnerTextOrNull(cells[4]);

                EntryCache[id] = new Z0rEntry(id, artist, song, image, tag);
            }
        }
        public LinkAndInfo ResolveLink(LinkToResolve link)
        {
            string absoluteUri = link.Link.AbsoluteUri;
            Match  z0rMatch    = Z0rUrlPattern.Match(absoluteUri);

            if (!z0rMatch.Success)
            {
                // can't handle this
                return(null);
            }

            // obtain the ID
            long z0rID;

            if (!long.TryParse(z0rMatch.Groups["id"].Value, NumberStyles.None, CultureInfo.InvariantCulture, out z0rID))
            {
                // unparseable ID, probably too many digits
                return(null);
            }

            Z0rEntry entry;

            if (EntryCache.TryGetValue(z0rID, out entry))
            {
                // fast-path
                return(link.ToResult(FetchErrorLevel.Success, FormatEntry(entry)));
            }

            Z0rRange range = RangeForID(z0rID);

            if (!MaxPage.HasValue)
            {
                MaxPage = ObtainMaxPageValue();
            }

            if (!MaxPage.HasValue)
            {
                // bad
                return(link.ToResult(
                           FetchErrorLevel.TransientError,
                           string.Format(CultureInfo.InvariantCulture, "z0r #{0}; fetching index page list failed", z0rID)
                           ));
            }

            if (range.Page > MaxPage)
            {
                // the index does not contain this page
                entry = new Z0rEntry(z0rID, null, null, null, null);
                return(link.ToResult(FetchErrorLevel.Success, FormatEntry(entry)));
            }

            LoadFromPage(range.Page);

            if (EntryCache.TryGetValue(z0rID, out entry))
            {
                return(link.ToResult(FetchErrorLevel.Success, FormatEntry(entry)));
            }

            return(link.ToResult(
                       FetchErrorLevel.TransientError,
                       string.Format(CultureInfo.InvariantCulture, "z0r #{0}; fetching failed", z0rID)
                       ));
        }