PageRenderTime 143ms CodeModel.GetById 99ms app.highlight 29ms RepoModel.GetById 12ms app.codeStats 0ms

/Espera.Core/MusicBrainzArtworkFetcher.cs

http://github.com/flagbug/Espera
C# | 163 lines | 120 code | 33 blank | 10 comment | 47 complexity | 5a38e4235964d6c56906acffc08121a1 MD5 | raw file
Possible License(s): BSD-3-Clause, CC-BY-SA-3.0
  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}