PageRenderTime 3ms CodeModel.GetById 1ms app.highlight 11ms RepoModel.GetById 1ms app.codeStats 0ms

/Espera.Core/MusicBrainzArtworkFetcher.cs

Relevant Search: With Applications for Solr and Elasticsearch

'Chapter 4. Taming tokens'. If you want to know how to extract ideas rather than words this book is for you. Learn concepts of precision and recall, making trade-offs between them and controlling the specificity of matches. Amazon Affiliate Link
http://github.com/flagbug/Espera
C# | 163 lines | 120 code | 33 blank | 10 comment | 47 complexity | 5a38e4235964d6c56906acffc08121a1 MD5 | raw file
  1using System;
  2using System.Collections.Generic;
  3using System.Linq;
  4using System.Net;
  5using System.Net.Http;
  6using System.Text;
  7using System.Threading.Tasks;
  8using System.Xml.Linq;
  9using Newtonsoft.Json.Linq;
 10using ReactiveMarrow;
 11using ReactiveUI;
 12using Splat;
 13
 14namespace Espera.Core
 15{
 16    public class MusicBrainzArtworkFetcher : IArtworkFetcher, IEnableLogger
 17    {
 18        private const string ArtworkEndpoint = "http://coverartarchive.org/release/{0}/";
 19        private const string SearchEndpoint = "http://www.musicbrainz.org/ws/2/release/?query=artist:{0}+release:{1}";
 20
 21        private readonly RateLimitedOperationQueue queue;
 22
 23        public MusicBrainzArtworkFetcher()
 24        {
 25            // The MusicBraint search service allows us to perform onme request per second on
 26            // average, make sure we don't exceed that.
 27            this.queue = new RateLimitedOperationQueue(TimeSpan.FromSeconds(1.5), RxApp.TaskpoolScheduler);
 28        }
 29
 30        public async Task<Uri> RetrieveAsync(string artist, string album)
 31        {
 32            // Replace special character, as MusicBrainz uses Lucene in the backend
 33            artist = Escape(artist);
 34            album = Escape(album);
 35
 36            // Only searches are rate-limited, artwork retrievals are fine
 37            IReadOnlyList<string> releaseIds = await this.queue.EnqueueOperation(() => GetReleaseIdsAsync(artist, album));
 38
 39            if (releaseIds == null)
 40            {
 41                return null;
 42            }
 43
 44            return await GetArtworkLinkAsync(releaseIds);
 45        }
 46
 47        /// <summary>
 48        /// Escapes a lucene query
 49        /// </summary>
 50        private static String Escape(String s)
 51        {
 52            var sb = new StringBuilder();
 53
 54            foreach (char c in s)
 55            {
 56                // These characters are part of the query syntax and must be escaped
 57                if (c == '\\' || c == '+' || c == '-' || c == '!' || c == '(' || c == ')' || c == ':'
 58                    || c == '^' || c == '[' || c == ']' || c == '\"' || c == '{' || c == '}' || c == '~'
 59                    || c == '*' || c == '?' || c == '/')
 60                {
 61                    sb.Append(@"\");
 62                }
 63
 64                if (c == '|' || c == '&')
 65                {
 66                    sb.Append(@"\\");
 67                }
 68
 69                sb.Append(c);
 70            }
 71
 72            return sb.ToString();
 73        }
 74
 75        private static async Task<IReadOnlyList<string>> GetReleaseIdsAsync(string artist, string album)
 76        {
 77            string searchRequestUrl = string.Format(SearchEndpoint, artist, album);
 78            string searchResponse;
 79
 80            using (var client = new HttpClient())
 81            {
 82                client.DefaultRequestHeaders.Add("user-agent", "Espera/2.0 (http://getespera.com)");
 83
 84                try
 85                {
 86                    searchResponse = await client.GetStringAsync(searchRequestUrl);
 87                }
 88
 89                catch (HttpRequestException ex)
 90                {
 91                    throw new ArtworkFetchException(string.Format("Error while requesting the release id for artist {0} and album {1}", artist, album), ex);
 92                }
 93            }
 94
 95            XNamespace ns = "http://musicbrainz.org/ns/mmd-2.0#";
 96            var releases = XDocument.Parse(searchResponse).Descendants(ns + "release");
 97
 98            XNamespace scoreNs = "http://musicbrainz.org/ns/ext#-2.0";
 99
100            List<string> releaseIds = releases.Where(x => (int?)x.Attribute(scoreNs + "score") >= 95)
101                .Select(x => x.Attribute("id").Value)
102                .ToList();
103
104            return releaseIds;
105        }
106
107        private async Task<Uri> GetArtworkLinkAsync(IReadOnlyList<string> releaseIds)
108        {
109            using (var client = new HttpClient())
110            {
111                foreach (string releaseId in releaseIds)
112                {
113                    string artworkRequestUrl = string.Format(ArtworkEndpoint, releaseId);
114
115                    HttpResponseMessage response;
116
117                    try
118                    {
119                        response = await client.GetAsync(artworkRequestUrl);
120
121                        // The only valid failure status is "Not Found"
122                        if (response.StatusCode == HttpStatusCode.NotFound)
123                        {
124                            continue;
125                        }
126
127                        response.EnsureSuccessStatusCode();
128                    }
129
130                    catch (HttpRequestException ex)
131                    {
132                        string errorInfo = string.Format("Could not download artwork informations for release id {0}", releaseId);
133
134                        // If we can't even get the last artwork, throw
135                        if (releaseId == releaseIds.Last())
136                        {
137                            throw new ArtworkFetchException(errorInfo, ex);
138                        }
139
140                        if (releaseIds.Count > 1)
141                        {
142                            this.Log().Error(errorInfo + ", retrying with next in list.");
143                        }
144
145                        continue;
146                    }
147
148                    string responseContent = await response.Content.ReadAsStringAsync();
149                    JToken artworkUrlToken = JObject.Parse(responseContent).SelectToken("images[0].image");
150
151                    if (artworkUrlToken == null)
152                    {
153                        continue;
154                    }
155
156                    return new Uri(artworkUrlToken.ToObject<string>());
157                }
158            }
159
160            return null;
161        }
162    }
163}