Add analyzers and remove all warnings

This commit is contained in:
crobibero 2021-03-13 09:48:34 -07:00
parent 0d73da0f71
commit c6045d0096
12 changed files with 484 additions and 188 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
bin/
obj/
.vs/
.idea

View File

@ -2,27 +2,10 @@ using MediaBrowser.Model.Plugins;
namespace Jellyfin.Plugin.CoverArtArchive.Configuration
{
public enum SomeOptions
{
OneOption,
AnotherOption
}
/// <summary>
/// The (empty) plugin configuration.
/// </summary>
public class PluginConfiguration : BasePluginConfiguration
{
// store configurable settings your plugin might need
public bool TrueFalseSetting { get; set; }
public int AnInteger { get; set; }
public string AString { get; set; }
public SomeOptions Options { get; set; }
public PluginConfiguration()
{
// set default options here
Options = SomeOptions.AnotherOption;
TrueFalseSetting = true;
AnInteger = 2;
AString = "string";
}
}
}

View File

@ -1,8 +1,18 @@
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,51 @@
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
{
/// <summary>
/// The cover art plugin.
/// </summary>
public class CoverArtArchivePlugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
/// <summary>
/// Initializes a new instance of the <see cref="CoverArtArchivePlugin"/> 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 CoverArtArchivePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
Instance = this;
}
/// <inheritdoc />
public override string Name => "Cover Art Archive";
/// <inheritdoc />
public override Guid Id => Guid.Parse("8119f3c6-cfc2-4d9c-a0ba-028f1d93e526");
/// <summary>
/// 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

@ -5,6 +5,11 @@
<RootNamespace>Jellyfin.Plugin.CoverArtArchive</RootNamespace>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<FileVersion>2.0.0.0</FileVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
@ -13,6 +18,13 @@
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<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" />

View File

@ -1,36 +0,0 @@
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
{
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
public override string Name => "Cover Art Archive";
public override Guid Id => Guid.Parse("8119f3c6-cfc2-4d9c-a0ba-028f1d93e526");
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer)
{
Instance = this;
}
public static Plugin Instance { get; private set; }
public IEnumerable<PluginPageInfo> GetPages()
{
return new[]
{
new PluginPageInfo
{
Name = this.Name,
EmbeddedResourcePath = string.Format("{0}.Configuration.configPage.html", GetType().Namespace)
}
};
}
}
}

View File

@ -0,0 +1,46 @@
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

@ -0,0 +1,91 @@
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

@ -0,0 +1,21 @@
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

@ -0,0 +1,18 @@
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,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
@ -16,156 +16,173 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.CoverArtArchive.Providers {
/* https://musicbrainz.org/doc/Cover_Art/Types */
public enum ApiImageTypeEnum {
Front, // ImageType.Box
Back, // ImageType.BoxRear
Booklet,
Medium, // ImageType.Disc
Tray,
Obi,
Spine,
Track,
Liner,
Sticker,
Poster, // ImageType.Art
Watermark,
// Raw/Unedited,
Other,
}
public class ApiRelease {
public string Release { get; set; }
public List<ApiImage> Images { get; set; }
}
public class ApiImage {
public List<ApiImageTypeEnum> Types { get; set; }
public bool Front { get; set; }
public bool Back { get; set; }
public string Image { get; set; }
public ApiThumbnails Thumbnails { get; set; }
public string Comment { get; set; }
public bool Approved { get; set; }
}
public class ApiThumbnails {
public string Small { get; set; }
public string Large { get; set; }
}
public class CoverArtArchiveImageProvider : IRemoteImageProvider {
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;
public CoverArtArchiveImageProvider(IHttpClientFactory httpClientFactory, ILogger<CoverArtArchiveImageProvider> logger) {
/// <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 {
_serializerOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
_serializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
}
public string Name => "Cover Art Archive";
public bool Supports(BaseItem item) => item is MusicAlbum;
public IEnumerable<ImageType> GetSupportedImages(BaseItem item) {
return new[] { ImageType.Primary, ImageType.Box, ImageType.BoxRear, ImageType.Disc };
}
public async Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) {
_logger.LogDebug("GetImageResponse({url})", url);
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
return await httpClient.GetAsync(url).ConfigureAwait(false);
}
private async Task<IEnumerable<RemoteImageInfo>> _getImages(string url, CancellationToken cancellationToken) {
_logger.LogDebug("_getImages({url})", url);
List<RemoteImageInfo> list = new List<RemoteImageInfo>();
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
var response = await httpClient.GetAsync(url).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.OK) {
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
ApiRelease release = await JsonSerializer.DeserializeAsync<ApiRelease>(stream, _serializerOptions);
foreach (ApiImage image in release.Images) {
_logger.LogDebug(image.Types.ToString());
if (image.Types.Contains(ApiImageTypeEnum.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(ApiImageTypeEnum.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(ApiImageTypeEnum.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,
}
);
}
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
}
} else {
_logger.LogWarning("Got HTTP {} - {}", response.StatusCode, response.Headers.Location);
}
return list;
};
}
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) {
/// <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 _getImages($"{Constants.ApiBaseUrl}/release/{musicBrainzId}/", cancellationToken));
if (!string.IsNullOrEmpty(musicBrainzId))
{
list.AddRange(await GetImagesInternal($"{Constants.ApiBaseUrl}/release/{musicBrainzId}/", cancellationToken)
.ConfigureAwait(false));
}
if (list.Count == 0) {
if (list.Count == 0)
{
var musicBrainzGroupId = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup);
if (!string.IsNullOrEmpty(musicBrainzGroupId)) {
list.AddRange(await _getImages($"{Constants.ApiBaseUrl}/release-group/{musicBrainzGroupId}/", cancellationToken));
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;
}
}
}

82
jellyfin.ruleset Normal file
View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Rules for Jellyfin" Description="Code analysis rules for Jellyfin" ToolsVersion="14.0">
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
<!-- disable warning SA1202: 'public' members must come before 'private' members -->
<Rule Id="SA1202" Action="Info" />
<!-- disable warning SA1204: Static members must appear before non-static members -->
<Rule Id="SA1204" Action="Info" />
<!-- disable warning SA1404: Code analysis suppression should have justification -->
<Rule Id="SA1404" Action="Info" />
<!-- disable warning SA1009: Closing parenthesis should be followed by a space. -->
<Rule Id="SA1009" Action="None" />
<!-- disable warning SA1011: Closing square bracket should be followed by a space. -->
<Rule Id="SA1011" Action="None" />
<!-- disable warning SA1101: Prefix local calls with 'this.' -->
<Rule Id="SA1101" Action="None" />
<!-- disable warning SA1108: Block statements should not contain embedded comments -->
<Rule Id="SA1108" Action="None" />
<!-- disable warning SA1128:: Put constructor initializers on their own line -->
<Rule Id="SA1128" Action="None" />
<!-- disable warning SA1130: Use lambda syntax -->
<Rule Id="SA1130" Action="None" />
<!-- disable warning SA1200: 'using' directive must appear within a namespace declaration -->
<Rule Id="SA1200" Action="None" />
<!-- disable warning SA1309: Fields must not begin with an underscore -->
<Rule Id="SA1309" Action="None" />
<!-- disable warning SA1413: Use trailing comma in multi-line initializers -->
<Rule Id="SA1413" Action="None" />
<!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
<Rule Id="SA1512" Action="None" />
<!-- disable warning SA1515: Single-line comment should be preceded by blank line -->
<Rule Id="SA1515" Action="None" />
<!-- disable warning SA1600: Elements should be documented -->
<Rule Id="SA1600" Action="None" />
<!-- disable warning SA1602: Enumeration items should be documented -->
<Rule Id="SA1602" Action="None" />
<!-- disable warning SA1633: The file header is missing or not located at the top of the file -->
<Rule Id="SA1633" Action="None" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.Design">
<!-- error on CA2016: Forward the CancellationToken parameter to methods that take one
or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token -->
<Rule Id="CA2016" Action="Error" />
<!-- disable warning CA1031: Do not catch general exception types -->
<Rule Id="CA1031" Action="Info" />
<!-- disable warning CA1032: Implement standard exception constructors -->
<Rule Id="CA1032" Action="Info" />
<!-- disable warning CA1062: Validate arguments of public methods -->
<Rule Id="CA1062" Action="Info" />
<!-- disable warning CA1716: Identifiers should not match keywords -->
<Rule Id="CA1716" Action="Info" />
<!-- disable warning CA1720: Identifiers should not contain type names -->
<Rule Id="CA1720" Action="Info" />
<!-- disable warning CA1805: Do not initialize unnecessarily -->
<Rule Id="CA1805" Action="Info" />
<!-- disable warning CA1812: internal class that is apparently never instantiated.
If so, remove the code from the assembly.
If this class is intended to contain only static members, make it static -->
<Rule Id="CA1812" Action="Info" />
<!-- disable warning CA1822: Member does not access instance data and can be marked as static -->
<Rule Id="CA1822" Action="Info" />
<!-- disable warning CA2000: Dispose objects before losing scope -->
<Rule Id="CA2000" Action="Info" />
<!-- disable warning CA5394: Do not use insecure randomness -->
<Rule Id="CA5394" Action="Info" />
<!-- disable warning CA1014: Mark assemblies with CLSCompliantAttribute -->
<Rule Id="CA1014" Action="Info" />
<!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
<Rule Id="CA1054" Action="None" />
<!-- disable warning CA1055: URI return values should not be strings -->
<Rule Id="CA1055" Action="None" />
<!-- disable warning CA1056: URI properties should not be strings -->
<Rule Id="CA1056" Action="None" />
<!-- disable warning CA1303: Do not pass literals as localized parameters -->
<Rule Id="CA1303" Action="None" />
<!-- disable warning CA1308: Normalize strings to uppercase -->
<Rule Id="CA1308" Action="None" />
</Rules>
</RuleSet>