aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs46
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonDefaultStringEnumConverter.cs49
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonDefaultStringEnumConverterFactory.cs31
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs2
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs5
-rw-r--r--src/Jellyfin.Extensions/Json/JsonDefaults.cs1
-rw-r--r--src/Jellyfin.Extensions/Json/Utf8JsonExtensions.cs27
-rw-r--r--src/Jellyfin.LiveTv/Channels/ChannelManager.cs1
-rw-r--r--src/Jellyfin.LiveTv/Guide/GuideManager.cs42
9 files changed, 193 insertions, 11 deletions
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 4ae5a9a48..a158e5c86 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -19,8 +19,8 @@ namespace Jellyfin.Drawing.Skia;
/// </summary>
public class SkiaEncoder : IImageEncoder
{
+ private const string SvgFormat = "svg";
private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
-
private readonly ILogger<SkiaEncoder> _logger;
private readonly IApplicationPaths _appPaths;
private static readonly SKImageFilter _imageFilter;
@@ -89,12 +89,13 @@ public class SkiaEncoder : IImageEncoder
// working on windows at least
"cr2",
"nef",
- "arw"
+ "arw",
+ SvgFormat
};
/// <inheritdoc/>
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
- => new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
+ => new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png, ImageFormat.Svg };
/// <summary>
/// Check if the native lib is available.
@@ -187,7 +188,8 @@ public class SkiaEncoder : IImageEncoder
ArgumentException.ThrowIfNullOrEmpty(path);
var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
- if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
+ if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase)
+ || extension.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path);
return string.Empty;
@@ -312,6 +314,31 @@ public class SkiaEncoder : IImageEncoder
return Decode(path, false, orientation, out _);
}
+ private SKBitmap? GetBitmapFromSvg(string path)
+ {
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException("File not found", path);
+ }
+
+ using var svg = SKSvg.CreateFromFile(path);
+ if (svg.Drawable is null)
+ {
+ return null;
+ }
+
+ var width = (int)Math.Round(svg.Drawable.Bounds.Width);
+ var height = (int)Math.Round(svg.Drawable.Bounds.Height);
+
+ var bitmap = new SKBitmap(width, height);
+ using var canvas = new SKCanvas(bitmap);
+ canvas.DrawPicture(svg.Picture);
+ canvas.Flush();
+ canvas.Save();
+
+ return bitmap;
+ }
+
private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
{
var needsFlip = origin is SKEncodedOrigin.LeftBottom or SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightBottom or SKEncodedOrigin.RightTop;
@@ -402,6 +429,12 @@ public class SkiaEncoder : IImageEncoder
return inputPath;
}
+ if (outputFormat == ImageFormat.Svg
+ && !inputFormat.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException($"Requested svg output from {inputFormat} input");
+ }
+
var skiaOutputFormat = GetImageFormat(outputFormat);
var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
@@ -409,7 +442,10 @@ public class SkiaEncoder : IImageEncoder
var blur = options.Blur ?? 0;
var hasIndicator = options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
- using var bitmap = GetBitmap(inputPath, autoOrient, orientation);
+ using var bitmap = inputFormat.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase)
+ ? GetBitmapFromSvg(inputPath)
+ : GetBitmap(inputPath, autoOrient, orientation);
+
if (bitmap is null)
{
throw new InvalidDataException($"Skia unable to read image {inputPath}");
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDefaultStringEnumConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDefaultStringEnumConverter.cs
new file mode 100644
index 000000000..06ecfc558
--- /dev/null
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonDefaultStringEnumConverter.cs
@@ -0,0 +1,49 @@
+using System;
+using System.ComponentModel;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Jellyfin.Extensions.Json.Converters;
+
+/// <summary>
+/// Json unknown enum converter.
+/// </summary>
+/// <typeparam name="T">The type of enum.</typeparam>
+public class JsonDefaultStringEnumConverter<T> : JsonConverter<T>
+ where T : struct, Enum
+{
+ private readonly JsonConverter<T> _baseConverter;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="JsonDefaultStringEnumConverter{T}"/> class.
+ /// </summary>
+ /// <param name="baseConverter">The base json converter.</param>
+ public JsonDefaultStringEnumConverter(JsonConverter<T> baseConverter)
+ {
+ _baseConverter = baseConverter;
+ }
+
+ /// <inheritdoc />
+ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.IsNull() || reader.IsEmptyString())
+ {
+ var customValueAttribute = typeToConvert.GetCustomAttribute<DefaultValueAttribute>();
+ if (customValueAttribute?.Value is null)
+ {
+ throw new InvalidOperationException($"Default value not set for '{typeToConvert.Name}'");
+ }
+
+ return (T)customValueAttribute.Value;
+ }
+
+ return _baseConverter.Read(ref reader, typeToConvert, options);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
+ {
+ _baseConverter.Write(writer, value, options);
+ }
+}
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDefaultStringEnumConverterFactory.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDefaultStringEnumConverterFactory.cs
new file mode 100644
index 000000000..5a9bf546e
--- /dev/null
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonDefaultStringEnumConverterFactory.cs
@@ -0,0 +1,31 @@
+using System;
+using System.ComponentModel;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Jellyfin.Extensions.Json.Converters;
+
+/// <summary>
+/// Utilizes the JsonStringEnumConverter and sets a default value if not provided.
+/// </summary>
+public class JsonDefaultStringEnumConverterFactory : JsonConverterFactory
+{
+ private static readonly JsonStringEnumConverter _baseConverterFactory = new();
+
+ /// <inheritdoc />
+ public override bool CanConvert(Type typeToConvert)
+ {
+ return _baseConverterFactory.CanConvert(typeToConvert)
+ && typeToConvert.IsDefined(typeof(DefaultValueAttribute));
+ }
+
+ /// <inheritdoc />
+ public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ {
+ var baseConverter = _baseConverterFactory.CreateConverter(typeToConvert, options);
+ var converterType = typeof(JsonDefaultStringEnumConverter<>).MakeGenericType(typeToConvert);
+
+ return (JsonConverter?)Activator.CreateInstance(converterType, baseConverter);
+ }
+}
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs
index ea6d141cb..2964c6943 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonGuidConverter.cs
@@ -12,7 +12,7 @@ namespace Jellyfin.Extensions.Json.Converters
{
/// <inheritdoc />
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- => reader.TokenType == JsonTokenType.Null
+ => reader.IsNull()
? Guid.Empty
: ReadInternal(ref reader);
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs
index 28437023f..94004fa49 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonNullableStructConverter.cs
@@ -15,10 +15,7 @@ namespace Jellyfin.Extensions.Json.Converters
/// <inheritdoc />
public override TStruct? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
- // Token is empty string.
- if (reader.TokenType == JsonTokenType.String
- && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty)
- || (!reader.HasValueSequence && reader.ValueSpan.IsEmpty)))
+ if (reader.IsEmptyString())
{
return null;
}
diff --git a/src/Jellyfin.Extensions/Json/JsonDefaults.cs b/src/Jellyfin.Extensions/Json/JsonDefaults.cs
index 9e6d4c3f8..cbe5849ec 100644
--- a/src/Jellyfin.Extensions/Json/JsonDefaults.cs
+++ b/src/Jellyfin.Extensions/Json/JsonDefaults.cs
@@ -38,6 +38,7 @@ namespace Jellyfin.Extensions.Json
new JsonNullableGuidConverter(),
new JsonVersionConverter(),
new JsonFlagEnumConverterFactory(),
+ new JsonDefaultStringEnumConverterFactory(),
new JsonStringEnumConverter(),
new JsonNullableStructConverterFactory(),
new JsonDateTimeConverter(),
diff --git a/src/Jellyfin.Extensions/Json/Utf8JsonExtensions.cs b/src/Jellyfin.Extensions/Json/Utf8JsonExtensions.cs
new file mode 100644
index 000000000..d06508a26
--- /dev/null
+++ b/src/Jellyfin.Extensions/Json/Utf8JsonExtensions.cs
@@ -0,0 +1,27 @@
+using System.Text.Json;
+
+namespace Jellyfin.Extensions.Json;
+
+/// <summary>
+/// Extensions for Utf8JsonReader and Utf8JsonWriter.
+/// </summary>
+public static class Utf8JsonExtensions
+{
+ /// <summary>
+ /// Determines if the reader contains an empty string.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <returns>Whether the reader contains an empty string.</returns>
+ public static bool IsEmptyString(this Utf8JsonReader reader)
+ => reader.TokenType == JsonTokenType.String
+ && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty)
+ || (!reader.HasValueSequence && reader.ValueSpan.IsEmpty));
+
+ /// <summary>
+ /// Determines if the reader contains a null value.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <returns>Whether the reader contains null.</returns>
+ public static bool IsNull(this Utf8JsonReader reader)
+ => reader.TokenType == JsonTokenType.Null;
+}
diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
index 1948a9ab9..cce2911dc 100644
--- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
+++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
@@ -570,7 +570,6 @@ namespace Jellyfin.LiveTv.Channels
return new ChannelFeatures(channel.Name, channel.Id)
{
CanFilter = !features.MaxPageSize.HasValue,
- CanSearch = provider is ISearchableChannel,
ContentTypes = features.ContentTypes.ToArray(),
DefaultSortFields = features.DefaultSortFields.ToArray(),
MaxPageSize = features.MaxPageSize,
diff --git a/src/Jellyfin.LiveTv/Guide/GuideManager.cs b/src/Jellyfin.LiveTv/Guide/GuideManager.cs
index 39f174cc2..093970c38 100644
--- a/src/Jellyfin.LiveTv/Guide/GuideManager.cs
+++ b/src/Jellyfin.LiveTv/Guide/GuideManager.cs
@@ -27,6 +27,8 @@ public class GuideManager : IGuideManager
private const string EtagKey = "ProgramEtag";
private const string ExternalServiceTag = "ExternalServiceId";
+ private static readonly ParallelOptions _cacheParallelOptions = new() { MaxDegreeOfParallelism = Math.Min(Environment.ProcessorCount, 10) };
+
private readonly ILogger<GuideManager> _logger;
private readonly IConfigurationManager _config;
private readonly IFileSystem _fileSystem;
@@ -209,6 +211,7 @@ public class GuideManager : IGuideManager
_logger.LogInformation("Refreshing guide with {0} days of guide data", guideDays);
+ var maxCacheDate = DateTime.UtcNow.AddDays(2);
foreach (var currentChannel in list)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -263,6 +266,7 @@ public class GuideManager : IGuideManager
if (newPrograms.Count > 0)
{
_libraryManager.CreateItems(newPrograms, null, cancellationToken);
+ await PrecacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
}
if (updatedPrograms.Count > 0)
@@ -272,6 +276,7 @@ public class GuideManager : IGuideManager
currentChannel,
ItemUpdateType.MetadataImport,
cancellationToken).ConfigureAwait(false);
+ await PrecacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
}
currentChannel.IsMovie = isMovie;
@@ -708,4 +713,41 @@ public class GuideManager : IGuideManager
return (item, isNew, isUpdated);
}
+
+ private async Task PrecacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
+ {
+ await Parallel.ForEachAsync(
+ programs
+ .Where(p => p.EndDate.HasValue && p.EndDate.Value < maxCacheDate)
+ .DistinctBy(p => p.Id),
+ _cacheParallelOptions,
+ async (program, cancellationToken) =>
+ {
+ for (var i = 0; i < program.ImageInfos.Length; i++)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ var imageInfo = program.ImageInfos[i];
+ if (!imageInfo.IsLocalFile)
+ {
+ try
+ {
+ program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal(
+ program,
+ imageInfo,
+ imageIndex: 0,
+ removeOnFailure: false)
+ .ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Unable to precache {Url}", imageInfo.Path);
+ }
+ }
+ }
+ }).ConfigureAwait(false);
+ }
}