aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs1
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs1
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs9
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs24
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs39
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaExtensions.cs58
-rw-r--r--src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs8
-rw-r--r--src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs81
-rw-r--r--src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs8
-rw-r--r--src/Jellyfin.Drawing/ImageProcessor.cs6
-rw-r--r--src/Jellyfin.Extensions/FileHelper.cs20
-rw-r--r--src/Jellyfin.Extensions/StringExtensions.cs13
-rw-r--r--src/Jellyfin.LiveTv/Channels/ChannelManager.cs9
13 files changed, 208 insertions, 69 deletions
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs
index 06b290e4f..39b449553 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs
@@ -14,7 +14,6 @@ public class TrickplayInfo
/// <remarks>
/// Required.
/// </remarks>
- [JsonIgnore]
public Guid ItemId { get; set; }
/// <summary>
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
index 4da7074ec..6c81fa729 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
@@ -61,7 +61,6 @@ namespace Jellyfin.Database.Implementations.Entities
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
- [JsonIgnore]
public Guid Id { get; set; }
/// <summary>
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs
index 34ac7dc83..b0dc98469 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
@@ -62,4 +63,12 @@ public interface IJellyfinDatabaseProvider
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
Task RestoreBackupFast(string key, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Removes all contents from the database.
+ /// </summary>
+ /// <param name="dbContext">The Database context.</param>
+ /// <param name="tableNames">The names of the tables to purge or null for all tables to be purged.</param>
+ /// <returns>A Task.</returns>
+ Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames);
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
index 156d9618e..519584003 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
@@ -82,7 +83,7 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
}
// Run before disposing the application
- var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+ var context = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
@@ -127,4 +128,25 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
File.Copy(backupFile, path, true);
return Task.CompletedTask;
}
+
+ /// <inheritdoc/>
+ public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames)
+ {
+ ArgumentNullException.ThrowIfNull(tableNames);
+
+ var deleteQueries = new List<string>();
+ foreach (var tableName in tableNames)
+ {
+ deleteQueries.Add($"DELETE FROM \"{tableName}\";");
+ }
+
+ var deleteAllQuery =
+ $"""
+ PRAGMA foreign_keys = OFF;
+ {string.Join('\n', deleteQueries)}
+ PRAGMA foreign_keys = ON;
+ """;
+
+ await dbContext.Database.ExecuteSqlRawAsync(deleteAllQuery).ConfigureAwait(false);
+ }
}
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 73c8c3966..4626bc914 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -27,6 +27,16 @@ public class SkiaEncoder : IImageEncoder
private static readonly SKImageFilter _imageFilter;
private static readonly SKTypeface[] _typefaces;
+ /// <summary>
+ /// The default sampling options, equivalent to old high quality filter settings when upscaling.
+ /// </summary>
+ public static readonly SKSamplingOptions UpscaleSamplingOptions;
+
+ /// <summary>
+ /// The sampling options, used for downscaling images, equivalent to old high quality filter settings when not upscaling.
+ /// </summary>
+ public static readonly SKSamplingOptions DefaultSamplingOptions;
+
#pragma warning disable CA1810
static SkiaEncoder()
#pragma warning restore CA1810
@@ -63,6 +73,11 @@ public class SkiaEncoder : IImageEncoder
SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ي'), // Arabic
SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) // Default font
];
+
+ // use cubic for upscaling
+ UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell);
+ // use bilinear for everything else
+ DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear);
}
/// <summary>
@@ -441,7 +456,7 @@ public class SkiaEncoder : IImageEncoder
break;
}
- surface.DrawBitmap(bitmap, 0, 0);
+ surface.DrawBitmap(bitmap, 0, 0, DefaultSamplingOptions);
return rotated;
}
catch (Exception e)
@@ -467,18 +482,23 @@ public class SkiaEncoder : IImageEncoder
{
using var surface = SKSurface.Create(targetInfo);
using var canvas = surface.Canvas;
- using var paint = new SKPaint
- {
- FilterQuality = SKFilterQuality.High,
- IsAntialias = isAntialias,
- IsDither = isDither
- };
+ using var paint = new SKPaint();
+ paint.IsAntialias = isAntialias;
+ paint.IsDither = isDither;
+
+ // Historically, kHigh implied cubic filtering, but only when upsampling.
+ // If specified kHigh, and were down-sampling, Skia used to switch back to kMedium (bilinear filtering plus mipmaps).
+ // With current skia API, passing Mitchell cubic when down-sampling will cause serious quality degradation.
+ var samplingOptions = source.Width > targetInfo.Width || source.Height > targetInfo.Height
+ ? DefaultSamplingOptions
+ : UpscaleSamplingOptions;
paint.ImageFilter = _imageFilter;
canvas.DrawBitmap(
source,
SKRect.Create(0, 0, source.Width, source.Height),
SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
+ samplingOptions,
paint);
return surface.Snapshot();
@@ -560,11 +580,10 @@ public class SkiaEncoder : IImageEncoder
using var paint = new SKPaint();
// Add blur if option is present
using var filter = blur > 0 ? SKImageFilter.CreateBlur(blur, blur) : null;
- paint.FilterQuality = SKFilterQuality.High;
paint.ImageFilter = filter;
// create image from resized bitmap to apply blur
- canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
+ canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), DefaultSamplingOptions, paint);
// If foreground layer present then draw
if (hasForegroundColor)
@@ -690,7 +709,7 @@ public class SkiaEncoder : IImageEncoder
throw new InvalidOperationException("Image height does not match first image height.");
}
- canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value);
+ canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value, DefaultSamplingOptions);
}
}
diff --git a/src/Jellyfin.Drawing.Skia/SkiaExtensions.cs b/src/Jellyfin.Drawing.Skia/SkiaExtensions.cs
new file mode 100644
index 000000000..f7d6842ff
--- /dev/null
+++ b/src/Jellyfin.Drawing.Skia/SkiaExtensions.cs
@@ -0,0 +1,58 @@
+using SkiaSharp;
+
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// The SkiaSharp extensions.
+/// </summary>
+public static class SkiaExtensions
+{
+ /// <summary>
+ /// Draws an SKBitmap on the canvas with specified SkSamplingOptions.
+ /// </summary>
+ /// <param name="canvas">The SKCanvas to draw on.</param>
+ /// <param name="bitmap">The SKBitmap to draw.</param>
+ /// <param name="dest">The destination SKRect.</param>
+ /// <param name="options">The SKSamplingOptions to use for rendering.</param>
+ /// <param name="paint">Optional SKPaint to apply additional effects or styles.</param>
+ public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, SKRect dest, SKSamplingOptions options, SKPaint? paint = null)
+ {
+ using var image = SKImage.FromBitmap(bitmap);
+ canvas.DrawImage(image, dest, options, paint);
+ }
+
+ /// <summary>
+ /// Draws an SKBitmap on the canvas at the specified coordinates with the given SkSamplingOptions.
+ /// </summary>
+ /// <param name="canvas">The SKCanvas to draw on.</param>
+ /// <param name="bitmap">The SKBitmap to draw.</param>
+ /// <param name="x">The x-coordinate where the bitmap will be drawn.</param>
+ /// <param name="y">The y-coordinate where the bitmap will be drawn.</param>
+ /// <param name="options">The SKSamplingOptions to use for rendering.</param>
+ /// <param name="paint">Optional SKPaint to apply additional effects or styles.</param>
+ public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, float x, float y, SKSamplingOptions options, SKPaint? paint = null)
+ {
+ using var image = SKImage.FromBitmap(bitmap);
+ canvas.DrawImage(image, x, y, options, paint);
+ }
+
+ /// <summary>
+ /// Draws an SKBitmap on the canvas using a specified source rectangle, destination rectangle,
+ /// and optional paint, with the given SkSamplingOptions.
+ /// </summary>
+ /// <param name="canvas">The SKCanvas to draw on.</param>
+ /// <param name="bitmap">The SKBitmap to draw.</param>
+ /// <param name="source">
+ /// The source SKRect defining the portion of the bitmap to draw.
+ /// </param>
+ /// <param name="dest">
+ /// The destination SKRect defining the area on the canvas where the bitmap will be drawn.
+ /// </param>
+ /// <param name="options">The SKSamplingOptions to use for rendering.</param>
+ /// <param name="paint">Optional SKPaint to apply additional effects or styles.</param>
+ public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, SKRect source, SKRect dest, SKSamplingOptions options, SKPaint? paint = null)
+ {
+ using var image = SKImage.FromBitmap(bitmap);
+ canvas.DrawImage(image, source, dest, options, paint);
+ }
+}
diff --git a/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
index 03733d4f8..554707a3f 100644
--- a/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
+++ b/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
@@ -101,10 +101,12 @@ public class SplashscreenBuilder
{
var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height);
using var resizedBitmap = new SKBitmap(imageWidth, posterHeight);
- currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High);
-
+ var samplingOptions = currentImage.Width > imageWidth || currentImage.Height > posterHeight
+ ? SkiaEncoder.DefaultSamplingOptions
+ : SkiaEncoder.UpscaleSamplingOptions;
+ currentImage.ScalePixels(resizedBitmap, samplingOptions);
// draw on canvas
- canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight);
+ canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight, samplingOptions);
// resize to the same aspect as the original
currentWidthPos += imageWidth + Spacing;
diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index 03e202e5a..64c33d5c2 100644
--- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -111,38 +111,31 @@ public partial class StripCollageBuilder
var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width);
using var resizedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace));
using var paint = new SKPaint();
- paint.FilterQuality = SKFilterQuality.High;
// draw the backdrop
- canvas.DrawImage(resizedBackdrop, 0, 0, paint);
+ canvas.DrawImage(resizedBackdrop, 0, 0, SkiaEncoder.DefaultSamplingOptions, paint);
// draw shadow rectangle
- using var paintColor = new SKPaint
- {
- Color = SKColors.Black.WithAlpha(0x78),
- Style = SKPaintStyle.Fill,
- FilterQuality = SKFilterQuality.High
- };
+ using var paintColor = new SKPaint();
+ paintColor.Color = SKColors.Black.WithAlpha(0x78);
+ paintColor.Style = SKPaintStyle.Fill;
canvas.DrawRect(0, 0, width, height, paintColor);
var typeFace = SkiaEncoder.DefaultTypeFace;
// draw library name
- using var textPaint = new SKPaint
- {
- Color = SKColors.White,
- Style = SKPaintStyle.Fill,
- TextSize = 112,
- TextAlign = SKTextAlign.Left,
- Typeface = typeFace,
- IsAntialias = true,
- FilterQuality = SKFilterQuality.High
- };
+ using var textFont = new SKFont();
+ textFont.Size = 112;
+ textFont.Typeface = typeFace;
+ using var textPaint = new SKPaint();
+ textPaint.Color = SKColors.White;
+ textPaint.Style = SKPaintStyle.Fill;
+ textPaint.IsAntialias = true;
// scale down text to 90% of the width if text is larger than 95% of the width
- var textWidth = textPaint.MeasureText(libraryName);
+ var textWidth = textFont.MeasureText(libraryName);
if (textWidth > width * 0.95)
{
- textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth;
+ textFont.Size = 0.9f * width * textFont.Size / textWidth;
}
if (string.IsNullOrWhiteSpace(libraryName))
@@ -150,23 +143,22 @@ public partial class StripCollageBuilder
return bitmap;
}
- var realWidth = DrawText(null, 0, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint);
+ var realWidth = DrawText(null, 0, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont);
if (realWidth > width * 0.95)
{
- textPaint.TextSize = 0.9f * width * textPaint.TextSize / realWidth;
- realWidth = DrawText(null, 0, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint);
+ textFont.Size = 0.9f * width * textFont.Size / realWidth;
+ realWidth = DrawText(null, 0, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont);
}
var padding = (width - realWidth) / 2;
if (IsRtlTextRegex().IsMatch(libraryName))
{
- textPaint.TextAlign = SKTextAlign.Right;
- DrawText(canvas, width - padding, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint, true);
+ DrawText(canvas, width - padding, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont, true);
}
else
{
- DrawText(canvas, padding, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint);
+ DrawText(canvas, padding, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont);
}
return bitmap;
@@ -196,12 +188,11 @@ public partial class StripCollageBuilder
var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
using var resizeImage = SkiaEncoder.ResizeImage(currentBitmap, imageInfo);
using var paint = new SKPaint();
- paint.FilterQuality = SKFilterQuality.High;
// draw this image into the strip at the next position
var xPos = x * cellWidth;
var yPos = y * cellHeight;
- canvas.DrawImage(resizeImage, xPos, yPos, paint);
+ canvas.DrawImage(resizeImage, xPos, yPos, SkiaEncoder.DefaultSamplingOptions, paint);
}
}
@@ -216,11 +207,13 @@ public partial class StripCollageBuilder
/// <param name="y">y position of the canvas to draw text.</param>
/// <param name="text">The text to draw.</param>
/// <param name="textPaint">The SKPaint to style the text.</param>
+ /// <param name="textFont">The SKFont to style the text.</param>
+ /// <param name="alignment">The alignment of the text. Default aligns to left.</param>
/// <returns>The width of the text.</returns>
- private static float MeasureAndDrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint)
+ private static float MeasureAndDrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint, SKFont textFont, SKTextAlign alignment = SKTextAlign.Left)
{
- var width = textPaint.MeasureText(text);
- canvas?.DrawShapedText(text, x, y, textPaint);
+ var width = textFont.MeasureText(text);
+ canvas?.DrawShapedText(text, x, y, alignment, textFont, textPaint);
return width;
}
@@ -232,16 +225,18 @@ public partial class StripCollageBuilder
/// <param name="y">y position of the canvas to draw text.</param>
/// <param name="text">The text to draw.</param>
/// <param name="textPaint">The SKPaint to style the text.</param>
+ /// <param name="textFont">The SKFont to style the text.</param>
/// <param name="isRtl">If true, render from right to left.</param>
/// <returns>The width of the text.</returns>
- private static float DrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint, bool isRtl = false)
+ private static float DrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint, SKFont textFont, bool isRtl = false)
{
float width = 0;
+ var alignment = isRtl ? SKTextAlign.Right : SKTextAlign.Left;
- if (textPaint.ContainsGlyphs(text))
+ if (textFont.ContainsGlyphs(text))
{
// Current font can render all characters in text
- return MeasureAndDrawText(canvas, x, y, text, textPaint);
+ return MeasureAndDrawText(canvas, x, y, text, textPaint, textFont, alignment);
}
// Iterate over all text elements using TextElementEnumerator
@@ -254,7 +249,7 @@ public partial class StripCollageBuilder
{
bool notAtEnd;
var textElement = enumerator.GetTextElement();
- if (textPaint.ContainsGlyphs(textElement))
+ if (textFont.ContainsGlyphs(textElement))
{
continue;
}
@@ -264,12 +259,12 @@ public partial class StripCollageBuilder
if (start != enumerator.ElementIndex)
{
var regularText = text.Substring(start, enumerator.ElementIndex - start);
- width += MeasureAndDrawText(canvas, MoveX(x, width), y, regularText, textPaint);
+ width += MeasureAndDrawText(canvas, MoveX(x, width), y, regularText, textPaint, textFont, alignment);
start = enumerator.ElementIndex;
}
// Search for next point where current font can render the character there
- while ((notAtEnd = enumerator.MoveNext()) && !textPaint.ContainsGlyphs(enumerator.GetTextElement()))
+ while ((notAtEnd = enumerator.MoveNext()) && !textFont.ContainsGlyphs(enumerator.GetTextElement()))
{
// Do nothing, just move enumerator to the point where current font can render the character
}
@@ -284,21 +279,21 @@ public partial class StripCollageBuilder
if (fallback is not null)
{
+ using var fallbackTextFont = new SKFont();
+ fallbackTextFont.Size = textFont.Size;
+ fallbackTextFont.Typeface = fallback;
using var fallbackTextPaint = new SKPaint();
fallbackTextPaint.Color = textPaint.Color;
fallbackTextPaint.Style = textPaint.Style;
- fallbackTextPaint.TextSize = textPaint.TextSize;
- fallbackTextPaint.TextAlign = textPaint.TextAlign;
- fallbackTextPaint.Typeface = fallback;
fallbackTextPaint.IsAntialias = textPaint.IsAntialias;
// Do the search recursively to select all possible fonts
- width += DrawText(canvas, MoveX(x, width), y, subtext, fallbackTextPaint, isRtl);
+ width += DrawText(canvas, MoveX(x, width), y, subtext, fallbackTextPaint, fallbackTextFont, isRtl);
}
else
{
// Used up all fonts and no fonts can be found, just use current font
- width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint);
+ width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint, textFont, alignment);
}
start = notAtEnd ? enumerator.ElementIndex : text.Length;
@@ -307,7 +302,7 @@ public partial class StripCollageBuilder
// Render the remaining text that current fonts can render
if (start < text.Length)
{
- width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint);
+ width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint, textFont, alignment);
}
return width;
diff --git a/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
index 456b84b8c..46c48357e 100644
--- a/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
+++ b/src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs
@@ -34,10 +34,12 @@ public static class UnplayedCountIndicator
Style = SKPaintStyle.Fill
};
+ using var font = new SKFont();
+
canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
paint.Color = new SKColor(255, 255, 255, 255);
- paint.TextSize = 24;
+ font.Size = 24;
paint.IsAntialias = true;
var y = OffsetFromTopRightCorner + 9;
@@ -55,9 +57,9 @@ public static class UnplayedCountIndicator
{
x -= 15;
y -= 2;
- paint.TextSize = 18;
+ font.Size = 18;
}
- canvas.DrawText(text, x, y, paint);
+ canvas.DrawText(text, x, y, font, paint);
}
}
diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs
index 7718f6c6a..46e5213a8 100644
--- a/src/Jellyfin.Drawing/ImageProcessor.cs
+++ b/src/Jellyfin.Drawing/ImageProcessor.cs
@@ -34,7 +34,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
private const char Version = '3';
private static readonly HashSet<string> _transparentImageTypes
- = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };
+ = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif", ".svg" };
private readonly ILogger<ImageProcessor> _logger;
private readonly IFileSystem _fileSystem;
@@ -524,11 +524,11 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
/// <inheritdoc />
public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{
- _logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath);
+ _logger.LogDebug("Creating image collage and saving to {Path}", options.OutputPath);
_imageEncoder.CreateImageCollage(options, libraryName);
- _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
+ _logger.LogDebug("Completed creation of image collage and saved to {Path}", options.OutputPath);
}
/// <inheritdoc />
diff --git a/src/Jellyfin.Extensions/FileHelper.cs b/src/Jellyfin.Extensions/FileHelper.cs
new file mode 100644
index 000000000..b1ccf8d47
--- /dev/null
+++ b/src/Jellyfin.Extensions/FileHelper.cs
@@ -0,0 +1,20 @@
+using System.IO;
+
+namespace Jellyfin.Extensions;
+
+/// <summary>
+/// Provides helper functions for <see cref="File" />.
+/// </summary>
+public static class FileHelper
+{
+ /// <summary>
+ /// Creates, or truncates a file in the specified path.
+ /// </summary>
+ /// <param name="path">The path and name of the file to create.</param>
+ public static void CreateEmpty(string path)
+ {
+ using (File.OpenHandle(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
+ {
+ }
+ }
+}
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
index 715cbf220..60df47113 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -135,5 +135,18 @@ namespace Jellyfin.Extensions
{
return values.Select(i => (i ?? string.Empty).Trim());
}
+
+ /// <summary>
+ /// Truncates a string at the first null character ('\0').
+ /// </summary>
+ /// <param name="text">The input string.</param>
+ /// <returns>
+ /// The substring up to (but not including) the first null character,
+ /// or the original string if no null character is present.
+ /// </returns>
+ public static string TruncateAtNull(this string text)
+ {
+ return string.IsNullOrEmpty(text) ? text : text.AsSpan().LeftPart('\0').ToString();
+ }
}
}
diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
index 0ca294a28..74182171f 100644
--- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
+++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs
@@ -363,7 +363,7 @@ namespace Jellyfin.LiveTv.Channels
Directory.CreateDirectory(Path.GetDirectoryName(path));
- FileStream createStream = File.Create(path);
+ FileStream createStream = AsyncFile.Create(path);
await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
@@ -445,12 +445,13 @@ namespace Jellyfin.LiveTv.Channels
if (item is null)
{
+ var info = Directory.CreateDirectory(path);
item = new Channel
{
Name = channelInfo.Name,
Id = id,
- DateCreated = _fileSystem.GetCreationTimeUtc(path),
- DateModified = _fileSystem.GetLastWriteTimeUtc(path)
+ DateCreated = info.CreationTimeUtc,
+ DateModified = info.LastWriteTimeUtc
};
isNew = true;
@@ -866,7 +867,7 @@ namespace Jellyfin.LiveTv.Channels
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
- var createStream = File.Create(path);
+ var createStream = AsyncFile.Create(path);
await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false);