Remove configuration page and migrate to MetaBrainz.MusicBrainz.CoverArt

This commit is contained in:
MrTimscampi 2021-07-31 20:26:41 +02:00
parent f0c8951f1f
commit 543bca15ca
11 changed files with 160 additions and 480 deletions

View File

@ -1,75 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Cover Art Archive</title>
</head>
<body>
<div id="CoverArtArchiveConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
<div data-role="content">
<div class="content-primary">
<form id="CoverArtArchiveConfigForm">
<div class="selectContainer">
<label class="selectLabel" for="Options">Several Options</label>
<select is="emby-select" id="Options" name="Options" class="emby-select-withcolor emby-select">
<option id="optOneOption" value="OneOption">One Option</option>
<option id="optAnotherOption" value="AnotherOption">Another Option</option>
</select>
</div>
<div class="inputContainer">
<label class="inputeLabel inputLabelUnfocused" for="AnInteger">An Integer</label>
<input id="AnInteger" name="AnInteger" type="number" is="emby-input" min="0" />
<div class="fieldDescription">A Description</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescripton">
<label class="emby-checkbox-label">
<input id="TrueFalseSetting" name="TrueFalseCheckBox" type="checkbox" is="emby-checkbox" />
<span>A Checkbox</span>
</label>
</div>
<div class="inputContainer">
<label class="inputeLabel inputLabelUnfocused" for="AString">A String</label>
<input id="AString" name="AString" type="text" is="emby-input" />
<div class="fieldDescription">Another Description</div>
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
<span>Save</span>
</button>
</div>
</form>
</div>
</div>
<script type="text/javascript">
var CoverArtArchiveConfig = {
pluginUniqueId: '8119f3c6-cfc2-4d9c-a0ba-028f1d93e526'
};
$('#CoverArtArchiveConfigPage').on('pageshow', function () {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(CoverArtArchiveConfig.pluginUniqueId).then(function (config) {
$('#Options').val(config.Options).change();
$('#AnInteger').val(config.AnInteger).change();
document.getElementById('TrueFalseSetting').checked = config.TrueFalseSetting;
$('#AString').val(config.AString).change();
Dashboard.hideLoadingMsg();
});
});
$('#CoverArtArchiveConfigForm').on('submit', function () {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(CoverArtArchiveConfig.pluginUniqueId).then(function (config) {
config.Options = $('#Options').val();
config.AnInteger = $('#AnInteger').val();
config.TrueFalseSetting = document.getElementById('TrueFalseSetting').checked;
config.AString = $('#AString').val();
ApiClient.updatePluginConfiguration(CoverArtArchiveConfig.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
});
});
return false;
});
</script>
</div>
</body>
</html>

View File

@ -1,18 +0,0 @@
namespace Jellyfin.Plugin.CoverArtArchive
{
/// <summary>
/// Constants.
/// </summary>
public static class Constants
{
/// <summary>
/// Gets the user agent.
/// </summary>
public const string UserAgent = "jellyfin-plugin-coverartarchive";
/// <summary>
/// Gets the api base url.
/// </summary>
public const string ApiBaseUrl = "https://coverartarchive.org";
}
}

View File

@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using MetaBrainz.MusicBrainz.CoverArt;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.CoverArtArchive
{
/// <summary>
/// The cover art archive image provider.
/// </summary>
public class CoverArtArchiveImageProvider : IRemoteImageProvider
{
private readonly ILogger<CoverArtArchiveImageProvider> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly JsonSerializerOptions _serializerOptions;
private readonly CoverArt _coverArtClient;
/// <summary>
/// Initializes a new instance of the <see cref="CoverArtArchiveImageProvider"/> class.
/// </summary>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{CoverArtArchiveImageProvider}"/> interface.</param>
public CoverArtArchiveImageProvider(
IHttpClientFactory httpClientFactory,
ILogger<CoverArtArchiveImageProvider> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_serializerOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
}
};
_coverArtClient = new CoverArt("Jellyfin Cover Art Archive Plugin", Assembly.GetExecutingAssembly().GetName().Version!.ToString(), "apps@jellyfin.org");
}
/// <inheritdoc />
public string Name
=> "Cover Art Archive";
/// <inheritdoc />
public bool Supports(BaseItem item)
=> item is MusicAlbum;
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new[]
{
ImageType.Primary
};
}
/// <inheritdoc />
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
// TODO: Should probably also use the MusicBrainz library for this.
_logger.LogDebug("GetImageResponse({Url})", url);
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
return await httpClient.GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var list = new List<RemoteImageInfo>();
var musicBrainzId = item.GetProviderId(MetadataProvider.MusicBrainzAlbum);
if (!string.IsNullOrEmpty(musicBrainzId))
{
try
{
var release = await _coverArtClient.FetchReleaseAsync(Guid.Parse(musicBrainzId)).ConfigureAwait(false);
var frontImage = release.Images.SingleOrDefault(image => image.Types == CoverArtType.Front);
if (frontImage is not null)
{
list.Add(new RemoteImageInfo
{
ProviderName = Name,
Url = frontImage.Location!.ToString(),
Type = ImageType.Primary,
ThumbnailUrl = ((frontImage.Thumbnails.Large ?? frontImage.Thumbnails.Small)!).ToString(),
CommunityRating = frontImage.Approved ? 1 : 0,
RatingType = RatingType.Score,
});
}
}
catch (WebException e)
{
HttpWebResponse response = (HttpWebResponse)e.Response!;
_logger.LogWarning("Got HTTP {StatusCode} when getting image for MusicBrainz release {ReleaseId}", response.StatusCode, musicBrainzId);
}
}
if (list.Count == 0)
{
var musicBrainzGroupId = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup);
if (!string.IsNullOrEmpty(musicBrainzGroupId))
{
try
{
var release = await _coverArtClient.FetchGroupReleaseAsync(Guid.Parse(musicBrainzGroupId)).ConfigureAwait(false);
var frontImage = release.Images.SingleOrDefault(image => image.Types == CoverArtType.Front);
if (frontImage is not null)
{
list.Add(new RemoteImageInfo
{
ProviderName = Name,
Url = frontImage.Location!.ToString(),
Type = ImageType.Primary,
ThumbnailUrl = ((frontImage.Thumbnails.Large ?? frontImage.Thumbnails.Small)!).ToString(),
CommunityRating = frontImage.Approved ? 1 : 0,
RatingType = RatingType.Score,
});
}
}
catch (WebException e)
{
HttpWebResponse response = (HttpWebResponse)e.Response!;
_logger.LogWarning("Got HTTP {StatusCode} when getting image for MusicBrainz release group {ReleaseId}", response.StatusCode, musicBrainzGroupId);
}
}
}
return list;
}
}
}

View File

@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using Jellyfin.Plugin.CoverArtArchive.Configuration;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
namespace Jellyfin.Plugin.CoverArtArchive
@ -11,7 +9,7 @@ namespace Jellyfin.Plugin.CoverArtArchive
/// <summary>
/// The cover art plugin.
/// </summary>
public class CoverArtArchivePlugin : BasePlugin<PluginConfiguration>, IHasWebPages
public class CoverArtArchivePlugin : BasePlugin<PluginConfiguration>
{
/// <summary>
/// Initializes a new instance of the <see cref="CoverArtArchivePlugin"/> class.
@ -34,18 +32,5 @@ namespace Jellyfin.Plugin.CoverArtArchive
/// Gets the plugin instance.
/// </summary>
public static CoverArtArchivePlugin? Instance { get; private set; }
/// <inheritdoc />
public IEnumerable<PluginPageInfo> GetPages()
{
return new[]
{
new PluginPageInfo
{
Name = Name,
EmbeddedResourcePath = $"{GetType().Namespace}.Configuration.configPage.html"
}
};
}
}
}

View File

@ -3,8 +3,8 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>Jellyfin.Plugin.CoverArtArchive</RootNamespace>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<FileVersion>2.0.0.0</FileVersion>
<AssemblyVersion>4.0.0.0</AssemblyVersion>
<FileVersion>4.0.0.0</FileVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
@ -15,6 +15,8 @@
<ItemGroup>
<PackageReference Include="Jellyfin.Controller" Version="10.*-*" />
<PackageReference Include="Jellyfin.Model" Version="10.*-*" />
<PackageReference Include="MetaBrainz.Common.Json" Version="4.0.0" />
<PackageReference Include="MetaBrainz.MusicBrainz.CoverArt" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
</ItemGroup>
@ -24,10 +26,5 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.*" />
</ItemGroup>
<ItemGroup>
<None Remove="Configuration\configPage.html" />
<EmbeddedResource Include="Configuration\configPage.html" />
</ItemGroup>
</Project>

View File

@ -1,46 +0,0 @@
using System;
using System.Collections.Generic;
namespace Jellyfin.Plugin.CoverArtArchive.Providers
{
/// <summary>
/// The api image dto.
/// </summary>
public class ApiImageDto
{
/// <summary>
/// Gets or sets the list of types.
/// </summary>
public IReadOnlyList<ApiImageType> Types { get; set; } = Array.Empty<ApiImageType>();
/// <summary>
/// Gets or sets a value indicating whether this is a front image.
/// </summary>
public bool Front { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this is a back image.
/// </summary>
public bool Back { get; set; }
/// <summary>
/// Gets or sets the image.
/// </summary>
public string? Image { get; set; }
/// <summary>
/// Gets or sets the thumbnails.
/// </summary>
public ApiThumbnailsDto? Thumbnails { get; set; }
/// <summary>
/// Gets or sets the image comment.
/// </summary>
public string? Comment { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this image has been approved.
/// </summary>
public bool Approved { get; set; }
}
}

View File

@ -1,91 +0,0 @@
namespace Jellyfin.Plugin.CoverArtArchive.Providers
{
/// <summary>
/// The api image type.
/// </summary>
/// <remarks>
/// https://musicbrainz.org/doc/Cover_Art/Types.
/// </remarks>
public enum ApiImageType
{
/// <summary>
/// Front image.
/// </summary>
/// <remarks>
/// ImageType.Box
/// </remarks>
Front,
/// <summary>
/// Back image.
/// </summary>
/// <remarks>
/// ImageType.BoxRear
/// </remarks>
Back, // ImageType.BoxRear
/// <summary>
/// Booklet image.
/// </summary>
Booklet,
/// <summary>
/// Medium image
/// </summary>
/// <remarks>
/// ImageType.Disc
/// </remarks>
Medium, // ImageType.Disc
/// <summary>
/// Tray image.
/// </summary>
Tray,
/// <summary>
/// Obi image.
/// </summary>
Obi,
/// <summary>
/// Spine image.
/// </summary>
Spine,
/// <summary>
/// Track image.
/// </summary>
Track,
/// <summary>
/// Liner image.
/// </summary>
Liner,
/// <summary>
/// Sticker image.
/// </summary>
Sticker,
/// <summary>
/// Poster image
/// </summary>
/// <remarks>
/// ImageType.Art
/// </remarks>
Poster, // ImageType.Art
/// <summary>
/// Watermark image.
/// </summary>
Watermark,
/// <summary>
/// Other image
/// </summary>
/// <remarks>
/// Raw or Unedited
/// </remarks>
Other,
}
}

View File

@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
namespace Jellyfin.Plugin.CoverArtArchive.Providers
{
/// <summary>
/// The api release dto.
/// </summary>
public class ApiReleaseDto
{
/// <summary>
/// Gets or sets the release.
/// </summary>
public string? Release { get; set; }
/// <summary>
/// Gets or sets the list of images.
/// </summary>
public IReadOnlyList<ApiImageDto> Images { get; set; } = Array.Empty<ApiImageDto>();
}
}

View File

@ -1,18 +0,0 @@
namespace Jellyfin.Plugin.CoverArtArchive.Providers
{
/// <summary>
/// Api thumbnails dto.
/// </summary>
public class ApiThumbnailsDto
{
/// <summary>
/// Gets or sets the small thumbnail.
/// </summary>
public string? Small { get; set; }
/// <summary>
/// Gets or sets the large thumbnail.
/// </summary>
public string? Large { get; set; }
}
}

View File

@ -1,188 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.CoverArtArchive.Providers
{
/// <summary>
/// The cover art archive image provider.
/// </summary>
public class CoverArtArchiveImageProvider : IRemoteImageProvider
{
private readonly ILogger<CoverArtArchiveImageProvider> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly JsonSerializerOptions _serializerOptions;
/// <summary>
/// Initializes a new instance of the <see cref="CoverArtArchiveImageProvider"/> class.
/// </summary>
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{CoverArtArchiveImageProvider}"/> interface.</param>
public CoverArtArchiveImageProvider(
IHttpClientFactory httpClientFactory,
ILogger<CoverArtArchiveImageProvider> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_serializerOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
}
};
}
/// <inheritdoc />
public string Name
=> "Cover Art Archive";
/// <inheritdoc />
public bool Supports(BaseItem item)
=> item is MusicAlbum;
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new[]
{
ImageType.Primary,
ImageType.Box,
ImageType.BoxRear,
ImageType.Disc
};
}
/// <inheritdoc />
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
_logger.LogDebug("GetImageResponse({Url})", url);
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
return await httpClient.GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var list = new List<RemoteImageInfo>();
var musicBrainzId = item.GetProviderId(MetadataProvider.MusicBrainzAlbum);
if (!string.IsNullOrEmpty(musicBrainzId))
{
list.AddRange(await GetImagesInternal($"{Constants.ApiBaseUrl}/release/{musicBrainzId}/", cancellationToken)
.ConfigureAwait(false));
}
if (list.Count == 0)
{
var musicBrainzGroupId = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup);
if (!string.IsNullOrEmpty(musicBrainzGroupId))
{
list.AddRange(await GetImagesInternal($"{Constants.ApiBaseUrl}/release-group/{musicBrainzGroupId}/", cancellationToken)
.ConfigureAwait(false));
}
}
return list;
}
private async Task<IEnumerable<RemoteImageInfo>> GetImagesInternal(string url, CancellationToken cancellationToken)
{
_logger.LogDebug("GetImagesInternal({Url})", url);
List<RemoteImageInfo> list = new List<RemoteImageInfo>();
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
var response = await httpClient.GetAsync(new Uri(url), cancellationToken).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.OK)
{
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var releaseDto = await JsonSerializer.DeserializeAsync<ApiReleaseDto>(stream, _serializerOptions, cancellationToken)
.ConfigureAwait(false);
if (releaseDto == null)
{
return Array.Empty<RemoteImageInfo>();
}
foreach (ApiImageDto image in releaseDto.Images)
{
_logger.LogDebug("ImageType: {ImageType}", image.Types);
if (image.Types.Contains(ApiImageType.Front))
{
list.Add(
new RemoteImageInfo
{
ProviderName = Name,
Url = image.Image,
Type = ImageType.Box,
ThumbnailUrl = image.Thumbnails?.Small ?? image.Thumbnails?.Large,
CommunityRating = image.Approved ? 1 : 0,
RatingType = RatingType.Score,
});
list.Add(
new RemoteImageInfo
{
ProviderName = Name,
Url = image.Image,
Type = ImageType.Primary,
ThumbnailUrl = image.Thumbnails?.Small ?? image.Thumbnails?.Large,
CommunityRating = image.Approved ? 1 : 0,
RatingType = RatingType.Score,
});
}
if (image.Types.Contains(ApiImageType.Back))
{
list.Add(
new RemoteImageInfo
{
ProviderName = Name,
Url = image.Image,
Type = ImageType.BoxRear,
ThumbnailUrl = image.Thumbnails?.Small ?? image.Thumbnails?.Large,
CommunityRating = image.Approved ? 1 : 0,
RatingType = RatingType.Score,
});
}
if (image.Types.Contains(ApiImageType.Medium))
{
list.Add(
new RemoteImageInfo
{
ProviderName = Name,
Url = image.Image,
Type = ImageType.Disc,
ThumbnailUrl = image.Thumbnails?.Small ?? image.Thumbnails?.Large,
CommunityRating = image.Approved ? 1 : 0,
RatingType = RatingType.Score,
});
}
}
}
else
{
_logger.LogWarning("Got HTTP {StatusCode} - {Location}", response.StatusCode, response.Headers.Location);
}
return list;
}
}
}

View File

@ -12,6 +12,8 @@ category: "Metadata"
owner: "jellyfin"
artifacts:
- "Jellyfin.Plugin.CoverArtArchive.dll"
- "MetaBrainz.Common.Json.dll"
- "MetaBrainz.MusicBrainz.CoverArt.dll"
changelog: |2-
### New features and improvements ###
- chore: plugin images (#5) @h1dden-da3m0n