2
0
Fork 0
mirror of https://github.com/sphildreth/roadie synced 2025-02-17 21:48:27 +00:00

MusicBrainz local repository implemented, many bug fixes regarding thumbnails.

This commit is contained in:
Steven Hildreth 2019-07-27 23:05:24 -05:00
parent 93b4014b91
commit aa11415e70
24 changed files with 793 additions and 519 deletions

View file

@ -2,10 +2,13 @@
using Microsoft.Extensions.Logging;
using Roadie.Library.Caching;
using Roadie.Library.Configuration;
using Roadie.Library.MetaData.MusicBrainz;
using Roadie.Library.Processors;
using Roadie.Library.SearchEngines.MetaData.Discogs;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
@ -65,6 +68,65 @@ namespace Roadie.Library.Tests
}
[Fact]
public async Task MusicBrainzArtistSearch()
{
if (!Configuration.Integrations.MusicBrainzProviderEnabled)
{
return;
}
var logger = new EventMessageLogger<MusicBrainzProvider>();
logger.Messages += MessageLogger_Messages;
var mb = new MusicBrainzProvider(Configuration, CacheManager, logger);
var artistName = "Billy Joel";
var mbId = "64b94289-9474-4d43-8c93-918ccc1920d1"; //https://musicbrainz.org/artist/64b94289-9474-4d43-8c93-918ccc1920d1
var sw = Stopwatch.StartNew();
var result = await mb.PerformArtistSearch(artistName, 1);
sw.Stop();
var elapsedTime = sw.ElapsedMilliseconds;
Assert.NotNull(result);
Assert.NotNull(result.Data);
Assert.NotEmpty(result.Data);
var artist = result.Data.FirstOrDefault();
Assert.NotNull(artist);
Assert.Equal(artist.MusicBrainzId, mbId);
}
[Fact]
public async Task MusicBrainzReleaseSearch()
{
if (!Configuration.Integrations.MusicBrainzProviderEnabled)
{
return;
}
var logger = new EventMessageLogger<MusicBrainzProvider>();
logger.Messages += MessageLogger_Messages;
var mb = new MusicBrainzProvider(Configuration, CacheManager, logger);
var artistName = "Billy Joel";
var mbId = "584a3887-c74e-4ff2-81e1-1620fbbe4f84"; // https://musicbrainz.org/release/584a3887-c74e-4ff2-81e1-1620fbbe4f84
var title = "Piano Man";
var sw = Stopwatch.StartNew();
var result = await mb.PerformReleaseSearch(artistName, title, 1);
sw.Stop();
var elapsedTime = sw.ElapsedMilliseconds;
Assert.NotNull(result);
Assert.NotNull(result.Data);
Assert.NotEmpty(result.Data);
var release = result.Data.FirstOrDefault();
Assert.NotNull(release);
Assert.Equal(release.MusicBrainzId, mbId);
}
private void MessageLogger_Messages(object sender, EventMessage e)
{
Console.WriteLine($"Log Level [{ e.Level }] Log Message [{ e.Message }] ");

View file

@ -385,5 +385,16 @@ namespace Roadie.Library.Tests
var t = input.ToTrackDuration();
Assert.Null(t);
}
[Theory]
[InlineData("https://itunes.apple.com/us/artist/id485953", "id485953")]
[InlineData("https://www.last.fm/music/Billy+Joel", "Billy+Joel")]
[InlineData("https://www.discogs.com/artist/137418", "137418")]
public void LastSegmentInUrl(string input, string shouldBe)
{
var v = input.LastSegmentInUrl();
Assert.Equal(v, shouldBe);
}
}
}

View file

@ -41,5 +41,6 @@ namespace Roadie.Library.Configuration
bool IsRegistrationClosed { get; set; }
bool UseRegistrationTokens { get; set; }
string SearchEngineReposFolder { get; set; }
}
}

View file

@ -93,6 +93,10 @@ namespace Roadie.Library.Configuration
/// If true then don't allow new registrations
/// </summary>
public bool IsRegistrationClosed { get; set; }
/// <summary>
/// Place to hold cache repositories used by SearchEngine and MetaData engines
/// </summary>
public string SearchEngineReposFolder { get; set; }
public RoadieSettings()
{

View file

@ -68,8 +68,7 @@ namespace Roadie.Library.Engines
var artistGenreTables = artist.Genres;
var ArtistImages = artist.Images;
var now = DateTime.UtcNow;
artist.AlternateNames =
artist.AlternateNames.AddToDelimitedList(new[] { artist.Name.ToAlphanumericName() });
artist.AlternateNames = artist.AlternateNames.AddToDelimitedList(new[] { artist.Name.ToAlphanumericName() });
artist.Genres = null;
artist.Images = null;
if (artist.Thumbnail == null && ArtistImages != null)
@ -87,10 +86,16 @@ namespace Roadie.Library.Engines
}
if (!artist.IsValid)
{
return new OperationResult<Artist>
{
Errors = new Exception[1] { new Exception("Artist is Invalid") }
};
}
if (artist.Thumbnail != null)
{
artist.Thumbnail = ImageHelper.ResizeToThumbnail(artist.Thumbnail, Configuration);
}
var addArtistResult = DbContext.Artists.Add(artist);
var inserted = 0;
inserted = await DbContext.SaveChangesAsync();
@ -265,8 +270,7 @@ namespace Roadie.Library.Engines
{
artist = artistSearch.Data;
// See if Artist already exist with either Name or Sort Name
var alreadyExists = DatabaseQueryForArtistName(artistSearch.Data.Name,
artistSearch.Data.SortNameValue);
var alreadyExists = DatabaseQueryForArtistName(artistSearch.Data.Name, artistSearch.Data.SortNameValue);
if (alreadyExists == null || !alreadyExists.IsValid)
{
var addResult = await Add(artist);
@ -317,8 +321,7 @@ namespace Roadie.Library.Engines
public async Task<OperationResult<Artist>> PerformMetaDataProvidersArtistSearch(AudioMetaData metaData)
{
SimpleContract.Requires<ArgumentNullException>(metaData != null, "Invalid MetaData");
SimpleContract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(metaData.Artist),
"Invalid MetaData, Missing Artist");
SimpleContract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(metaData.Artist), "Invalid MetaData, Missing Artist");
var sw = new Stopwatch();
sw.Start();
@ -340,12 +343,29 @@ namespace Roadie.Library.Engines
{
var i = iTunesResult.Data.First();
if (i.AlternateNames != null)
{
result.AlternateNames = result.AlternateNames.AddToDelimitedList(i.AlternateNames);
if (i.Tags != null) result.Tags = result.Tags.AddToDelimitedList(i.Tags);
if (i.Urls != null) result.URLs = result.URLs.AddToDelimitedList(i.Urls);
if (i.ISNIs != null) result.ISNI = result.ISNI.AddToDelimitedList(i.ISNIs);
if (i.ImageUrls != null) artistImageUrls.AddRange(i.ImageUrls);
if (i.ArtistGenres != null) artistGenres.AddRange(i.ArtistGenres);
}
if (i.Tags != null)
{
result.Tags = result.Tags.AddToDelimitedList(i.Tags);
}
if (i.Urls != null)
{
result.URLs = result.URLs.AddToDelimitedList(i.Urls);
}
if (i.ISNIs != null)
{
result.ISNI = result.ISNI.AddToDelimitedList(i.ISNIs);
}
if (i.ImageUrls != null)
{
artistImageUrls.AddRange(i.ImageUrls);
}
if (i.ArtistGenres != null)
{
artistGenres.AddRange(i.ArtistGenres);
}
result.CopyTo(new Artist
{
EndDate = i.EndDate,
@ -379,15 +399,34 @@ namespace Roadie.Library.Engines
{
var mb = mbResult.Data.First();
if (mb.AlternateNames != null)
{
result.AlternateNames = result.AlternateNames.AddToDelimitedList(mb.AlternateNames);
if (mb.Tags != null) result.Tags = result.Tags.AddToDelimitedList(mb.Tags);
if (mb.Urls != null) result.URLs = result.URLs.AddToDelimitedList(mb.Urls);
if (mb.ISNIs != null) result.ISNI = result.ISNI.AddToDelimitedList(mb.ISNIs);
if (mb.ImageUrls != null) artistImageUrls.AddRange(mb.ImageUrls);
if (mb.ArtistGenres != null) artistGenres.AddRange(mb.ArtistGenres);
}
if (mb.Tags != null)
{
result.Tags = result.Tags.AddToDelimitedList(mb.Tags);
}
if (mb.Urls != null)
{
result.URLs = result.URLs.AddToDelimitedList(mb.Urls);
}
if (mb.ISNIs != null)
{
result.ISNI = result.ISNI.AddToDelimitedList(mb.ISNIs);
}
if (mb.ImageUrls != null)
{
artistImageUrls.AddRange(mb.ImageUrls);
}
if (mb.ArtistGenres != null)
{
artistGenres.AddRange(mb.ArtistGenres);
}
if (!string.IsNullOrEmpty(mb.ArtistName) &&
!mb.ArtistName.Equals(result.Name, StringComparison.OrdinalIgnoreCase))
{
result.AlternateNames.AddToDelimitedList(new[] { mb.ArtistName });
}
result.CopyTo(new Artist
{
EndDate = mb.EndDate,
@ -544,7 +583,7 @@ namespace Roadie.Library.Engines
wikiName += " band";
}
var wikipediaResult = await WikipediaArtistSearchEngine.PerformArtistSearch(wikiName, 1);
if (wikipediaResult != null)
if (wikipediaResult?.Data != null)
{
if (wikipediaResult.IsSuccess)
{
@ -553,7 +592,7 @@ namespace Roadie.Library.Engines
{
result.CopyTo(new Artist
{
BioContext = HttpEncoder.HtmlEncode(w.Bio)
BioContext = HttpEncoder.HtmlEncode(w.Bio ?? string.Empty)
});
}
}
@ -570,12 +609,17 @@ namespace Roadie.Library.Engines
try
{
if (result.AlternateNames != null)
result.AlternateNames = string.Join("|",
result.AlternateNames.ToListFromDelimited().Distinct().OrderBy(x => x));
{
result.AlternateNames = string.Join("|", result.AlternateNames.ToListFromDelimited().Distinct().OrderBy(x => x));
}
if (result.URLs != null)
{
result.URLs = string.Join("|", result.URLs.ToListFromDelimited().Distinct().OrderBy(x => x));
}
if (result.Tags != null)
{
result.Tags = string.Join("|", result.Tags.ToListFromDelimited().Distinct().OrderBy(x => x));
}
if (artistGenres.Any())
{
var genreInfos = from ag in artistGenres
@ -588,17 +632,21 @@ namespace Roadie.Library.Engines
};
result.Genres = new List<ArtistGenre>();
foreach (var genreInfo in genreInfos)
result.Genres.Add(new ArtistGenre
{
var ag = new ArtistGenre
{
Genre = genreInfo.existingGenre != null
? genreInfo.existingGenre
: new Genre
{
Name = genreInfo.newGenre,
NormalizedName = genreInfo.newGenre.ToAlphanumericName()
Genre = genreInfo.existingGenre != null ? genreInfo.existingGenre : new Genre
{
Name = genreInfo.newGenre,
NormalizedName = genreInfo.newGenre.ToAlphanumericName()
}
});
}
};
if (!result.Genres.Any(x => x.Genre.NormalizedName == ag.Genre.NormalizedName))
{
result.Genres.Add(ag);
}
}
}
if (artistImageUrls.Any())
@ -614,15 +662,6 @@ namespace Roadie.Library.Engines
.Select(x => x.First())
.Take(Configuration.Processing.MaximumArtistImagesToAdd)
.ToList();
if (result.Thumbnail == null && result.Images != null)
{
result.Thumbnail = result.Images.First().Bytes;
}
}
if (result.Thumbnail != null)
{
result.Thumbnail = ImageHelper.ResizeToThumbnail(result.Thumbnail, Configuration);
}
}
catch (Exception ex)

View file

@ -4,6 +4,7 @@ using Roadie.Library.Configuration;
using Roadie.Library.Data;
using Roadie.Library.Encoding;
using Roadie.Library.Extensions;
using Roadie.Library.Imaging;
using Roadie.Library.SearchEngines.MetaData;
using Roadie.Library.Utility;
using System;
@ -34,11 +35,17 @@ namespace Roadie.Library.Engines
{
var now = DateTime.UtcNow;
label.AlternateNames = label.AlternateNames.AddToDelimitedList(new[] { label.Name.ToAlphanumericName() });
if (label.Thumbnail != null)
{
label.Thumbnail = ImageHelper.ResizeToThumbnail(label.Thumbnail, Configuration);
}
if (!label.IsValid)
{
return new OperationResult<Label>
{
Errors = new Exception[1] { new Exception("Label is Invalid") }
};
}
DbContext.Labels.Add(label);
var inserted = 0;
try
@ -163,10 +170,14 @@ namespace Roadie.Library.Engines
var d = discogsResult.Data.First();
if (d.Urls != null) result.URLs = result.URLs.AddToDelimitedList(d.Urls);
if (d.AlternateNames != null)
{
result.AlternateNames = result.AlternateNames.AddToDelimitedList(d.AlternateNames);
}
if (!string.IsNullOrEmpty(d.LabelName) &&
!d.LabelName.Equals(result.Name, StringComparison.OrdinalIgnoreCase))
{
result.AlternateNames.AddToDelimitedList(new[] { d.LabelName });
}
result.CopyTo(new Label
{
Profile = HttpEncoder.HtmlEncode(d.Profile),
@ -176,7 +187,10 @@ namespace Roadie.Library.Engines
});
}
if (discogsResult.Errors != null) resultsExceptions.AddRange(discogsResult.Errors);
if (discogsResult.Errors != null)
{
resultsExceptions.AddRange(discogsResult.Errors);
}
}
sw.Stop();

View file

@ -81,8 +81,7 @@ namespace Roadie.Library.Engines
var releaseMedias = release.Medias;
var releaseLabels = release.Labels;
var now = DateTime.UtcNow;
release.AlternateNames =
release.AlternateNames.AddToDelimitedList(new[] { release.Title.ToAlphanumericName() });
release.AlternateNames = release.AlternateNames.AddToDelimitedList(new[] { release.Title.ToAlphanumericName() });
release.Images = null;
release.Labels = null;
release.Medias = null;
@ -90,10 +89,16 @@ namespace Roadie.Library.Engines
release.LibraryStatus = LibraryStatus.Incomplete;
release.Status = Statuses.New;
if (!release.IsValid)
{
return new OperationResult<Release>
{
Errors = new Exception[1] { new Exception("Release is Invalid") }
};
}
if (release.Thumbnail != null)
{
release.Thumbnail = ImageHelper.ResizeToThumbnail(release.Thumbnail, Configuration);
}
DbContext.Releases.Add(release);
var inserted = 0;
try
@ -838,12 +843,6 @@ namespace Roadie.Library.Engines
}
}
}
if (result.Thumbnail != null)
{
result.Thumbnail = ImageHelper.ResizeToThumbnail(result.Thumbnail, Configuration);
}
sw.Stop();
return new OperationResult<Release>
{

View file

@ -8,6 +8,7 @@ using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
namespace Roadie.Library.Extensions
{
@ -331,5 +332,15 @@ namespace Roadie.Library.Extensions
return input.Substring(0, input.Length - suffixToRemove.Length);
return input;
}
public static string LastSegmentInUrl(this string input)
{
if(string.IsNullOrEmpty(input))
{
return null;
}
var uri = new Uri(input);
return uri.Segments.Last();
}
}
}

View file

@ -21,7 +21,7 @@ namespace Roadie.Library.Imaging
public static string ArtistImageFilename = "artist.jpg";
public static string ArtistSecondaryImageFilename = "artist {0}.jpg";
public static string LabelImageFilename = "label.jpg";
public static int MaximumThumbnailByteSize = 65000;
public static int MaximumThumbnailByteSize = 50000;
// Replace with counter of image
public static string ReleaseCoverFilename = "cover.jpg";
@ -36,10 +36,7 @@ namespace Roadie.Library.Imaging
IImageFormat imageFormat = null;
using (var image = Image.Load(imageBytes, out imageFormat))
{
if (imageFormat != ImageFormats.Jpeg)
{
image.Save(outStream, ImageFormats.Jpeg);
}
image.Save(outStream, ImageFormats.Jpeg);
}
return outStream.ToArray();
}
@ -266,8 +263,10 @@ namespace Roadie.Library.Imaging
{
return imageBytes;
}
var result = ImageHelper.ResizeImage(ImageHelper.ConvertToJpegFormat(imageBytes), configuration.ThumbnailImageSize.Width, configuration.ThumbnailImageSize.Height, true).Item2;
if(result.Length > ImageHelper.MaximumThumbnailByteSize)
var width = configuration?.ThumbnailImageSize?.Width ?? 80;
var height = configuration?.ThumbnailImageSize?.Height ?? 80;
var result = ImageHelper.ResizeImage(ImageHelper.ConvertToJpegFormat(imageBytes), width, height, true).Item2;
if(result.Length >= ImageHelper.MaximumThumbnailByteSize)
{
Trace.WriteLine($"Thumbnail larger than maximum size after resizing to [{configuration.ThumbnailImageSize.Width}x{configuration.ThumbnailImageSize.Height}] Thumbnail Size [{result.Length}]");
result = new byte[0];

View file

@ -17,6 +17,7 @@
<PackageReference Include="IdSharp.Common" Version="1.0.1" />
<PackageReference Include="IdSharp.Tagging" Version="1.0.0-rc3" />
<PackageReference Include="Inflatable.Lastfm" Version="1.1.0.339" />
<PackageReference Include="LiteDB" Version="4.1.4" />
<PackageReference Include="Mapster" Version="4.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.2.6" />
@ -35,7 +36,7 @@
<PackageReference Include="System.Drawing.Common" Version="4.5.1" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="4.5.0" />
<PackageReference Include="System.Runtime.Caching" Version="4.5.0" />
<PackageReference Include="z440.atl.core" Version="2.11.0" />
<PackageReference Include="z440.atl.core" Version="2.12.0" />
<PackageReference Include="zlib.net-mutliplatform" Version="1.0.4" />
</ItemGroup>

View file

@ -5,97 +5,11 @@ using System.Diagnostics;
namespace Roadie.Library.MetaData.MusicBrainz
{
public class Alias
{
public object begindate { get; set; }
public object enddate { get; set; }
public object locale { get; set; }
public string name { get; set; }
public object primary { get; set; }
[JsonProperty(PropertyName = "sort-name")]
public string sortname { get; set; }
public object type { get; set; }
}
public class Area
{
public string disambiguation { get; set; }
public string id { get; set; }
public List<string> iso31661codes { get; set; }
public string name { get; set; }
public string sortname { get; set; }
}
[DebuggerDisplay("name: {name}")]
public class Artist
{
public List<Alias> aliases { get; set; }
public Area area { get; set; }
public Begin_Area begin_area { get; set; }
public string country { get; set; }
public string disambiguation { get; set; }
public object end_area { get; set; }
public string gender { get; set; }
public string id { get; set; }
public List<string> ipis { get; set; }
[JsonProperty(PropertyName = "isni-list")]
public List<string> isnis { get; set; }
public LifeSpan lifespan { get; set; }
public string name { get; set; }
public List<Release> releases { get; set; }
[JsonProperty(PropertyName = "sort-name")]
public string sortname { get; set; }
public List<Tag> tags { get; set; }
public string type { get; set; }
}
public class ArtistResult
{
public List<Artist> artists { get; set; }
public int? count { get; set; }
public DateTime? created { get; set; }
public int? offset { get; set; }
}
public class AttributeValues
{
}
public class Begin_Area
{
public string disambiguation { get; set; }
public string id { get; set; }
public List<string> iso_3166_1_codes { get; set; }
public List<string> iso_3166_2_codes { get; set; }
public List<string> iso_3166_3_codes { get; set; }
public string name { get; set; }
public string sortname { get; set; }
}
public class BeginArea
{
public string id { get; set; }
public string name { get; set; }
public string sortname { get; set; }
}
[Serializable]
public class CoverArtArchive
{
public bool artwork { get; set; }
@ -119,8 +33,12 @@ namespace Roadie.Library.MetaData.MusicBrainz
[JsonProperty(PropertyName = "sort-name")]
public string sortname { get; set; }
public NameAndCount[] tags { get; set; }
public NameAndCount[] genres { get; set; }
}
[Serializable]
public class LabelInfo
{
[JsonProperty(PropertyName = "catalog-number")]
@ -129,13 +47,8 @@ namespace Roadie.Library.MetaData.MusicBrainz
public Label label { get; set; }
}
public class LifeSpan
{
public string begin { get; set; }
public string end { get; set; }
public bool ended { get; set; }
}
[Serializable]
public class Medium
{
public object format { get; set; }
@ -144,12 +57,13 @@ namespace Roadie.Library.MetaData.MusicBrainz
[JsonProperty(PropertyName = "track-count")]
public short? trackcount { get; set; }
[JsonProperty(PropertyName = "track-offset")]
public int? trackoffset { get; set; }
public List<Track> tracks { get; set; }
}
[Serializable]
public class Recording
{
public List<Alias> aliases { get; set; }
@ -158,8 +72,11 @@ namespace Roadie.Library.MetaData.MusicBrainz
public int? length { get; set; }
public string title { get; set; }
public bool video { get; set; }
public NameAndCount[] tags { get; set; }
public NameAndCount[] genres { get; set; }
}
[Serializable]
public class Relation
{
public List<object> attributes { get; set; }
@ -173,10 +90,19 @@ namespace Roadie.Library.MetaData.MusicBrainz
public string targettype { get; set; }
public string type { get; set; }
public string typeid { get; set; }
public Url url { get; set; }
public MbUrl url { get; set; }
}
[Serializable]
public class RepositoryRelease
{
public int Id { get; set; }
public string ArtistMbId { get; set; }
public Release Release { get; set; }
}
[DebuggerDisplay("title: {title}, date: {date}")]
[Serializable]
public class Release
{
public List<object> aliases { get; set; }
@ -203,13 +129,17 @@ namespace Roadie.Library.MetaData.MusicBrainz
[JsonProperty(PropertyName = "release-events")]
public List<ReleaseEvents> releaseevents { get; set; }
[JsonProperty(PropertyName = "release-group")]
public ReleaseGroup releasegroup { get; set; }
public string status { get; set; }
[JsonProperty(PropertyName = "text-representation")]
public TextRepresentation textrepresentation { get; set; }
public string title { get; set; }
[JsonProperty("artist-credits")]
public Artist[] artistcredits { get; set; }
}
[Serializable]
public class ReleaseBrowseResult
{
[JsonProperty(PropertyName = "release-count")]
@ -221,6 +151,7 @@ namespace Roadie.Library.MetaData.MusicBrainz
public List<Release> releases { get; set; }
}
[Serializable]
public class ReleaseEvents
{
public Area area { get; set; }
@ -228,30 +159,30 @@ namespace Roadie.Library.MetaData.MusicBrainz
public string date { get; set; }
}
[Serializable]
public class ReleaseGroup
{
public List<object> aliases { get; set; }
public string disambiguation { get; set; }
[JsonProperty("first-release-date")]
public string firstreleasedate { get; set; }
public string id { get; set; }
[JsonProperty("primary-type")]
public string primarytype { get; set; }
public List<object> secondarytypes { get; set; }
public string title { get; set; }
public NameAndCount[] tags { get; set; }
public NameAndCount[] genres { get; set; }
}
public class Tag
{
public int? count { get; set; }
public string name { get; set; }
}
[Serializable]
public class TextRepresentation
{
public string language { get; set; }
public string script { get; set; }
}
[Serializable]
public class Track
{
public string id { get; set; }
@ -262,9 +193,11 @@ namespace Roadie.Library.MetaData.MusicBrainz
public Recording recording { get; set; }
public string title { get; set; }
}
public class Url
[Serializable]
public class MbUrl
{
public string id { get; set; }
public string resource { get; set; }

View file

@ -7,14 +7,6 @@ namespace Roadie.Library.MetaData.MusicBrainz
{
public interface IMusicBrainzProvider : IArtistSearchEngine, IReleaseSearchEngine
{
Task<CoverArtArchivesResult> CoverArtForMusicBrainzReleaseById(string musicBrainzId);
Task<Release> MusicBrainzReleaseById(string musicBrainzId);
Task<IEnumerable<AudioMetaData>> MusicBrainzReleaseTracks(string artistName, string releaseTitle);
Task<Data.Release> ReleaseForMusicBrainzReleaseById(string musicBrainzId);
Task<IEnumerable<Release>> ReleasesForArtist(string artist, string artistMusicBrainzId = null);
Task<IEnumerable<AudioMetaData>> MusicBrainzReleaseTracks(string artistName, string releaseTitle);
}
}

View file

@ -1,74 +1,26 @@
using Microsoft.Extensions.Logging;
using Roadie.Library.Caching;
using Roadie.Library.Configuration;
using Roadie.Library.Data;
using Roadie.Library.Extensions;
using Roadie.Library.MetaData.Audio;
using Roadie.Library.SearchEngines.MetaData;
using Roadie.Library.Utility;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace Roadie.Library.MetaData.MusicBrainz
{
public class MusicBrainzProvider : MetaDataProviderBase, IMusicBrainzProvider
public sealed class MusicBrainzProvider : MetaDataProviderBase, IMusicBrainzProvider
{
public override bool IsEnabled => Configuration.Integrations.MusicBrainzProviderEnabled;
private MusicBrainzRepository Repository { get; }
public MusicBrainzProvider(IRoadieSettings configuration, ICacheManager cacheManager, ILogger<MusicBrainzProvider> logger)
: base(configuration, cacheManager, logger)
: base(configuration, cacheManager, logger)
{
}
public async Task<CoverArtArchivesResult> CoverArtForMusicBrainzReleaseById(string musicBrainzId)
{
return await MusicBrainzRequestHelper.GetAsync<CoverArtArchivesResult>(MusicBrainzRequestHelper.CreateCoverArtReleaseUrl(musicBrainzId));
}
public async Task<Release> MusicBrainzReleaseById(string musicBrainzId)
{
if (string.IsNullOrEmpty(musicBrainzId)) return null;
Release release = null;
try
{
var artistCacheKey = string.Format("uri:musicbrainz:MusicBrainzReleaseById:{0}", musicBrainzId);
release = CacheManager.Get<Release>(artistCacheKey);
if (release == null)
{
release = await MusicBrainzRequestHelper.GetAsync<Release>(
MusicBrainzRequestHelper.CreateLookupUrl("release", musicBrainzId,
"labels+aliases+recordings+release-groups+media+url-rels"));
if (release != null)
{
var coverUrls = await CoverArtForMusicBrainzReleaseById(musicBrainzId);
if (coverUrls != null)
{
var frontCover = coverUrls.images.FirstOrDefault(i => i.front);
release.imageUrls = coverUrls.images.Select(x => x.image).ToList();
if (frontCover != null)
{
release.coverThumbnailUrl = frontCover.image;
release.imageUrls = release.imageUrls.Where(x => x != release.coverThumbnailUrl)
.ToList();
}
}
CacheManager.Add(artistCacheKey, release);
}
}
}
catch (HttpRequestException)
{
}
if (release == null)
Logger.LogWarning("MusicBrainzReleaseById: MusicBrainzId [{0}], No MusicBrainz Release Found",
musicBrainzId);
return release;
Repository = new MusicBrainzRepository(configuration, logger);
}
public async Task<IEnumerable<AudioMetaData>> MusicBrainzReleaseTracks(string artistName, string releaseTitle)
@ -112,6 +64,7 @@ namespace Roadie.Library.MetaData.MusicBrainz
var result = new List<AudioMetaData>();
foreach (var media in release.media)
{
foreach (var track in media.tracks)
{
var date = 0;
@ -149,6 +102,7 @@ namespace Roadie.Library.MetaData.MusicBrainz
//}).ToArray()
});
}
}
return result;
}
@ -159,126 +113,55 @@ namespace Roadie.Library.MetaData.MusicBrainz
return null;
}
public async Task<OperationResult<IEnumerable<ArtistSearchResult>>> PerformArtistSearch(string query,
int resultsCount)
public async Task<OperationResult<IEnumerable<ArtistSearchResult>>> PerformArtistSearch(string query, int resultsCount)
{
ArtistSearchResult result = null;
try
Logger.LogTrace("MusicBrainzProvider:PerformArtistSearch:{0}", query);
var a = await Repository.ArtistByName(query, resultsCount);
if (a != null)
{
Logger.LogTrace("MusicBrainzProvider:PerformArtistSearch:{0}", query);
// Find the Artist
var artistCacheKey = string.Format("uri:musicbrainz:ArtistSearchResult:{0}", query);
result = CacheManager.Get<ArtistSearchResult>(artistCacheKey);
if (result == null)
var imageUrls = a.relations?.Where(x => x.type.Equals("image", StringComparison.OrdinalIgnoreCase)).Select(x => x.url.resource).Distinct().ToArray();
var notImageUrls = a.relations?.Where(x => !x.type.Equals("image", StringComparison.OrdinalIgnoreCase)).Select(x => x.url.resource).Distinct().ToArray();
var discogRelation = a.relations?.FirstOrDefault(x => x.type.Equals("discogs", StringComparison.OrdinalIgnoreCase));
var discogId = discogRelation?.url?.resource?.LastSegmentInUrl();
var amgRelation = a.relations?.FirstOrDefault(x => x.type.Equals("allmusic", StringComparison.OrdinalIgnoreCase));
var amgId = amgRelation?.url?.resource?.LastSegmentInUrl();
var lastFmRelation = a.relations?.FirstOrDefault(x => x.type.Equals("last.fm", StringComparison.OrdinalIgnoreCase));
var lastFmId = lastFmRelation?.url?.resource?.LastSegmentInUrl();
var iTunesRelation = a.relations?.FirstOrDefault(x => x.url?.resource?.StartsWith("https://itunes.apple.com/") ?? false);
var iTunesId = iTunesRelation?.url?.resource?.LastSegmentInUrl();
var spotifyRelation = a.relations?.FirstOrDefault(x => x.url?.resource?.StartsWith("https://open.spotify.com/artist/") ?? false);
var spotifyId = spotifyRelation?.url?.resource?.LastSegmentInUrl();
result = new ArtistSearchResult
{
ArtistResult artistResult = null;
try
{
artistResult = await MusicBrainzRequestHelper.GetAsync<ArtistResult>(
MusicBrainzRequestHelper.CreateSearchTemplate("artist", query, resultsCount, 0));
}
catch (Exception ex)
{
Logger.LogError(ex);
}
if (artistResult == null || artistResult.artists == null || artistResult.count < 1)
return new OperationResult<IEnumerable<ArtistSearchResult>>();
var a = artistResult.artists.First();
var mbArtist = await MusicBrainzRequestHelper.GetAsync<Artist>(
MusicBrainzRequestHelper.CreateLookupUrl("artist", artistResult.artists.First().id,
"releases"));
if (mbArtist == null) return new OperationResult<IEnumerable<ArtistSearchResult>>();
result = new ArtistSearchResult
{
ArtistName = mbArtist.name,
ArtistSortName = mbArtist.sortname,
MusicBrainzId = mbArtist.id,
ArtistType = mbArtist.type,
IPIs = mbArtist.ipis,
ISNIs = mbArtist.isnis
};
if (mbArtist.lifespan != null)
{
result.BeginDate = SafeParser.ToDateTime(mbArtist.lifespan.begin);
result.EndDate = SafeParser.ToDateTime(mbArtist.lifespan.end);
}
if (a.aliases != null) result.AlternateNames = a.aliases.Select(x => x.name).Distinct().ToArray();
if (a.tags != null) result.Tags = a.tags.Select(x => x.name).Distinct().ToArray();
var mbFilteredReleases = new List<Release>();
var filteredPlaces = new List<string> { "US", "WORLDWIDE", "XW", "GB" };
foreach (var release in mbArtist.releases)
if (filteredPlaces.Contains((release.country ?? string.Empty).ToUpper()))
mbFilteredReleases.Add(release);
result.Releases = new List<ReleaseSearchResult>();
var bag = new ConcurrentBag<Release>();
var filteredReleaseDetails = mbFilteredReleases.Select(async release =>
{
bag.Add(await MusicBrainzReleaseById(release.id));
});
await Task.WhenAll(filteredReleaseDetails);
foreach (var mbRelease in bag.Where(x => x != null))
{
var release = new ReleaseSearchResult
{
MusicBrainzId = mbRelease.id,
ReleaseTitle = mbRelease.title,
ReleaseThumbnailUrl = mbRelease.coverThumbnailUrl
};
if (mbRelease.imageUrls != null) release.ImageUrls = mbRelease.imageUrls;
if (mbRelease.releaseevents != null)
release.ReleaseDate = SafeParser.ToDateTime(mbRelease.releaseevents.First().date);
// Labels
if (mbRelease.media != null)
{
var releaseMedias = new List<ReleaseMediaSearchResult>();
foreach (var mbMedia in mbRelease.media)
{
var releaseMedia = new ReleaseMediaSearchResult
{
ReleaseMediaNumber = SafeParser.ToNumber<short?>(mbMedia.position),
TrackCount = mbMedia.trackcount
};
if (mbMedia.tracks != null)
{
var releaseTracks = new List<TrackSearchResult>();
foreach (var mbTrack in mbMedia.tracks)
releaseTracks.Add(new TrackSearchResult
{
MusicBrainzId = mbTrack.id,
TrackNumber = SafeParser.ToNumber<short?>(mbTrack.number),
Title = mbTrack.title,
Duration = mbTrack.length
});
releaseMedia.Tracks = releaseTracks;
}
releaseMedias.Add(releaseMedia);
}
release.ReleaseMedia = releaseMedias;
}
result.Releases.Add(release);
}
;
CacheManager.Add(artistCacheKey, result);
}
ArtistName = a.name,
ArtistSortName = a.sortname,
MusicBrainzId = a.id,
ArtistType = a.type,
ArtistGenres = a.genres?.Select(x => x.name).ToArray(),
AlternateNames = a.aliases?.Select(x => x.name).Distinct().ToArray(),
ArtistRealName = a.aliases?.FirstOrDefault(x => x.type == "Legal name")?.name,
BeginDate = (a.type ?? string.Empty) == "group" ? SafeParser.ToDateTime(a.lifespan?.begin) : null,
BirthDate = (a.type ?? string.Empty) == "group" ? null : SafeParser.ToDateTime(a.lifespan?.begin),
ImageUrls = imageUrls,
Urls = notImageUrls,
LastFMId = lastFmId,
AmgId = amgId,
iTunesId = iTunesId,
DiscogsId = discogId,
SpotifyId = spotifyId,
EndDate = SafeParser.ToDateTime(a.lifespan?.end),
Tags = a.tags?.Select(x => x.name).Distinct().ToArray(),
IPIs = a.ipis,
ISNIs = a.isnilist?.Select(x => x.isni).ToArray()
};
Logger.LogTrace($"MusicBrainzArtist: ArtistName [{ query }], MbId [{ result.MusicBrainzId }], DiscogId [{ result.DiscogsId }], LastFMId [{ result.LastFMId }], AmgId [{ result.AmgId }], Itunes [{ result.iTunesId }], Spotify [{ result.SpotifyId }]");
}
catch (HttpRequestException)
{
}
catch (Exception ex)
{
Logger.LogError(ex);
}
if (result == null)
Logger.LogWarning("MusicBrainzArtist: ArtistName [{0}], No MusicBrainz Artist Found", query);
else
Logger.LogTrace("MusicBrainzArtist: Result [{0}]", query, result.ToString());
{
Logger.LogWarning("MusicBrainzArtist: ArtistName [{0}], No MusicBrainz Artist Found", query);
}
return new OperationResult<IEnumerable<ArtistSearchResult>>
{
IsSuccess = result != null,
@ -286,8 +169,7 @@ namespace Roadie.Library.MetaData.MusicBrainz
};
}
public async Task<OperationResult<IEnumerable<ReleaseSearchResult>>> PerformReleaseSearch(string artistName,
string query, int resultsCount)
public async Task<OperationResult<IEnumerable<ReleaseSearchResult>>> PerformReleaseSearch(string artistName, string query, int resultsCount)
{
ReleaseSearchResult result = null;
try
@ -295,74 +177,96 @@ namespace Roadie.Library.MetaData.MusicBrainz
var releaseInfosForArtist = await ReleasesForArtist(artistName);
if (releaseInfosForArtist != null)
{
var releaseInfo = releaseInfosForArtist.FirstOrDefault(x =>
x.title.Equals(query, StringComparison.OrdinalIgnoreCase));
if (releaseInfo != null)
var r = releaseInfosForArtist.FirstOrDefault(x => x.title.Equals(query, StringComparison.OrdinalIgnoreCase));
if (r != null)
{
var mbRelease = await MusicBrainzReleaseById(releaseInfo.id);
if (mbRelease != null)
var imageUrls = r.relations?.Where(x => x.type.Equals("image", StringComparison.OrdinalIgnoreCase)).Select(x => x.url.resource).Distinct().ToArray();
var notImageUrls = r.relations?.Where(x => !x.type.Equals("image", StringComparison.OrdinalIgnoreCase)).Select(x => x.url.resource).Distinct().ToArray();
var discogRelation = r.relations?.FirstOrDefault(x => x.type.Equals("discogs", StringComparison.OrdinalIgnoreCase));
var discogId = discogRelation?.url?.resource?.LastSegmentInUrl();
var amgRelation = r.relations?.FirstOrDefault(x => x.type.Equals("allmusic", StringComparison.OrdinalIgnoreCase));
var amgId = amgRelation?.url?.resource?.LastSegmentInUrl();
var lastFmRelation = r.relations?.FirstOrDefault(x => x.type.Equals("last.fm", StringComparison.OrdinalIgnoreCase));
var lastFmId = lastFmRelation?.url?.resource?.LastSegmentInUrl();
var iTunesRelation = r.relations?.FirstOrDefault(x => x.url?.resource?.StartsWith("https://itunes.apple.com/") ?? false);
var iTunesId = iTunesRelation?.url?.resource?.LastSegmentInUrl();
var spotifyRelation = r.relations?.FirstOrDefault(x => x.url?.resource?.StartsWith("https://open.spotify.com/artist/") ?? false);
var spotifyId = spotifyRelation?.url?.resource?.LastSegmentInUrl();
result = new ReleaseSearchResult
{
result = new ReleaseSearchResult
ReleaseDate = r.releasegroup != null ? SafeParser.ToDateTime(r.releasegroup.firstreleasedate) : null,
ReleaseTitle = r.title,
ImageUrls = imageUrls,
Urls = notImageUrls,
LastFMId = lastFmId,
AmgId = amgId,
iTunesId = iTunesId,
DiscogsId = discogId,
SpotifyId = spotifyId,
MusicBrainzId = r.id,
Tags = r.releasegroup?.tags?.Select(x => x.name).Distinct().ToArray(),
ReleaseGenres = r.releasegroup?.genres?.Select(x => x.name).Distinct().ToArray(),
ReleaseType = r.releasegroup?.primarytype
};
var coverUrls = await CoverArtForMusicBrainzReleaseById(r.id);
if (coverUrls != null)
{
var frontCover = coverUrls.images.FirstOrDefault(i => i.front);
result.ImageUrls = coverUrls.images.Select(x => x.image).ToList();
if (frontCover != null)
{
ReleaseDate = mbRelease.releasegroup != null
? SafeParser.ToDateTime(mbRelease.releasegroup.firstreleasedate)
: null,
ReleaseTitle = mbRelease.title,
MusicBrainzId = mbRelease.id,
ReleaseType = mbRelease.releasegroup != null ? mbRelease.releasegroup.primarytype : null
};
if (mbRelease.labelinfo != null)
{
var releaseLabels = new List<ReleaseLabelSearchResult>();
foreach (var mbLabel in mbRelease.labelinfo)
releaseLabels.Add(new ReleaseLabelSearchResult
{
CatalogNumber = mbLabel.catalognumber,
Label = new LabelSearchResult
{
LabelName = mbLabel.label.name,
MusicBrainzId = mbLabel.label.id,
LabelSortName = mbLabel.label.sortname,
AlternateNames = mbLabel.label.aliases.Select(x => x.name).ToList()
}
});
result.ReleaseLabel = releaseLabels;
result.ReleaseThumbnailUrl = frontCover.image;
result.ImageUrls = result.ImageUrls.Where(x => x != result.ReleaseThumbnailUrl).ToList();
}
if (mbRelease.media != null)
{
var releaseMedia = new List<ReleaseMediaSearchResult>();
foreach (var mbMedia in mbRelease.media.OrderBy(x => x.position))
}
if (r.labelinfo != null)
{
var releaseLabels = new List<ReleaseLabelSearchResult>();
foreach (var mbLabel in r.labelinfo)
releaseLabels.Add(new ReleaseLabelSearchResult
{
var mediaTracks = new List<TrackSearchResult>();
short trackLooper = 0;
foreach (var mbTrack in mbMedia.tracks.OrderBy(x => x.position))
CatalogNumber = mbLabel.catalognumber,
Label = new LabelSearchResult
{
trackLooper++;
mediaTracks.Add(new TrackSearchResult
{
Title = mbTrack.title,
TrackNumber = trackLooper,
Duration = mbTrack.length,
MusicBrainzId = mbTrack.id,
AlternateNames =
mbTrack.recording != null && mbTrack.recording.aliases != null
? mbTrack.recording.aliases.Select(x => x.name).ToList()
: null
});
LabelName = mbLabel.label.name,
MusicBrainzId = mbLabel.label.id,
LabelSortName = mbLabel.label.sortname,
AlternateNames = mbLabel.label.aliases.Select(x => x.name).ToList()
}
});
result.ReleaseLabel = releaseLabels;
}
releaseMedia.Add(new ReleaseMediaSearchResult
if (r.media != null)
{
var releaseMedia = new List<ReleaseMediaSearchResult>();
foreach (var mbMedia in r.media.OrderBy(x => x.position))
{
var mediaTracks = new List<TrackSearchResult>();
short trackLooper = 0;
foreach (var mbTrack in mbMedia.tracks.OrderBy(x => x.position))
{
trackLooper++;
mediaTracks.Add(new TrackSearchResult
{
ReleaseMediaNumber = SafeParser.ToNumber<short?>(mbMedia.position),
ReleaseMediaSubTitle = mbMedia.title,
TrackCount = SafeParser.ToNumber<short?>(mbMedia.trackcount),
Tracks = mediaTracks
Title = mbTrack.title,
TrackNumber = trackLooper,
Duration = mbTrack.length,
MusicBrainzId = mbTrack.id,
AlternateNames = mbTrack.recording != null && mbTrack.recording.aliases != null ? mbTrack.recording.aliases.Select(x => x.name).ToList() : null
});
}
result.ReleaseMedia = releaseMedia;
releaseMedia.Add(new ReleaseMediaSearchResult
{
ReleaseMediaNumber = SafeParser.ToNumber<short?>(mbMedia.position),
ReleaseMediaSubTitle = mbMedia.title,
TrackCount = SafeParser.ToNumber<short?>(mbMedia.trackcount),
Tracks = mediaTracks
});
}
result.ReleaseMedia = releaseMedia;
}
}
}
@ -373,11 +277,13 @@ namespace Roadie.Library.MetaData.MusicBrainz
}
if (result == null)
Logger.LogWarning(
"MusicBrainzArtist: ArtistName [{0}], ReleaseTitle [{0}], No MusicBrainz Release Found", artistName,
query);
{
Logger.LogWarning("MusicBrainzArtist: ArtistName [{0}], ReleaseTitle [{0}], No MusicBrainz Release Found", artistName, query);
}
else
{
Logger.LogTrace("MusicBrainzArtist: Result [{0}]", query, result.ToString());
}
return new OperationResult<IEnumerable<ReleaseSearchResult>>
{
IsSuccess = result != null,
@ -385,81 +291,28 @@ namespace Roadie.Library.MetaData.MusicBrainz
};
}
public async Task<Data.Release> ReleaseForMusicBrainzReleaseById(string musicBrainzId)
private async Task<CoverArtArchivesResult> CoverArtForMusicBrainzReleaseById(string musicBrainzId)
{
var release = await MusicBrainzReleaseById(musicBrainzId);
if (release == null) return null;
var media = release.media.First();
if (media == null) return null;
var result = new Data.Release
{
Title = release.title.ToTitleCase(false),
ReleaseDate = SafeParser.ToDateTime(release.date),
MusicBrainzId = release.id
};
var releaseMedia = new ReleaseMedia
{
Tracks = media.tracks.Select(m => new Data.Track
{
TrackNumber = SafeParser.ToNumber<short>(m.position ?? m.number),
Title = m.title.ToTitleCase(false),
MusicBrainzId = m.id
}).ToList()
};
result.Medias = new List<ReleaseMedia> { releaseMedia };
return result;
return await MusicBrainzRequestHelper.GetAsync<CoverArtArchivesResult>(MusicBrainzRequestHelper.CreateCoverArtReleaseUrl(musicBrainzId));
}
public async Task<IEnumerable<Release>> ReleasesForArtist(string artist, string artistMusicBrainzId = null)
private async Task<IEnumerable<Release>> ReleasesForArtist(string artist, string artistMusicBrainzId = null)
{
try
if (string.IsNullOrEmpty(artistMusicBrainzId))
{
var artistSearch = await PerformArtistSearch(artist, 1);
if (artistSearch == null || !artistSearch.IsSuccess) return null;
if (artistSearch == null || !artistSearch.IsSuccess)
{
return null;
}
var mbArtist = artistSearch.Data.First();
if (string.IsNullOrEmpty(artistMusicBrainzId))
if (mbArtist == null)
{
if (mbArtist == null) return null;
artistMusicBrainzId = mbArtist.MusicBrainzId;
return null;
}
var cacheKey = string.Format("uri:musicbrainz:ReleasesForArtist:{0}", artistMusicBrainzId);
var result = CacheManager.Get<List<Release>>(cacheKey);
if (result == null)
{
var pageSize = 50;
var page = 0;
var url = MusicBrainzRequestHelper.CreateArtistBrowseTemplate(artistMusicBrainzId, pageSize, 0);
var mbReleaseBrowseResult = await MusicBrainzRequestHelper.GetAsync<ReleaseBrowseResult>(url);
var totalReleases = mbReleaseBrowseResult != null ? mbReleaseBrowseResult.releasecount : 0;
var totalPages = Math.Ceiling((decimal)totalReleases / pageSize);
result = new List<Release>();
do
{
if (mbReleaseBrowseResult != null) result.AddRange(mbReleaseBrowseResult.releases);
page++;
mbReleaseBrowseResult = await MusicBrainzRequestHelper.GetAsync<ReleaseBrowseResult>(
MusicBrainzRequestHelper.CreateArtistBrowseTemplate(artistMusicBrainzId, pageSize,
pageSize * page));
} while (page < totalPages);
result = result.OrderBy(x => x.date).ThenBy(x => x.title).ToList();
CacheManager.Add(cacheKey, result);
}
return result;
artistMusicBrainzId = mbArtist.MusicBrainzId;
}
catch (HttpRequestException)
{
}
catch (Exception ex)
{
Logger.LogError(ex);
}
return null;
return await Repository.ReleasesForArtist(artistMusicBrainzId);
}
}
}

View file

@ -0,0 +1,145 @@
using LiteDB;
using Microsoft.Extensions.Logging;
using Roadie.Library.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace Roadie.Library.MetaData.MusicBrainz
{
public class MusicBrainzRepository
{
private string FileName { get; }
private ILogger<MusicBrainzProvider> Logger { get; }
public MusicBrainzRepository(IRoadieSettings configuration, ILogger<MusicBrainzProvider> logger)
{
Logger = logger;
var location = System.Reflection.Assembly.GetEntryAssembly().Location;
var directory = configuration.SearchEngineReposFolder ?? Path.Combine(System.IO.Path.GetDirectoryName(location), "SearchEngineRepos");
if(!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
FileName = Path.Combine(directory, "MusicBrainzRespository.db");
}
/// <summary>
/// Return if artist exists in repository, if not fetch from MusicBrainz and populate then return
/// </summary>
/// <param name="name">Query name of Artist</param>
/// <param name="resultsCount">Maximum Number of Results</param>
public async Task<Artist> ArtistByName(string name, int? resultsCount = null)
{
Artist result = null;
try
{
using (var db = new LiteDatabase(FileName))
{
var col = db.GetCollection<RepositoryArtist>("artists");
col.EnsureIndex(x => x.ArtistName);
col.EnsureIndex(x => x.ArtistMbId);
var artist = col.Find(x => x.ArtistName.Equals(name, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
if (artist == null)
{
// Perform a query to get the MbId for the Name
var artistResult = await MusicBrainzRequestHelper.GetAsync<ArtistResult>(MusicBrainzRequestHelper.CreateSearchTemplate("artist", name, resultsCount ?? 1, 0));
if (artistResult == null || artistResult.artists == null || !artistResult.artists.Any() || artistResult.count < 1)
{
return null;
}
var mbId = artistResult.artists.First().id;
// Now perform a detail request to get the details by the MbId
result = await MusicBrainzRequestHelper.GetAsync<Artist>(MusicBrainzRequestHelper.CreateLookupUrl("artist", mbId, "aliases+tags+genres+url-rels"));
if (result != null)
{
col.Insert(new RepositoryArtist
{
ArtistName = name,
ArtistMbId = result.id,
Artist = result
});
}
}
else
{
result = artist.Artist;
}
}
}
catch (HttpRequestException ex)
{
Logger.LogTrace($"MusicBrainzArtist: ArtistName [{ name }], HttpRequestException: [{ ex.ToString() }] ");
}
catch (Exception ex)
{
Logger.LogError(ex);
}
return result;
}
/// <summary>
/// Return if releases exist in repository for artist, if not fetch from MusicBrainz and populate then return
/// </summary>
/// <param name="artistMbId">Artist Music Brainz Id</param>
/// <returns>Collection of Music Brainz Releases</returns>
public async Task<IEnumerable<Release>> ReleasesForArtist(string artistMbId)
{
IEnumerable<Release> results = null;
try
{
using (var db = new LiteDatabase(FileName))
{
var col = db.GetCollection<RepositoryRelease>("releases");
col.EnsureIndex(x => x.ArtistMbId);
col.EnsureIndex(x => x.Release.id);
var releases = col.Find(x => x.ArtistMbId == artistMbId);
if(releases == null || !releases.Any())
{
// Query to get collection of Releases for Artist
var pageSize = 50;
var page = 0;
var url = MusicBrainzRequestHelper.CreateArtistBrowseTemplate(artistMbId, pageSize, 0);
var mbReleaseBrowseResult = await MusicBrainzRequestHelper.GetAsync<ReleaseBrowseResult>(url);
var totalReleases = mbReleaseBrowseResult != null ? mbReleaseBrowseResult.releasecount : 0;
var totalPages = Math.Ceiling((decimal)totalReleases / pageSize);
var fetchResult = new List<Release>();
do
{
if (mbReleaseBrowseResult != null)
{
fetchResult.AddRange(mbReleaseBrowseResult.releases.Where(x => !string.IsNullOrEmpty(x.date)));
}
page++;
mbReleaseBrowseResult = await MusicBrainzRequestHelper.GetAsync<ReleaseBrowseResult>(MusicBrainzRequestHelper.CreateArtistBrowseTemplate(artistMbId, pageSize, pageSize * page));
} while (page < totalPages);
col.InsertBulk(fetchResult.Select(x => new RepositoryRelease
{
ArtistMbId = artistMbId,
Release = x
}));
results = fetchResult;
}
else
{
results = releases.Select(x => x.Release).ToArray();
}
}
}
catch (HttpRequestException ex)
{
Logger.LogTrace($"MusicBrainz:ReleasesForArtist, Artist [{ artistMbId }], Ex [{ ex.ToString() }]");
}
catch (Exception ex)
{
Logger.LogError(ex);
}
return results.OrderBy(x => x.date).ThenBy(x => x.title).ToArray();
}
}
}

View file

@ -1,6 +1,7 @@
using Newtonsoft.Json;
using Roadie.Library.Utility;
using System;
using System.Diagnostics;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
@ -11,13 +12,13 @@ namespace Roadie.Library.MetaData.MusicBrainz
{
private const string LookupTemplate = "{0}/{1}/?inc={2}&fmt=json&limit=100";
private const int MaxRetries = 6;
private const string ReleaseBrowseTemplate = "release?artist={0}&limit={1}&offset={2}&fmt=json";
private const string ReleaseBrowseTemplate = "release?artist={0}&limit={1}&offset={2}&fmt=json&inc={3}";
private const string SearchTemplate = "{0}?query={1}&limit={2}&offset={3}&fmt=json";
private const string WebServiceUrl = "http://musicbrainz.org/ws/2/";
internal static string CreateArtistBrowseTemplate(string id, int limit, int offset)
{
return string.Format("{0}{1}", WebServiceUrl, string.Format(ReleaseBrowseTemplate, id, limit, offset));
return string.Format("{0}{1}", WebServiceUrl, string.Format(ReleaseBrowseTemplate, id, limit, offset, "labels+aliases+recordings+release-groups+media+url-rels+tags+genres"));
}
internal static string CreateCoverArtReleaseUrl(string musicBrainzId)
@ -47,24 +48,35 @@ namespace Roadie.Library.MetaData.MusicBrainz
{
var tryCount = 0;
var result = default(T);
while (tryCount < MaxRetries && result == null)
while (result == null && tryCount < MaxRetries)
{
try
{
using (var webClient = new WebClient())
{
webClient.Headers.Add("user-agent", WebHelper.UserAgent);
result = JsonConvert.DeserializeObject<T>(
await webClient.DownloadStringTaskAsync(new Uri(url)));
result = JsonConvert.DeserializeObject<T>(await webClient.DownloadStringTaskAsync(new Uri(url)));
}
}
catch
catch (WebException ex)
{
var response = ex.Response as HttpWebResponse;
if(response?.StatusCode == HttpStatusCode.NotFound)
{
Trace.WriteLine($"GetAsync: 404 Response For url [{ url }]");
return result;
}
}
catch (Exception ex)
{
Trace.WriteLine($"GetAsync: [{ ex.ToString() }]");
Thread.Sleep(100);
}
finally
{
tryCount++;
}
}
return result;
}

View file

@ -0,0 +1,153 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
namespace Roadie.Library.MetaData.MusicBrainz
{
[Serializable]
public class ArtistResult
{
public DateTime created { get; set; }
public int count { get; set; }
public int offset { get; set; }
public Artist[] artists { get; set; }
}
[Serializable]
public class RepositoryArtist
{
public int Id { get; set; }
public string ArtistName { get; set; }
public string ArtistMbId { get; set; }
public Artist Artist { get; set; }
}
[DebuggerDisplay("name: {name}")]
[Serializable]
public class Artist
{
public string id { get; set; }
public string type { get; set; }
[JsonProperty("type-id")]
public string typeid { get; set; }
public int score { get; set; }
public string name { get; set; }
[JsonProperty("sort-name")]
public string sortname { get; set; }
public string gender { get; set; }
public string country { get; set; }
public Area area { get; set; }
[JsonProperty("begin_area")]
public BeginAndEndArea beginarea { get; set; }
[JsonProperty("end_area")]
public BeginAndEndArea endarea { get; set; }
public string[] ipis { get; set; }
[JsonProperty("life-span")]
public LifeSpan lifespan { get; set; }
public Alias[] aliases { get; set; }
public NameAndCount[] tags { get; set; }
public IsniList[] isnilist { get; set; }
public NameAndCount[] genres { get; set; }
public Relation[] relations { get; set; }
}
[Serializable]
public class Relations
{
[JsonProperty("attribute-ids")]
public string[] attributeids { get; set; }
public string[] attributes { get; set; }
public string direction { get; set; }
[JsonProperty("target-credit")]
public string targetcredit { get; set; }
[JsonProperty("type-id")]
public string typeid { get; set; }
[JsonProperty("target-type")]
public string targettype { get; set; }
public string type { get; set; }
public bool ended { get; set; }
public Url url { get; set; }
public string[] attributevalues { get; set; }
[JsonProperty("source-credit")]
public string sourcecredit { get; set; }
public object end { get; set; }
public object begin { get; set; }
}
[Serializable]
public class Url
{
public string resource { get; set; }
public string id { get; set; }
}
[Serializable]
public class Area
{
public string id { get; set; }
public string type { get; set; }
[JsonProperty("type-id")]
public string typeid { get; set; }
public string name { get; set; }
[JsonProperty("sort-name")]
public string sortname { get; set; }
[JsonProperty("life-span")]
public LifeSpan lifespan { get; set; }
[JsonProperty("iso-3166-1-codes")]
public string[] iso31661codes { get; set; }
}
[Serializable]
public class LifeSpan
{
public string end { get; set; }
public string ended { get; set; }
public string begin { get; set; }
}
[Serializable]
public class BeginAndEndArea
{
public string id { get; set; }
public string type { get; set; }
public string typeid { get; set; }
public string name { get; set; }
[JsonProperty("sort-name")]
public string sortname { get; set; }
[JsonProperty("life-span")]
public LifeSpan lifespan { get; set; }
}
[Serializable]
public class Alias
{
[JsonProperty("sort-name")]
public string sortname { get; set; }
[JsonProperty("type-id")]
public string typeid { get; set; }
public string name { get; set; }
public string locale { get; set; }
public string type { get; set; }
public string primary { get; set; }
public string begin { get; set; }
public string end { get; set; }
public bool ended { get; set; }
}
[Serializable]
public class IsniList
{
public string isni { get; set; }
}
[Serializable]
public class NameAndCount
{
public int? count { get; set; }
public string name { get; set; }
}
}

View file

@ -21,24 +21,21 @@ namespace Roadie.Library.SearchEngines.MetaData.Wikipedia
HttpEncoder = httpEncoder;
}
public Task<OperationResult<IEnumerable<ArtistSearchResult>>> PerformArtistSearch(string query,
int resultsCount)
public Task<OperationResult<IEnumerable<ArtistSearchResult>>> PerformArtistSearch(string query, int resultsCount)
{
var tcs = new TaskCompletionSource<OperationResult<IEnumerable<ArtistSearchResult>>>();
var client =
new RestClient(
"https://en.wikipedia.org/w/api.php?format=xml&action=query&redirects=1&prop=extracts&exintro=&explaintext=&titles=" +
HttpEncoder.UrlEncode(query));
var client = new RestClient("https://en.wikipedia.org/w/api.php?format=xml&action=query&redirects=1&prop=extracts&exintro=&explaintext=&titles=" + HttpEncoder.UrlEncode(query ?? string.Empty));
var request = new RestRequest(Method.GET);
client.ExecuteAsync<api>(request, response =>
{
ArtistSearchResult data = null;
if (response != null && response.Data != null && response.Data.query != null &&
response.Data.query.pages != null)
if (response?.Data?.query?.pages != null)
{
data = new ArtistSearchResult
{
Bio = response.Data.query.pages.First().extract
};
}
tcs.SetResult(new OperationResult<IEnumerable<ArtistSearchResult>>
{
IsSuccess = data != null,
@ -53,19 +50,18 @@ namespace Roadie.Library.SearchEngines.MetaData.Wikipedia
{
var tcs = new TaskCompletionSource<OperationResult<IEnumerable<ReleaseSearchResult>>>();
var client =
new RestClient(
"https://en.wikipedia.org/w/api.php?format=xml&action=query&redirects=1&prop=extracts&exintro=&explaintext=&titles=" +
HttpEncoder.UrlEncode(query) + " (album)");
var client = new RestClient("https://en.wikipedia.org/w/api.php?format=xml&action=query&redirects=1&prop=extracts&exintro=&explaintext=&titles=" + HttpEncoder.UrlEncode(query ?? string.Empty) + " (album)");
var request = new RestRequest(Method.GET);
client.ExecuteAsync<api>(request, response =>
{
ReleaseSearchResult data = null;
if (response.Data != null)
if (response?.Data?.query?.pages != null)
{
data = new ReleaseSearchResult
{
Bio = response.Data.query.pages.First().extract
};
}
tcs.SetResult(new OperationResult<IEnumerable<ReleaseSearchResult>>
{
IsSuccess = data != null,

View file

@ -74,19 +74,25 @@ namespace Roadie.Library.Utility
{
}
if (imageBytes != null)
try
{
var signature = ImageHasher.AverageHash(imageBytes).ToString();
var ib = ImageHelper.ConvertToJpegFormat(imageBytes);
return new Image
if (imageBytes != null)
{
Url = url,
Status = Statuses.New,
Signature = signature,
Bytes = ib
};
var signature = ImageHasher.AverageHash(imageBytes).ToString();
var ib = ImageHelper.ConvertToJpegFormat(imageBytes);
return new Image
{
Url = url,
Status = Statuses.New,
Signature = signature,
Bytes = ib
};
}
}
catch (Exception ex)
{
Trace.WriteLine($"GetImageFromUrlAsync Url [{ url }], Exception [{ ex.ToString() }");
}
return null;
}

View file

@ -579,17 +579,40 @@ namespace Roadie.Api.Services
};
}
public async Task<OperationResult<bool>> ScanArtist(ApplicationUser user, Guid artistId,
bool isReadOnly = false)
public async Task<OperationResult<bool>> ScanArtists(ApplicationUser user, IEnumerable<Guid> artistIds, bool isReadOnly = false)
{
var sw = new Stopwatch();
sw.Start();
var sw = Stopwatch.StartNew();
var errors = new List<Exception>();
foreach (var artistId in artistIds)
{
var result = await ScanArtist(user, artistId, isReadOnly);
if (!result.IsSuccess)
{
if (result.Errors?.Any() ?? false)
{
errors.AddRange(result.Errors);
}
}
}
sw.Stop();
return new OperationResult<bool>
{
IsSuccess = !errors.Any(),
OperationTime = sw.ElapsedMilliseconds,
Errors = errors
};
}
public async Task<OperationResult<bool>> ScanArtist(ApplicationUser user, Guid artistId, bool isReadOnly = false)
{
var sw = Stopwatch.StartNew();
var errors = new List<Exception>();
var artist = DbContext.Artists.FirstOrDefault(x => x.RoadieId == artistId);
if (artist == null)
{
await LogAndPublish($"ScanArtist Unknown Release [{artistId}]", LogLevel.Warning);
await LogAndPublish($"ScanArtist Unknown Artist [{artistId}]", LogLevel.Warning);
return new OperationResult<bool>(true, $"Artist Not Found [{artistId}]");
}

View file

@ -292,11 +292,8 @@ namespace Roadie.Api.Services
where !onlyWithReleases || a.ReleaseCount > 0
where request.FilterToArtistId == null || a.RoadieId == request.FilterToArtistId
where request.FilterMinimumRating == null || a.Rating >= request.FilterMinimumRating.Value
where request.FilterValue == "" || a.Name.Contains(request.FilterValue) ||
a.SortName.Contains(request.FilterValue) || a.AlternateNames.Contains(request.FilterValue) ||
a.AlternateNames.Contains(normalizedFilterValue)
where !isEqualFilter || a.Name.Equals(request.FilterValue) || a.SortName.Equals(request.FilterValue) ||
a.AlternateNames.Equals(request.FilterValue) || a.AlternateNames.Equals(normalizedFilterValue)
where request.FilterValue == "" || a.Name.Contains(request.FilterValue) || a.SortName.Contains(request.FilterValue) || a.AlternateNames.Contains(request.FilterValue) || a.AlternateNames.Contains(normalizedFilterValue)
where !isEqualFilter || a.Name.Equals(request.FilterValue) || a.SortName.Equals(request.FilterValue) || a.AlternateNames.Equals(request.FilterValue) || a.AlternateNames.Equals(normalizedFilterValue)
where !request.FilterFavoriteOnly || favoriteArtistIds.Contains(a.Id)
where request.FilterToLabelId == null || labelArtistIds.Contains(a.Id)
where !isFilteredToGenre || genreArtistIds.Contains(a.Id)
@ -333,13 +330,15 @@ namespace Roadie.Api.Services
{
string sortBy;
if (request.ActionValue == User.ActionKeyUserRated)
sortBy = string.IsNullOrEmpty(request.Sort)
? request.OrderValue(
new Dictionary<string, string> { { "Rating", "DESC" }, { "Artist.Text", "ASC" } })
{
sortBy = string.IsNullOrEmpty(request.Sort)
? request.OrderValue(new Dictionary<string, string> { { "Rating", "DESC" }, { "Artist.Text", "ASC" } })
: request.OrderValue();
}
else
sortBy = request.OrderValue(new Dictionary<string, string>
{{"SortName", "ASC"}, {"Artist.Text", "ASC"}});
{
sortBy = request.OrderValue(new Dictionary<string, string> {{"SortName", "ASC"}, {"Artist.Text", "ASC"}});
}
rows = result.OrderBy(sortBy).Skip(request.SkipValue).Take(request.LimitValue).ToArray();
}
@ -1340,12 +1339,17 @@ namespace Roadie.Api.Services
Logger.LogWarning("Unable To Find Artist [{0}]", artistId);
return new OperationResult<bool>();
}
var releaseScannedCount = 0;
var artistFolder = artist.ArtistFileFolder(Configuration);
if (!Directory.Exists(artistFolder))
{
Logger.LogDebug($"ScanArtistReleasesFolders: ArtistFolder Not Found [{ artistFolder }] For Artist `{ artist }`");
return new OperationResult<bool>();
}
var scannedArtistFolders = new List<string>();
// Scan known releases for changes
if (artist.Releases != null)
{
foreach (var release in artist.Releases)
try
{
@ -1357,7 +1361,7 @@ namespace Roadie.Api.Services
{
Logger.LogError(ex, ex.Serialize());
}
}
// Any folder found in Artist folder not already scanned scan
var nonReleaseFolders = from d in Directory.EnumerateDirectories(artistFolder)
where !(from r in scannedArtistFolders select r).Contains(d)
@ -1390,8 +1394,7 @@ namespace Roadie.Api.Services
sw.Stop();
CacheManager.ClearRegion(artist.CacheRegion);
Logger.LogInformation("Scanned Artist [{0}], Releases Scanned [{1}], OperationTime [{2}]",
artist.ToString(), releaseScannedCount, sw.ElapsedMilliseconds);
Logger.LogInformation("Scanned Artist [{0}], Releases Scanned [{1}], OperationTime [{2}]", artist.ToString(), releaseScannedCount, sw.ElapsedMilliseconds);
}
catch (Exception ex)
{

View file

@ -33,6 +33,8 @@ namespace Roadie.Api.Services
Task<OperationResult<bool>> ScanArtist(ApplicationUser user, Guid artistId, bool isReadOnly = false);
Task<OperationResult<bool>> ScanArtists(ApplicationUser user, IEnumerable<Guid> artistIds, bool isReadOnly = false);
Task<OperationResult<bool>> ScanCollection(ApplicationUser user, Guid collectionId, bool isReadOnly = false,
bool doPurgeFirst = false, bool doUpdateRanks = true);

View file

@ -66,7 +66,7 @@ namespace Roadie.Api.Services
public IEnumerable<int> AddedTrackIds => _addedTrackIds;
public ReleaseService(IRoadieSettings configuration,
public ReleaseService(IRoadieSettings configuration,
IHttpEncoder httpEncoder,
IHttpContext httpContext,
data.IRoadieDbContext dbContext,
@ -884,8 +884,7 @@ namespace Roadie.Api.Services
};
}
public async Task<OperationResult<bool>> DeleteReleases(ApplicationUser user, IEnumerable<Guid> releaseIds,
bool doDeleteFiles = false)
public async Task<OperationResult<bool>> DeleteReleases(ApplicationUser user, IEnumerable<Guid> releaseIds, bool doDeleteFiles = false)
{
SimpleContract.Requires<ArgumentNullException>(releaseIds != null && releaseIds.Any(),
"No Release Ids Found");
@ -904,10 +903,13 @@ namespace Roadie.Api.Services
foreach (var release in releases)
{
var defaultResult = await Delete(user, release, doDeleteFiles, false);
result = result & defaultResult.IsSuccess;
result &= defaultResult.IsSuccess;
}
foreach (var artistId in artistIds) await UpdateArtistCounts(artistId, now);
foreach (var artistId in artistIds)
{
await UpdateArtistCounts(artistId, now);
}
sw.Stop();
return new OperationResult<bool>
@ -1394,7 +1396,7 @@ namespace Roadie.Api.Services
if (releaseImage != null)
{
// Save unaltered image to cover file
var coverFileName = Path.Combine(release.ReleaseFileFolder(release.Artist.ArtistFileFolder(Configuration)),"cover.jpg");
var coverFileName = Path.Combine(release.ReleaseFileFolder(release.Artist.ArtistFileFolder(Configuration)), "cover.jpg");
File.WriteAllBytes(coverFileName, ImageHelper.ConvertToJpegFormat(releaseImage));
// Resize to store in database as thumbnail
@ -1479,7 +1481,7 @@ namespace Roadie.Api.Services
await DbContext.SaveChangesAsync();
await CheckAndChangeReleaseTitle(release, originalReleaseFolder);
CacheManager.ClearRegion(release.CacheRegion);
Logger.LogInformation( $"UpdateRelease `{release}` By User `{user}`: Edited Artist [{didChangeArtist}], Uploaded new image [{didChangeThumbnail}]");
Logger.LogInformation($"UpdateRelease `{release}` By User `{user}`: Edited Artist [{didChangeArtist}], Uploaded new image [{didChangeThumbnail}]");
}
catch (Exception ex)
{
@ -1510,7 +1512,7 @@ namespace Roadie.Api.Services
SimpleContract.Requires<ArgumentNullException>(release != null, "Invalid Release");
SimpleContract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(oldReleaseFolder), "Invalid Release Old Folder");
var sw = new Stopwatch();
var sw = new Stopwatch();
sw.Start();
var now = DateTime.UtcNow;
@ -1863,7 +1865,7 @@ namespace Roadie.Api.Services
if (release.Thumbnail != null)
{
// Save unaltered image to cover file
var coverFileName = Path.Combine(release.ReleaseFileFolder(release.Artist.ArtistFileFolder(Configuration)),"cover.jpg");
var coverFileName = Path.Combine(release.ReleaseFileFolder(release.Artist.ArtistFileFolder(Configuration)), "cover.jpg");
File.WriteAllBytes(coverFileName, ImageHelper.ConvertToJpegFormat(imageBytes));
// Resize to store in database as thumbnail

View file

@ -686,13 +686,13 @@ namespace Roadie.Api.Services
var artist = DbContext.Artists.FirstOrDefault(x => x.Id == artistId);
if (artist != null)
{
artist.ReleaseCount = DbContext.Releases.Where(x => x.ArtistId == artistId).Count();
artist.ReleaseCount = DbContext.Releases.Where(x => x.ArtistId == artistId).Count();
artist.TrackCount = (from r in DbContext.Releases
join rm in DbContext.ReleaseMedias on r.Id equals rm.ReleaseId
join tr in DbContext.Tracks on rm.Id equals tr.ReleaseMediaId
where tr.ArtistId == artistId || r.ArtistId == artistId
select tr).Count();
artist.LastUpdated = now;
await DbContext.SaveChangesAsync();
CacheManager.ClearRegion(artist.CacheRegion);
@ -704,7 +704,10 @@ namespace Roadie.Api.Services
/// </summary>
protected async Task UpdateArtistCountsForRelease(int releaseId, DateTime now)
{
foreach (var artistId in ArtistIdsForRelease(releaseId)) await UpdateArtistCounts(artistId, now);
foreach (var artistId in ArtistIdsForRelease(releaseId))
{
await UpdateArtistCounts(artistId, now);
}
}
/// <summary>

View file

@ -138,6 +138,16 @@ namespace Roadie.Api.Controllers
return Ok(result);
}
[HttpPost("scan/artists")]
[ProducesResponseType(200)]
public async Task<IActionResult> ScanArtists(IEnumerable<Guid> ids)
{
var result = await AdminService.ScanArtists(await UserManager.GetUserAsync(User), ids);
if (!result.IsSuccess) return StatusCode((int)HttpStatusCode.InternalServerError);
return Ok(result);
}
[HttpPost("scan/collection/{id}")]
[ProducesResponseType(200)]
public async Task<IActionResult> ScanCollection(Guid id)