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:
parent
93b4014b91
commit
aa11415e70
24 changed files with 793 additions and 519 deletions
Roadie.Api.Library.Tests
Roadie.Api.Library
Configuration
Engines
Extensions
Imaging
Roadie.Library.csprojSearchEngines/MetaData
MusicBrainz
Entities.csIMusicBrainzProvider.csMusicBrainzProvider.csMusicBrainzRepository.csMusicBrainzRequestHelper.csResultEntities.cs
Wikipedia
Utility
Roadie.Api.Services
Roadie.Api/Controllers
|
@ -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 }] ");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -41,5 +41,6 @@ namespace Roadie.Library.Configuration
|
|||
|
||||
bool IsRegistrationClosed { get; set; }
|
||||
bool UseRegistrationTokens { get; set; }
|
||||
string SearchEngineReposFolder { get; set; }
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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}]");
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue