Migrate MusicBrainz plugin to MetaBrainz.MusicBrainz
Co-authored-by: crobibero <cody@robibe.ro> Co-authored-by: Shadowghost <Shadowghost@users.noreply.github.com>
This commit is contained in:
parent
7eae6891c8
commit
7ad0c9ba24
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="LrcParser" Version="2022.529.1" />
|
<PackageReference Include="LrcParser" Version="2022.529.1" />
|
||||||
|
<PackageReference Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||||
|
|
|
@ -1,37 +1,58 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
|
using MetaBrainz.MusicBrainz;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Plugins.MusicBrainz
|
namespace MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MusicBrainz plugin configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class PluginConfiguration : BasePluginConfiguration
|
||||||
{
|
{
|
||||||
public class PluginConfiguration : BasePluginConfiguration
|
private const string DefaultServer = "musicbrainz.org";
|
||||||
|
|
||||||
|
private const double DefaultRateLimit = 1.0;
|
||||||
|
|
||||||
|
private string _server = DefaultServer;
|
||||||
|
|
||||||
|
private double _rateLimit = DefaultRateLimit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the server url.
|
||||||
|
/// </summary>
|
||||||
|
public string Server
|
||||||
{
|
{
|
||||||
private string _server = Plugin.DefaultServer;
|
get => _server;
|
||||||
|
|
||||||
private long _rateLimit = Plugin.DefaultRateLimit;
|
set
|
||||||
|
|
||||||
public string Server
|
|
||||||
{
|
{
|
||||||
get => _server;
|
_server = value.TrimEnd('/');
|
||||||
set => _server = value.TrimEnd('/');
|
Query.DefaultServer = _server;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long RateLimit
|
|
||||||
{
|
|
||||||
get => _rateLimit;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value < Plugin.DefaultRateLimit && _server == Plugin.DefaultServer)
|
|
||||||
{
|
|
||||||
_rateLimit = Plugin.DefaultRateLimit;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_rateLimit = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ReplaceArtistName { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the rate limit.
|
||||||
|
/// </summary>
|
||||||
|
public double RateLimit
|
||||||
|
{
|
||||||
|
get => _rateLimit;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value < DefaultRateLimit && _server == DefaultServer)
|
||||||
|
{
|
||||||
|
_rateLimit = DefaultRateLimit;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_rateLimit = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Query.DelayBetweenRequests = _rateLimit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to replace the artist name.
|
||||||
|
/// </summary>
|
||||||
|
public bool ReplaceArtistName { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,27 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using MediaBrowser.Providers.Plugins.MusicBrainz;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Music
|
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MusicBrainz album artist external id.
|
||||||
|
/// </summary>
|
||||||
|
public class MusicBrainzAlbumArtistExternalId : IExternalId
|
||||||
{
|
{
|
||||||
public class MusicBrainzAlbumArtistExternalId : IExternalId
|
/// <inheritdoc />
|
||||||
{
|
public string ProviderName => "MusicBrainz";
|
||||||
/// <inheritdoc />
|
|
||||||
public string ProviderName => "MusicBrainz";
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString();
|
public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist;
|
public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
|
public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Supports(IHasProviderIds item) => item is Audio;
|
public bool Supports(IHasProviderIds item) => item is Audio;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,27 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using MediaBrowser.Providers.Plugins.MusicBrainz;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Music
|
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MusicBrainz album external id.
|
||||||
|
/// </summary>
|
||||||
|
public class MusicBrainzAlbumExternalId : IExternalId
|
||||||
{
|
{
|
||||||
public class MusicBrainzAlbumExternalId : IExternalId
|
/// <inheritdoc />
|
||||||
{
|
public string ProviderName => "MusicBrainz";
|
||||||
/// <inheritdoc />
|
|
||||||
public string ProviderName => "MusicBrainz";
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Key => MetadataProvider.MusicBrainzAlbum.ToString();
|
public string Key => MetadataProvider.MusicBrainzAlbum.ToString();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
|
public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}";
|
public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/release/{0}";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
|
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,28 +1,27 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using MediaBrowser.Providers.Plugins.MusicBrainz;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Music
|
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MusicBrains Artist ExternalId.
|
||||||
|
/// </summary>
|
||||||
|
public class MusicBrainzArtistExternalId : IExternalId
|
||||||
{
|
{
|
||||||
public class MusicBrainzArtistExternalId : IExternalId
|
/// <inheritdoc />
|
||||||
{
|
public string ProviderName => "MusicBrainz";
|
||||||
/// <inheritdoc />
|
|
||||||
public string ProviderName => "MusicBrainz";
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Key => MetadataProvider.MusicBrainzArtist.ToString();
|
public string Key => MetadataProvider.MusicBrainzArtist.ToString();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
|
public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
|
public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Supports(IHasProviderIds item) => item is MusicArtist;
|
public bool Supports(IHasProviderIds item) => item is MusicArtist;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,7 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
@ -18,257 +10,159 @@ using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using MediaBrowser.Providers.Plugins.MusicBrainz;
|
using MediaBrowser.Providers.Music;
|
||||||
|
using MetaBrainz.MusicBrainz;
|
||||||
|
using MetaBrainz.MusicBrainz.Interfaces.Entities;
|
||||||
|
using MetaBrainz.MusicBrainz.Interfaces.Searches;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Music
|
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MusicBrainz artist provider.
|
||||||
|
/// </summary>
|
||||||
|
public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IDisposable
|
||||||
{
|
{
|
||||||
public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>
|
private readonly Query _musicBrainzQuery;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MusicBrainzArtistProvider"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public MusicBrainzArtistProvider()
|
||||||
{
|
{
|
||||||
public string Name => "MusicBrainz";
|
_musicBrainzQuery = new Query();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken)
|
public string Name => "MusicBrainz";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var artistId = searchInfo.GetMusicBrainzArtistId();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(artistId))
|
||||||
{
|
{
|
||||||
var musicBrainzId = searchInfo.GetMusicBrainzArtistId();
|
var artistResult = await _musicBrainzQuery.LookupArtistAsync(new Guid(artistId), Include.Artists, null, null, cancellationToken).ConfigureAwait(false);
|
||||||
|
return GetResultFromResponse(artistResult).SingleItemAsEnumerable();
|
||||||
if (!string.IsNullOrWhiteSpace(musicBrainzId))
|
|
||||||
{
|
|
||||||
var url = "/ws/2/artist/?query=arid:{0}" + musicBrainzId.ToString(CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
|
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
return GetResultsFromResponse(stream);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// They seem to throw bad request failures on any term with a slash
|
|
||||||
var nameToSearch = searchInfo.Name.Replace('/', ' ');
|
|
||||||
|
|
||||||
var url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=\"{0}\"&dismax=true", UrlEncode(nameToSearch));
|
|
||||||
|
|
||||||
using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false))
|
|
||||||
await using (var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
var results = GetResultsFromResponse(stream).ToList();
|
|
||||||
|
|
||||||
if (results.Count > 0)
|
|
||||||
{
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchInfo.Name.HasDiacritics())
|
|
||||||
{
|
|
||||||
// Try again using the search with accent characters url
|
|
||||||
url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=artistaccent:\"{0}\"", UrlEncode(nameToSearch));
|
|
||||||
|
|
||||||
using var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false);
|
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
return GetResultsFromResponse(stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Enumerable.Empty<RemoteSearchResult>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<RemoteSearchResult> GetResultsFromResponse(Stream stream)
|
var artistSearchResults = await _musicBrainzQuery.FindArtistsAsync($"\"{searchInfo.Name}\"", null, null, false, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
if (artistSearchResults.Results.Count > 0)
|
||||||
{
|
{
|
||||||
using var oReader = new StreamReader(stream, Encoding.UTF8);
|
return GetResultsFromResponse(artistSearchResults.Results);
|
||||||
var settings = new XmlReaderSettings()
|
|
||||||
{
|
|
||||||
ValidationType = ValidationType.None,
|
|
||||||
CheckCharacters = false,
|
|
||||||
IgnoreProcessingInstructions = true,
|
|
||||||
IgnoreComments = true
|
|
||||||
};
|
|
||||||
|
|
||||||
using var reader = XmlReader.Create(oReader, settings);
|
|
||||||
reader.MoveToContent();
|
|
||||||
reader.Read();
|
|
||||||
|
|
||||||
// Loop through each element
|
|
||||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
|
||||||
{
|
|
||||||
if (reader.NodeType == XmlNodeType.Element)
|
|
||||||
{
|
|
||||||
switch (reader.Name)
|
|
||||||
{
|
|
||||||
case "artist-list":
|
|
||||||
{
|
|
||||||
if (reader.IsEmptyElement)
|
|
||||||
{
|
|
||||||
reader.Read();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var subReader = reader.ReadSubtree();
|
|
||||||
return ParseArtistList(subReader).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
reader.Skip();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
reader.Read();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Enumerable.Empty<RemoteSearchResult>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<RemoteSearchResult> ParseArtistList(XmlReader reader)
|
if (searchInfo.Name.HasDiacritics())
|
||||||
{
|
{
|
||||||
reader.MoveToContent();
|
// Try again using the search with an accented characters query
|
||||||
reader.Read();
|
var artistAccentsSearchResults = await _musicBrainzQuery.FindArtistsAsync($"artistaccent:\"{searchInfo.Name}\"", null, null, false, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
// Loop through each element
|
if (artistAccentsSearchResults.Results.Count > 0)
|
||||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
|
||||||
{
|
{
|
||||||
if (reader.NodeType == XmlNodeType.Element)
|
return GetResultsFromResponse(artistAccentsSearchResults.Results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Enumerable.Empty<RemoteSearchResult>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<RemoteSearchResult> GetResultsFromResponse(IEnumerable<ISearchResult<IArtist>>? releaseSearchResults)
|
||||||
|
{
|
||||||
|
if (releaseSearchResults is null)
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var result in releaseSearchResults)
|
||||||
|
{
|
||||||
|
yield return GetResultFromResponse(result.Item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<RemoteSearchResult> GetResultsFromResponse(IEnumerable<IArtist>? releaseSearchResults)
|
||||||
|
{
|
||||||
|
if (releaseSearchResults is null)
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var result in releaseSearchResults)
|
||||||
|
{
|
||||||
|
yield return GetResultFromResponse(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RemoteSearchResult GetResultFromResponse(IArtist artist)
|
||||||
|
{
|
||||||
|
var searchResult = new RemoteSearchResult
|
||||||
|
{
|
||||||
|
Name = artist.Name,
|
||||||
|
ProductionYear = artist.LifeSpan?.Begin?.Year,
|
||||||
|
PremiereDate = artist.LifeSpan?.Begin?.NearestDate
|
||||||
|
};
|
||||||
|
|
||||||
|
searchResult.SetProviderId(MetadataProvider.MusicBrainzArtist, artist.Id.ToString());
|
||||||
|
|
||||||
|
return searchResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo info, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = new MetadataResult<MusicArtist> { Item = new MusicArtist() };
|
||||||
|
|
||||||
|
var musicBrainzId = info.GetMusicBrainzArtistId();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(musicBrainzId))
|
||||||
|
{
|
||||||
|
var searchResults = await GetSearchResults(info, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var singleResult = searchResults.FirstOrDefault();
|
||||||
|
|
||||||
|
if (singleResult != null)
|
||||||
|
{
|
||||||
|
musicBrainzId = singleResult.GetProviderId(MetadataProvider.MusicBrainzArtist);
|
||||||
|
result.Item.Overview = singleResult.Overview;
|
||||||
|
|
||||||
|
if (Plugin.Instance!.Configuration.ReplaceArtistName)
|
||||||
{
|
{
|
||||||
switch (reader.Name)
|
result.Item.Name = singleResult.Name;
|
||||||
{
|
|
||||||
case "artist":
|
|
||||||
{
|
|
||||||
if (reader.IsEmptyElement)
|
|
||||||
{
|
|
||||||
reader.Read();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var mbzId = reader.GetAttribute("id");
|
|
||||||
|
|
||||||
using var subReader = reader.ReadSubtree();
|
|
||||||
var artist = ParseArtist(subReader, mbzId);
|
|
||||||
if (artist != null)
|
|
||||||
{
|
|
||||||
yield return artist;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
reader.Skip();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
reader.Read();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private RemoteSearchResult ParseArtist(XmlReader reader, string artistId)
|
if (!string.IsNullOrWhiteSpace(musicBrainzId))
|
||||||
{
|
{
|
||||||
var result = new RemoteSearchResult();
|
result.HasMetadata = true;
|
||||||
|
result.Item.SetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzId);
|
||||||
reader.MoveToContent();
|
|
||||||
reader.Read();
|
|
||||||
|
|
||||||
// http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator
|
|
||||||
|
|
||||||
// Loop through each element
|
|
||||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
|
||||||
{
|
|
||||||
if (reader.NodeType == XmlNodeType.Element)
|
|
||||||
{
|
|
||||||
switch (reader.Name)
|
|
||||||
{
|
|
||||||
case "name":
|
|
||||||
{
|
|
||||||
result.Name = reader.ReadElementContentAsString();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "annotation":
|
|
||||||
{
|
|
||||||
result.Overview = reader.ReadElementContentAsString();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
// there is sort-name if ever needed
|
|
||||||
reader.Skip();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
reader.Read();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.SetProviderId(MetadataProvider.MusicBrainzArtist, artistId);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(artistId) || string.IsNullOrWhiteSpace(result.Name))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
return result;
|
||||||
public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo info, CancellationToken cancellationToken)
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dispose all resources.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposing">Whether to dispose.</param>
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
{
|
{
|
||||||
var result = new MetadataResult<MusicArtist>
|
_musicBrainzQuery.Dispose();
|
||||||
{
|
|
||||||
Item = new MusicArtist()
|
|
||||||
};
|
|
||||||
|
|
||||||
var musicBrainzId = info.GetMusicBrainzArtistId();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(musicBrainzId))
|
|
||||||
{
|
|
||||||
var searchResults = await GetSearchResults(info, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var singleResult = searchResults.FirstOrDefault();
|
|
||||||
|
|
||||||
if (singleResult != null)
|
|
||||||
{
|
|
||||||
musicBrainzId = singleResult.GetProviderId(MetadataProvider.MusicBrainzArtist);
|
|
||||||
result.Item.Overview = singleResult.Overview;
|
|
||||||
|
|
||||||
if (Plugin.Instance.Configuration.ReplaceArtistName)
|
|
||||||
{
|
|
||||||
result.Item.Name = singleResult.Name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(musicBrainzId))
|
|
||||||
{
|
|
||||||
result.HasMetadata = true;
|
|
||||||
result.Item.SetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Encodes an URL.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">The name.</param>
|
|
||||||
/// <returns>System.String.</returns>
|
|
||||||
private static string UrlEncode(string name)
|
|
||||||
{
|
|
||||||
return WebUtility.UrlEncode(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,27 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using MediaBrowser.Providers.Plugins.MusicBrainz;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Music
|
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MusicBrainz other artist external id.
|
||||||
|
/// </summary>
|
||||||
|
public class MusicBrainzOtherArtistExternalId : IExternalId
|
||||||
{
|
{
|
||||||
public class MusicBrainzOtherArtistExternalId : IExternalId
|
/// <inheritdoc />
|
||||||
{
|
public string ProviderName => "MusicBrainz";
|
||||||
/// <inheritdoc />
|
|
||||||
public string ProviderName => "MusicBrainz";
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Key => MetadataProvider.MusicBrainzArtist.ToString();
|
public string Key => MetadataProvider.MusicBrainzArtist.ToString();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
|
public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
|
public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
|
public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,27 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using MediaBrowser.Providers.Plugins.MusicBrainz;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Music
|
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MusicBrainz release group external id.
|
||||||
|
/// </summary>
|
||||||
|
public class MusicBrainzReleaseGroupExternalId : IExternalId
|
||||||
{
|
{
|
||||||
public class MusicBrainzReleaseGroupExternalId : IExternalId
|
/// <inheritdoc />
|
||||||
{
|
public string ProviderName => "MusicBrainz";
|
||||||
/// <inheritdoc />
|
|
||||||
public string ProviderName => "MusicBrainz";
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString();
|
public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
|
public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}";
|
public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/release-group/{0}";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
|
public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,27 @@
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using MediaBrowser.Providers.Plugins.MusicBrainz;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Music
|
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MusicBrainz track id.
|
||||||
|
/// </summary>
|
||||||
|
public class MusicBrainzTrackId : IExternalId
|
||||||
{
|
{
|
||||||
public class MusicBrainzTrackId : IExternalId
|
/// <inheritdoc />
|
||||||
{
|
public string ProviderName => "MusicBrainz";
|
||||||
/// <inheritdoc />
|
|
||||||
public string ProviderName => "MusicBrainz";
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Key => MetadataProvider.MusicBrainzTrack.ToString();
|
public string Key => MetadataProvider.MusicBrainzTrack.ToString();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ExternalIdMediaType? Type => ExternalIdMediaType.Track;
|
public ExternalIdMediaType? Type => ExternalIdMediaType.Track;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}";
|
public string? UrlFormatString => Plugin.Instance!.Configuration.Server + "/track/{0}";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Supports(IHasProviderIds item) => item is Audio;
|
public bool Supports(IHasProviderIds item) => item is Audio;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,63 @@
|
||||||
#nullable disable
|
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Reflection;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
|
using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration;
|
||||||
|
using MetaBrainz.MusicBrainz;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Plugins.MusicBrainz
|
namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plugin instance.
|
||||||
|
/// </summary>
|
||||||
|
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
||||||
{
|
{
|
||||||
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="Plugin"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||||
|
/// <param name="xmlSerializer">Instance of the <see cref="IXmlSerializer"/> interface.</param>
|
||||||
|
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
||||||
|
: base(applicationPaths, xmlSerializer)
|
||||||
{
|
{
|
||||||
public const string DefaultServer = "https://musicbrainz.org";
|
Instance = this;
|
||||||
|
|
||||||
public const long DefaultRateLimit = 2000u;
|
// TODO: Change this to "JellyfinMusicBrainzPlugin" once we take it out of the server repo.
|
||||||
|
Query.DefaultUserAgent.Add(new ProductInfoHeaderValue("Jellyfin", Assembly.GetExecutingAssembly().GetName().Version?.ToString(3)));
|
||||||
|
Query.DefaultUserAgent.Add(new ProductInfoHeaderValue("(apps@jellyfin.org)"));
|
||||||
|
Query.DelayBetweenRequests = Instance.Configuration.RateLimit;
|
||||||
|
Query.DefaultServer = Instance.Configuration.Server;
|
||||||
|
}
|
||||||
|
|
||||||
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
|
/// <summary>
|
||||||
: base(applicationPaths, xmlSerializer)
|
/// Gets the current plugin instance.
|
||||||
|
/// </summary>
|
||||||
|
public static Plugin? Instance { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Guid Id => new Guid("8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Name => "MusicBrainz";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Description => "Get artist and album metadata from any MusicBrainz server.";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
// TODO remove when plugin removed from server.
|
||||||
|
public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<PluginPageInfo> GetPages()
|
||||||
|
{
|
||||||
|
yield return new PluginPageInfo
|
||||||
{
|
{
|
||||||
Instance = this;
|
Name = Name,
|
||||||
}
|
EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html"
|
||||||
|
};
|
||||||
public static Plugin Instance { get; private set; }
|
|
||||||
|
|
||||||
public override Guid Id => new Guid("8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a");
|
|
||||||
|
|
||||||
public override string Name => "MusicBrainz";
|
|
||||||
|
|
||||||
public override string Description => "Get artist and album metadata from any MusicBrainz server.";
|
|
||||||
|
|
||||||
// TODO remove when plugin removed from server.
|
|
||||||
public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml";
|
|
||||||
|
|
||||||
public IEnumerable<PluginPageInfo> GetPages()
|
|
||||||
{
|
|
||||||
yield return new PluginPageInfo
|
|
||||||
{
|
|
||||||
Name = Name,
|
|
||||||
EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +1,31 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Jellyfin.Extensions
|
namespace Jellyfin.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Static extensions for the <see cref="IEnumerable{T}"/> interface.
|
||||||
|
/// </summary>
|
||||||
|
public static class EnumerableExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Static extensions for the <see cref="IEnumerable{T}"/> interface.
|
/// Determines whether the value is contained in the source collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class EnumerableExtensions
|
/// <param name="source">An instance of the <see cref="IEnumerable{String}"/> interface.</param>
|
||||||
|
/// <param name="value">The value to look for in the collection.</param>
|
||||||
|
/// <param name="stringComparison">The string comparison.</param>
|
||||||
|
/// <returns>A value indicating whether the value is contained in the collection.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">The source is null.</exception>
|
||||||
|
public static bool Contains(this IEnumerable<string> source, ReadOnlySpan<char> value, StringComparison stringComparison)
|
||||||
{
|
{
|
||||||
/// <summary>
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
/// Determines whether the value is contained in the source collection.
|
|
||||||
/// </summary>
|
if (source is IList<string> list)
|
||||||
/// <param name="source">An instance of the <see cref="IEnumerable{String}"/> interface.</param>
|
|
||||||
/// <param name="value">The value to look for in the collection.</param>
|
|
||||||
/// <param name="stringComparison">The string comparison.</param>
|
|
||||||
/// <returns>A value indicating whether the value is contained in the collection.</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">The source is null.</exception>
|
|
||||||
public static bool Contains(this IEnumerable<string> source, ReadOnlySpan<char> value, StringComparison stringComparison)
|
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(source);
|
int len = list.Count;
|
||||||
|
for (int i = 0; i < len; i++)
|
||||||
if (source is IList<string> list)
|
|
||||||
{
|
{
|
||||||
int len = list.Count;
|
if (value.Equals(list[i], stringComparison))
|
||||||
for (int i = 0; i < len; i++)
|
|
||||||
{
|
|
||||||
if (value.Equals(list[i], stringComparison))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (string element in source)
|
|
||||||
{
|
|
||||||
if (value.Equals(element, stringComparison))
|
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -44,5 +33,26 @@ namespace Jellyfin.Extensions
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (string element in source)
|
||||||
|
{
|
||||||
|
if (value.Equals(element, stringComparison))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an IEnumerable from a single item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The item to return.</param>
|
||||||
|
/// <typeparam name="T">The type of item.</typeparam>
|
||||||
|
/// <returns>The IEnumerable{T}.</returns>
|
||||||
|
public static IEnumerable<T> SingleItemAsEnumerable<T>(this T item)
|
||||||
|
{
|
||||||
|
yield return item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using MediaBrowser.Providers.Music;
|
using MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||||
using MediaBrowser.XbmcMetadata.Parsers;
|
using MediaBrowser.XbmcMetadata.Parsers;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
|
@ -7,7 +7,7 @@ using MediaBrowser.Controller.Providers;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Providers;
|
using MediaBrowser.Model.Providers;
|
||||||
using MediaBrowser.Providers.Music;
|
using MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||||
using MediaBrowser.XbmcMetadata.Parsers;
|
using MediaBrowser.XbmcMetadata.Parsers;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user