aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj2
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs161
-rw-r--r--src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs169
-rw-r--r--src/Jellyfin.Drawing/ImageProcessor.cs26
-rw-r--r--src/Jellyfin.Drawing/Jellyfin.Drawing.csproj2
-rw-r--r--src/Jellyfin.Extensions/Jellyfin.Extensions.csproj4
-rw-r--r--src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs51
-rw-r--r--src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs2
-rw-r--r--src/Jellyfin.LiveTv/Guide/GuideManager.cs191
-rw-r--r--src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs2
-rw-r--r--src/Jellyfin.LiveTv/IO/EncodedRecorder.cs32
-rw-r--r--src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj2
-rw-r--r--src/Jellyfin.LiveTv/Listings/ListingsManager.cs7
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs31
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs2
-rw-r--r--src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs2
-rw-r--r--src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs6
-rw-r--r--src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs3
-rw-r--r--src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs4
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj2
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj2
-rw-r--r--src/Jellyfin.Networking/Jellyfin.Networking.csproj5
-rw-r--r--src/Jellyfin.Networking/Manager/NetworkManager.cs23
-rw-r--r--src/Jellyfin.Networking/PortForwardingHost.cs192
26 files changed, 419 insertions, 508 deletions
diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index 0590ded32..ba402dfe0 100644
--- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- TODO: Remove once we update SkiaSharp > 2.88.5 -->
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index ede93aaa5..2dac5598f 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -195,8 +195,10 @@ public class SkiaEncoder : IImageEncoder
return string.Empty;
}
+ // Use FileStream with FileShare.Read instead of having Skia open the file to allow concurrent read access
+ using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
// Any larger than 128x128 is too slow and there's no visually discernible difference
- return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
+ return BlurHashEncoder.Encode(xComp, yComp, fileStream, 128, 128);
}
private bool RequiresSpecialCharacterHack(string path)
@@ -269,14 +271,24 @@ public class SkiaEncoder : IImageEncoder
}
// create the bitmap
- var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
+ SKBitmap? bitmap = null;
+ try
+ {
+ bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
- // decode
- _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
+ // decode
+ _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
- origin = codec.EncodedOrigin;
+ origin = codec.EncodedOrigin;
- return bitmap;
+ return bitmap!;
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Detected intermediary error decoding image {0}", path);
+ bitmap?.Dispose();
+ throw;
+ }
}
var resultBitmap = SKBitmap.Decode(NormalizePath(path));
@@ -286,17 +298,26 @@ public class SkiaEncoder : IImageEncoder
return Decode(path, true, orientation, out origin);
}
- // If we have to resize these they often end up distorted
- if (resultBitmap.ColorType == SKColorType.Gray8)
+ try
{
- using (resultBitmap)
+ // If we have to resize these they often end up distorted
+ if (resultBitmap.ColorType == SKColorType.Gray8)
{
- return Decode(path, true, orientation, out origin);
+ using (resultBitmap)
+ {
+ return Decode(path, true, orientation, out origin);
+ }
}
- }
- origin = SKEncodedOrigin.TopLeft;
- return resultBitmap;
+ origin = SKEncodedOrigin.TopLeft;
+ return resultBitmap;
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Detected intermediary error decoding image {0}", path);
+ resultBitmap?.Dispose();
+ throw;
+ }
}
private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation)
@@ -335,58 +356,78 @@ public class SkiaEncoder : IImageEncoder
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();
+ SKBitmap? bitmap = null;
+ try
+ {
+ bitmap = new SKBitmap(width, height);
+ using var canvas = new SKCanvas(bitmap);
+ canvas.DrawPicture(svg.Picture);
+ canvas.Flush();
+ canvas.Save();
- return bitmap;
+ return bitmap!;
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Detected intermediary error extracting image {0}", path);
+ bitmap?.Dispose();
+ throw;
+ }
}
private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
{
var needsFlip = origin is SKEncodedOrigin.LeftBottom or SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightBottom or SKEncodedOrigin.RightTop;
- var rotated = needsFlip
- ? new SKBitmap(bitmap.Height, bitmap.Width)
- : new SKBitmap(bitmap.Width, bitmap.Height);
- using var surface = new SKCanvas(rotated);
- var midX = (float)rotated.Width / 2;
- var midY = (float)rotated.Height / 2;
-
- switch (origin)
- {
- case SKEncodedOrigin.TopRight:
- surface.Scale(-1, 1, midX, midY);
- break;
- case SKEncodedOrigin.BottomRight:
- surface.RotateDegrees(180, midX, midY);
- break;
- case SKEncodedOrigin.BottomLeft:
- surface.Scale(1, -1, midX, midY);
- break;
- case SKEncodedOrigin.LeftTop:
- surface.Translate(0, -rotated.Height);
- surface.Scale(1, -1, midX, midY);
- surface.RotateDegrees(-90);
- break;
- case SKEncodedOrigin.RightTop:
- surface.Translate(rotated.Width, 0);
- surface.RotateDegrees(90);
- break;
- case SKEncodedOrigin.RightBottom:
- surface.Translate(rotated.Width, 0);
- surface.Scale(1, -1, midX, midY);
- surface.RotateDegrees(90);
- break;
- case SKEncodedOrigin.LeftBottom:
- surface.Translate(0, rotated.Height);
- surface.RotateDegrees(-90);
- break;
- }
-
- surface.DrawBitmap(bitmap, 0, 0);
- return rotated;
+ SKBitmap? rotated = null;
+ try
+ {
+ rotated = needsFlip
+ ? new SKBitmap(bitmap.Height, bitmap.Width)
+ : new SKBitmap(bitmap.Width, bitmap.Height);
+ using var surface = new SKCanvas(rotated);
+ var midX = (float)rotated.Width / 2;
+ var midY = (float)rotated.Height / 2;
+
+ switch (origin)
+ {
+ case SKEncodedOrigin.TopRight:
+ surface.Scale(-1, 1, midX, midY);
+ break;
+ case SKEncodedOrigin.BottomRight:
+ surface.RotateDegrees(180, midX, midY);
+ break;
+ case SKEncodedOrigin.BottomLeft:
+ surface.Scale(1, -1, midX, midY);
+ break;
+ case SKEncodedOrigin.LeftTop:
+ surface.Translate(0, -rotated.Height);
+ surface.Scale(1, -1, midX, midY);
+ surface.RotateDegrees(-90);
+ break;
+ case SKEncodedOrigin.RightTop:
+ surface.Translate(rotated.Width, 0);
+ surface.RotateDegrees(90);
+ break;
+ case SKEncodedOrigin.RightBottom:
+ surface.Translate(rotated.Width, 0);
+ surface.Scale(1, -1, midX, midY);
+ surface.RotateDegrees(90);
+ break;
+ case SKEncodedOrigin.LeftBottom:
+ surface.Translate(0, rotated.Height);
+ surface.RotateDegrees(-90);
+ break;
+ }
+
+ surface.DrawBitmap(bitmap, 0, 0);
+ return rotated;
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Detected intermediary error rotating image");
+ rotated?.Dispose();
+ throw;
+ }
}
/// <summary>
@@ -562,7 +603,7 @@ public class SkiaEncoder : IImageEncoder
// Only generate the splash screen if we have at least one poster and at least one backdrop/thumbnail.
if (posters.Count > 0 && backdrops.Count > 0)
{
- var splashBuilder = new SplashscreenBuilder(this);
+ var splashBuilder = new SplashscreenBuilder(this, _logger);
var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
splashBuilder.GenerateSplash(posters, backdrops, outputPath);
}
diff --git a/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs b/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
index 990556623..03733d4f8 100644
--- a/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
+++ b/src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Microsoft.Extensions.Logging;
using SkiaSharp;
namespace Jellyfin.Drawing.Skia;
@@ -11,21 +12,24 @@ public class SplashscreenBuilder
{
private const int FinalWidth = 1920;
private const int FinalHeight = 1080;
- // generated collage resolution should be higher than the final resolution
+ // generated collage resolution should be greater than the final resolution
private const int WallWidth = FinalWidth * 3;
private const int WallHeight = FinalHeight * 2;
private const int Rows = 6;
private const int Spacing = 20;
private readonly SkiaEncoder _skiaEncoder;
+ private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="SplashscreenBuilder"/> class.
/// </summary>
/// <param name="skiaEncoder">The SkiaEncoder.</param>
- public SplashscreenBuilder(SkiaEncoder skiaEncoder)
+ /// <param name="logger">The logger.</param>
+ public SplashscreenBuilder(SkiaEncoder skiaEncoder, ILogger logger)
{
_skiaEncoder = skiaEncoder;
+ _logger = logger;
}
/// <summary>
@@ -55,65 +59,76 @@ public class SplashscreenBuilder
var posterIndex = 0;
var backdropIndex = 0;
- var bitmap = new SKBitmap(WallWidth, WallHeight);
- using var canvas = new SKCanvas(bitmap);
- canvas.Clear(SKColors.Black);
-
- int posterHeight = WallHeight / 6;
-
- for (int i = 0; i < Rows; i++)
+ SKBitmap? bitmap = null;
+ try
{
- int imageCounter = Random.Shared.Next(0, 5);
- int currentWidthPos = i * 75;
- int currentHeight = i * (posterHeight + Spacing);
-
- while (currentWidthPos < WallWidth)
- {
- SKBitmap? currentImage;
-
- switch (imageCounter)
- {
- case 0:
- case 2:
- case 3:
- currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, posters, posterIndex, out int newPosterIndex);
- posterIndex = newPosterIndex;
- break;
- default:
- currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrops, backdropIndex, out int newBackdropIndex);
- backdropIndex = newBackdropIndex;
- break;
- }
-
- if (currentImage is null)
- {
- throw new ArgumentException("Not enough valid pictures provided to create a splashscreen!");
- }
-
- // resize to the same aspect as the original
- var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height);
- using var resizedBitmap = new SKBitmap(imageWidth, posterHeight);
- currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High);
-
- // draw on canvas
- canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight);
+ bitmap = new SKBitmap(WallWidth, WallHeight);
+ using var canvas = new SKCanvas(bitmap);
+ canvas.Clear(SKColors.Black);
- currentWidthPos += imageWidth + Spacing;
+ int posterHeight = WallHeight / 6;
- currentImage.Dispose();
+ for (int i = 0; i < Rows; i++)
+ {
+ int imageCounter = Random.Shared.Next(0, 5);
+ int currentWidthPos = i * 75;
+ int currentHeight = i * (posterHeight + Spacing);
- if (imageCounter >= 4)
- {
- imageCounter = 0;
- }
- else
+ while (currentWidthPos < WallWidth)
{
- imageCounter++;
+ SKBitmap? currentImage;
+
+ switch (imageCounter)
+ {
+ case 0:
+ case 2:
+ case 3:
+ currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, posters, posterIndex, out int newPosterIndex);
+ posterIndex = newPosterIndex;
+ break;
+ default:
+ currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrops, backdropIndex, out int newBackdropIndex);
+ backdropIndex = newBackdropIndex;
+ break;
+ }
+
+ if (currentImage is null)
+ {
+ throw new ArgumentException("Not enough valid pictures provided to create a splashscreen!");
+ }
+
+ using (currentImage)
+ {
+ var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height);
+ using var resizedBitmap = new SKBitmap(imageWidth, posterHeight);
+ currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High);
+
+ // draw on canvas
+ canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight);
+
+ // resize to the same aspect as the original
+ currentWidthPos += imageWidth + Spacing;
+ }
+
+ if (imageCounter >= 4)
+ {
+ imageCounter = 0;
+ }
+ else
+ {
+ imageCounter++;
+ }
}
}
- }
- return bitmap;
+ return bitmap;
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Detected intermediary error creating splashscreen image");
+ bitmap?.Dispose();
+ throw;
+ }
}
/// <summary>
@@ -123,25 +138,35 @@ public class SplashscreenBuilder
/// <returns>The transformed image.</returns>
private SKBitmap Transform3D(SKBitmap input)
{
- var bitmap = new SKBitmap(FinalWidth, FinalHeight);
- using var canvas = new SKCanvas(bitmap);
- canvas.Clear(SKColors.Black);
- var matrix = new SKMatrix
+ SKBitmap? bitmap = null;
+ try
{
- ScaleX = 0.324108899f,
- ScaleY = 0.563934922f,
- SkewX = -0.244337708f,
- SkewY = 0.0377609022f,
- TransX = 42.0407715f,
- TransY = -198.104706f,
- Persp0 = -9.08959337E-05f,
- Persp1 = 6.85242048E-05f,
- Persp2 = 0.988209724f
- };
-
- canvas.SetMatrix(matrix);
- canvas.DrawBitmap(input, 0, 0);
-
- return bitmap;
+ bitmap = new SKBitmap(FinalWidth, FinalHeight);
+ using var canvas = new SKCanvas(bitmap);
+ canvas.Clear(SKColors.Black);
+ var matrix = new SKMatrix
+ {
+ ScaleX = 0.324108899f,
+ ScaleY = 0.563934922f,
+ SkewX = -0.244337708f,
+ SkewY = 0.0377609022f,
+ TransX = 42.0407715f,
+ TransY = -198.104706f,
+ Persp0 = -9.08959337E-05f,
+ Persp1 = 6.85242048E-05f,
+ Persp2 = 0.988209724f
+ };
+
+ canvas.SetMatrix(matrix);
+ canvas.DrawBitmap(input, 0, 0);
+
+ return bitmap;
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Detected intermediary error creating splashscreen image transforming the image");
+ bitmap?.Dispose();
+ throw;
+ }
}
}
diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs
index 5d4732234..0bd3b8920 100644
--- a/src/Jellyfin.Drawing/ImageProcessor.cs
+++ b/src/Jellyfin.Drawing/ImageProcessor.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Mime;
+using System.Reflection.Metadata.Ecma335;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -15,6 +16,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
@@ -404,8 +406,27 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
}
/// <inheritdoc />
+ public string GetImageCacheTag(string baseItemPath, DateTime imageDateModified)
+ => (baseItemPath + imageDateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
+
+ /// <inheritdoc />
public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
- => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
+ => GetImageCacheTag(item.Path, image.DateModified);
+
+ /// <inheritdoc />
+ public string GetImageCacheTag(BaseItemDto item, ItemImageInfo image)
+ => GetImageCacheTag(item.Path, image.DateModified);
+
+ /// <inheritdoc />
+ public string? GetImageCacheTag(BaseItemDto item, ChapterInfo chapter)
+ {
+ if (chapter.ImagePath is null)
+ {
+ return null;
+ }
+
+ return GetImageCacheTag(item.Path, chapter.ImageDateModified);
+ }
/// <inheritdoc />
public string? GetImageCacheTag(BaseItem item, ChapterInfo chapter)
@@ -431,8 +452,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
return null;
}
- return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
- .ToString("N", CultureInfo.InvariantCulture);
+ return GetImageCacheTag(user.ProfileImage.Path, user.ProfileImage.LastModified);
}
private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
index 4a02f90f9..5f4b3fe8d 100644
--- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
+++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index f786cc3b4..1613d83bc 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -15,7 +15,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Extensions</PackageId>
- <VersionPrefix>10.10.0</VersionPrefix>
+ <VersionPrefix>10.11.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
index 1466d3a71..7472f9c66 100644
--- a/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
+++ b/src/Jellyfin.Extensions/Json/Converters/JsonDelimitedArrayConverter.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -35,38 +36,27 @@ namespace Jellyfin.Extensions.Json.Converters
var stringEntries = reader.GetString()!.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries);
if (stringEntries.Length == 0)
{
- return Array.Empty<T>();
+ return [];
}
- var parsedValues = new object[stringEntries.Length];
- var convertedCount = 0;
+ var typedValues = new List<T>();
for (var i = 0; i < stringEntries.Length; i++)
{
try
{
- parsedValues[i] = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim()) ?? throw new FormatException();
- convertedCount++;
+ var parsedValue = _typeConverter.ConvertFromInvariantString(stringEntries[i].Trim());
+ if (parsedValue is not null)
+ {
+ typedValues.Add((T)parsedValue);
+ }
}
catch (FormatException)
{
- // TODO log when upgraded to .Net6
- // https://github.com/dotnet/runtime/issues/42975
- // _logger.LogDebug(e, "Error converting value.");
+ // Ignore unconvertible inputs
}
}
- var typedValues = new T[convertedCount];
- var typedValueIndex = 0;
- for (var i = 0; i < stringEntries.Length; i++)
- {
- if (parsedValues[i] is not null)
- {
- typedValues.SetValue(parsedValues[i], typedValueIndex);
- typedValueIndex++;
- }
- }
-
- return typedValues;
+ return typedValues.ToArray();
}
return JsonSerializer.Deserialize<T[]>(ref reader, options);
@@ -75,7 +65,26 @@ namespace Jellyfin.Extensions.Json.Converters
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options)
{
- throw new NotImplementedException();
+ if (value is not null)
+ {
+ writer.WriteStartArray();
+ if (value.Length > 0)
+ {
+ foreach (var it in value)
+ {
+ if (it is not null)
+ {
+ writer.WriteStringValue(it.ToString());
+ }
+ }
+ }
+
+ writer.WriteEndArray();
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
}
}
}
diff --git a/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs b/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs
index 79c5873d5..71e46764a 100644
--- a/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs
+++ b/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs
@@ -79,7 +79,7 @@ namespace Jellyfin.LiveTv.Channels
// Every so often
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks
+ Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks
}
};
}
diff --git a/src/Jellyfin.LiveTv/Guide/GuideManager.cs b/src/Jellyfin.LiveTv/Guide/GuideManager.cs
index f657422a0..b75cc0fb2 100644
--- a/src/Jellyfin.LiveTv/Guide/GuideManager.cs
+++ b/src/Jellyfin.LiveTv/Guide/GuideManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Entities.Libraries;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using Jellyfin.LiveTv.Configuration;
@@ -40,6 +41,11 @@ public class GuideManager : IGuideManager
private readonly LiveTvDtoService _tvDtoService;
/// <summary>
+ /// Amount of days images are pre-cached from external sources.
+ /// </summary>
+ public const int MaxCacheDays = 2;
+
+ /// <summary>
/// Initializes a new instance of the <see cref="GuideManager"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger{TCategoryName}"/>.</param>
@@ -204,14 +210,14 @@ public class GuideManager : IGuideManager
progress.Report(15);
numComplete = 0;
- var programs = new List<Guid>();
+ var programs = new List<LiveTvProgram>();
var channels = new List<Guid>();
var guideDays = GetGuideDays();
- _logger.LogInformation("Refreshing guide with {0} days of guide data", guideDays);
+ _logger.LogInformation("Refreshing guide with {Days} days of guide data", guideDays);
- var maxCacheDate = DateTime.UtcNow.AddDays(2);
+ var maxCacheDate = DateTime.UtcNow.AddDays(MaxCacheDays);
foreach (var currentChannel in list)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -237,22 +243,23 @@ public class GuideManager : IGuideManager
DtoOptions = new DtoOptions(true)
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
- var newPrograms = new List<LiveTvProgram>();
- var updatedPrograms = new List<BaseItem>();
+ var newPrograms = new List<Guid>();
+ var updatedPrograms = new List<Guid>();
foreach (var program in channelPrograms)
{
var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
+ var id = programItem.Id;
if (isNew)
{
- newPrograms.Add(programItem);
+ newPrograms.Add(id);
}
else if (isUpdated)
{
- updatedPrograms.Add(programItem);
+ updatedPrograms.Add(id);
}
- programs.Add(programItem.Id);
+ programs.Add(programItem);
isMovie |= program.IsMovie;
isSeries |= program.IsSeries;
@@ -261,24 +268,30 @@ public class GuideManager : IGuideManager
isKids |= program.IsKids;
}
- _logger.LogDebug("Channel {0} has {1} new programs and {2} updated programs", currentChannel.Name, newPrograms.Count, updatedPrograms.Count);
+ _logger.LogDebug(
+ "Channel {Name} has {NewCount} new programs and {UpdatedCount} updated programs",
+ currentChannel.Name,
+ newPrograms.Count,
+ updatedPrograms.Count);
if (newPrograms.Count > 0)
{
- _libraryManager.CreateItems(newPrograms, null, cancellationToken);
- await PrecacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
+ var newProgramDtos = programs.Where(b => newPrograms.Contains(b.Id)).ToList();
+ _libraryManager.CreateOrUpdateItems(newProgramDtos, null, cancellationToken);
}
if (updatedPrograms.Count > 0)
{
+ var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList();
await _libraryManager.UpdateItemsAsync(
- updatedPrograms,
+ updatedProgramDtos,
currentChannel,
ItemUpdateType.MetadataImport,
cancellationToken).ConfigureAwait(false);
- await PrecacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
}
+ await PreCacheImages(programs, maxCacheDate).ConfigureAwait(false);
+
currentChannel.IsMovie = isMovie;
currentChannel.IsNews = isNews;
currentChannel.IsSports = isSports;
@@ -313,7 +326,8 @@ public class GuideManager : IGuideManager
}
progress.Report(100);
- return new Tuple<List<Guid>, List<Guid>>(channels, programs);
+ var programIds = programs.Select(p => p.Id).ToList();
+ return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
}
private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
@@ -618,77 +632,17 @@ public class GuideManager : IGuideManager
item.IndexNumber = info.EpisodeNumber;
item.ParentIndexNumber = info.SeasonNumber;
- if (!item.HasImage(ImageType.Primary))
- {
- if (!string.IsNullOrWhiteSpace(info.ImagePath))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = info.ImagePath,
- Type = ImageType.Primary
- },
- 0);
- }
- else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = info.ImageUrl,
- Type = ImageType.Primary
- },
- 0);
- }
- }
+ forceUpdate = forceUpdate || UpdateImages(item, info);
- if (!item.HasImage(ImageType.Thumb))
- {
- if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = info.ThumbImageUrl,
- Type = ImageType.Thumb
- },
- 0);
- }
- }
-
- if (!item.HasImage(ImageType.Logo))
+ if (isNew)
{
- if (!string.IsNullOrWhiteSpace(info.LogoImageUrl))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = info.LogoImageUrl,
- Type = ImageType.Logo
- },
- 0);
- }
- }
+ item.OnMetadataChanged();
- if (!item.HasImage(ImageType.Backdrop))
- {
- if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = info.BackdropImageUrl,
- Type = ImageType.Backdrop
- },
- 0);
- }
+ return (item, isNew, false);
}
var isUpdated = false;
- if (isNew)
- {
- }
- else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
+ if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
{
isUpdated = true;
}
@@ -703,7 +657,7 @@ public class GuideManager : IGuideManager
}
}
- if (isNew || isUpdated)
+ if (isUpdated)
{
item.OnMetadataChanged();
}
@@ -711,7 +665,80 @@ public class GuideManager : IGuideManager
return (item, isNew, isUpdated);
}
- private async Task PrecacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
+ private static bool UpdateImages(BaseItem item, ProgramInfo info)
+ {
+ var updated = false;
+
+ // Primary
+ updated |= UpdateImage(ImageType.Primary, item, info);
+
+ // Thumbnail
+ updated |= UpdateImage(ImageType.Thumb, item, info);
+
+ // Logo
+ updated |= UpdateImage(ImageType.Logo, item, info);
+
+ // Backdrop
+ return updated || UpdateImage(ImageType.Backdrop, item, info);
+ }
+
+ private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info)
+ {
+ var image = item.GetImages(imageType).FirstOrDefault();
+ var currentImagePath = image?.Path;
+ var newImagePath = imageType switch
+ {
+ ImageType.Primary => info.ImagePath,
+ _ => string.Empty
+ };
+ var newImageUrl = imageType switch
+ {
+ ImageType.Backdrop => info.BackdropImageUrl,
+ ImageType.Logo => info.LogoImageUrl,
+ ImageType.Primary => info.ImageUrl,
+ ImageType.Thumb => info.ThumbImageUrl,
+ _ => string.Empty
+ };
+
+ var differentImage = newImageUrl?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false
+ || newImagePath?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false;
+ if (!differentImage)
+ {
+ return false;
+ }
+
+ if (!string.IsNullOrWhiteSpace(newImagePath))
+ {
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = newImagePath,
+ Type = imageType
+ },
+ 0);
+
+ return true;
+ }
+
+ if (!string.IsNullOrWhiteSpace(newImageUrl))
+ {
+ item.SetImage(
+ new ItemImageInfo
+ {
+ Path = newImageUrl,
+ Type = imageType
+ },
+ 0);
+
+ return true;
+ }
+
+ item.RemoveImage(image);
+
+ return false;
+ }
+
+ private async Task PreCacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
{
await Parallel.ForEachAsync(
programs
@@ -741,7 +768,7 @@ public class GuideManager : IGuideManager
}
catch (Exception ex)
{
- _logger.LogWarning(ex, "Unable to precache {Url}", imageInfo.Path);
+ _logger.LogWarning(ex, "Unable to pre-cache {Url}", imageInfo.Path);
}
}
}
diff --git a/src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs b/src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs
index a9fde0850..5164d695f 100644
--- a/src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs
+++ b/src/Jellyfin.LiveTv/Guide/RefreshGuideScheduledTask.cs
@@ -66,7 +66,7 @@ public class RefreshGuideScheduledTask : IScheduledTask, IConfigurableScheduledT
{
new TaskTriggerInfo
{
- Type = TaskTriggerInfo.TriggerInterval,
+ Type = TaskTriggerInfoType.IntervalTrigger,
IntervalTicks = TimeSpan.FromHours(24).Ticks
}
};
diff --git a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
index ff00c8999..c04954207 100644
--- a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
+++ b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs
@@ -124,22 +124,7 @@ namespace Jellyfin.LiveTv.IO
private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile)
{
- string videoArgs;
- if (EncodeVideo(mediaSource))
- {
- const int MaxBitrate = 25000000;
- videoArgs = string.Format(
- CultureInfo.InvariantCulture,
- "-codec:v:0 libx264 -force_key_frames \"expr:gte(t,n_forced*5)\" {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -vsync -1 -profile:v high -level 41",
- GetOutputSizeParam(),
- MaxBitrate);
- }
- else
- {
- videoArgs = "-codec:v:0 copy";
- }
-
- videoArgs += " -fflags +genpts";
+ string videoArgs = "-codec:v:0 copy -fflags +genpts";
var flags = new List<string>();
if (mediaSource.IgnoreDts)
@@ -157,7 +142,7 @@ namespace Jellyfin.LiveTv.IO
flags.Add("+genpts");
}
- var inputModifier = "-async 1 -vsync -1";
+ var inputModifier = "-async 1";
if (flags.Count > 0)
{
@@ -205,19 +190,6 @@ namespace Jellyfin.LiveTv.IO
private static string GetAudioArgs(MediaSourceInfo mediaSource)
{
return "-codec:a:0 copy";
-
- // var audioChannels = 2;
- // var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
- // if (audioStream is not null)
- // {
- // audioChannels = audioStream.Channels ?? audioChannels;
- // }
- // return "-codec:a:0 aac -strict experimental -ab 320000";
- }
-
- private static bool EncodeVideo(MediaSourceInfo mediaSource)
- {
- return false;
}
protected string GetOutputSizeParam()
diff --git a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj
index c58889740..f04c02504 100644
--- a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj
+++ b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs
index 3df2d0d2c..39c2bd375 100644
--- a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs
+++ b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs
@@ -230,10 +230,15 @@ public class ListingsManager : IListingsManager
var listingsProviderInfo = config.ListingProviders
.First(info => string.Equals(providerId, info.Id, StringComparison.OrdinalIgnoreCase));
+ var channelMappingExists = listingsProviderInfo.ChannelMappings
+ .Any(pair => string.Equals(pair.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(pair.Value, providerChannelNumber, StringComparison.OrdinalIgnoreCase));
+
listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings
.Where(pair => !string.Equals(pair.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray();
- if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
+ if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase)
+ && !channelMappingExists)
{
var newItem = new NameValuePair
{
diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
index c7a57859e..d6f15906e 100644
--- a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
@@ -19,6 +19,7 @@ using System.Threading.Tasks;
using AsyncKeyedLock;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
+using Jellyfin.LiveTv.Guide;
using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
@@ -38,7 +39,7 @@ namespace Jellyfin.LiveTv.Listings
private readonly IHttpClientFactory _httpClientFactory;
private readonly AsyncNonKeyedLocker _tokenLock = new(1);
- private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
+ private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private DateTime _lastErrorResponse;
private bool _disposed = false;
@@ -86,7 +87,7 @@ namespace Jellyfin.LiveTv.Listings
{
_logger.LogWarning("SchedulesDirect token is empty, returning empty program list");
- return Enumerable.Empty<ProgramInfo>();
+ return [];
}
var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
@@ -94,7 +95,7 @@ namespace Jellyfin.LiveTv.Listings
_logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
var requestList = new List<RequestScheduleForChannelDto>()
{
- new RequestScheduleForChannelDto()
+ new()
{
StationId = channelId,
Date = dates
@@ -109,7 +110,7 @@ namespace Jellyfin.LiveTv.Listings
var dailySchedules = await Request<IReadOnlyList<DayDto>>(options, true, info, cancellationToken).ConfigureAwait(false);
if (dailySchedules is null)
{
- return Array.Empty<ProgramInfo>();
+ return [];
}
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
@@ -120,17 +121,17 @@ namespace Jellyfin.LiveTv.Listings
var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
- var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken)
- .ConfigureAwait(false);
+ var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
if (programDetails is null)
{
- return Array.Empty<ProgramInfo>();
+ return [];
}
var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
var programIdsWithImages = programDetails
- .Where(p => p.HasImageArtwork).Select(p => p.ProgramId)
+ .Where(p => p.HasImageArtwork)
+ .Select(p => p.ProgramId)
.ToList();
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
@@ -138,17 +139,15 @@ namespace Jellyfin.LiveTv.Listings
var programsInfo = new List<ProgramInfo>();
foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs))
{
- // _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
- // " which corresponds to channel " + channelNumber + " and program id " +
- // schedule.ProgramId + " which says it has images? " +
- // programDict[schedule.ProgramId].hasImageArtwork);
-
if (string.IsNullOrEmpty(schedule.ProgramId))
{
continue;
}
- if (images is not null)
+ // Only add images which will be pre-cached until we can implement dynamic token fetching
+ var endDate = schedule.AirDateTime?.AddSeconds(schedule.Duration);
+ var willBeCached = endDate.HasValue && endDate.Value < DateTime.UtcNow.AddDays(GuideManager.MaxCacheDays);
+ if (willBeCached && images is not null)
{
var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
if (imageIndex > -1)
@@ -456,7 +455,7 @@ namespace Jellyfin.LiveTv.Listings
if (programIds.Count == 0)
{
- return Array.Empty<ShowImagesDto>();
+ return [];
}
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
@@ -483,7 +482,7 @@ namespace Jellyfin.LiveTv.Listings
{
_logger.LogError(ex, "Error getting image info from schedules direct");
- return Array.Empty<ShowImagesDto>();
+ return [];
}
}
diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
index 856b7a89b..79bcbe649 100644
--- a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs
@@ -8,7 +8,7 @@ namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
public class LineupDto
{
/// <summary>
- /// Gets or sets the linup.
+ /// Gets or sets the lineup.
/// </summary>
[JsonPropertyName("lineup")]
public string? Lineup { get; set; }
diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
index ea583a1ce..89c4ee5a8 100644
--- a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs
@@ -23,7 +23,7 @@ namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
/// Gets or sets the provider callsign.
/// </summary>
[JsonPropertyName("providerCallsign")]
- public string? ProvderCallsign { get; set; }
+ public string? ProviderCallsign { get; set; }
/// <summary>
/// Gets or sets the logical channel number.
diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
index cafc8e273..7998a7a92 100644
--- a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs
@@ -8,7 +8,7 @@ namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
public class MetadataDto
{
/// <summary>
- /// Gets or sets the linup.
+ /// Gets or sets the lineup.
/// </summary>
[JsonPropertyName("lineup")]
public string? Lineup { get; set; }
diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
index 8c3906f86..7bfc4bc8b 100644
--- a/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
+++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs
@@ -64,7 +64,7 @@ namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos
public IReadOnlyList<MetadataProgramsDto> Metadata { get; set; } = Array.Empty<MetadataProgramsDto>();
/// <summary>
- /// Gets or sets the list of content raitings.
+ /// Gets or sets the list of content ratings.
/// </summary>
[JsonPropertyName("contentRating")]
public IReadOnlyList<ContentRatingDto> ContentRating { get; set; } = Array.Empty<ContentRatingDto>();
diff --git a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
index 7dc30f727..7938b7a6e 100644
--- a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs
@@ -84,10 +84,11 @@ namespace Jellyfin.LiveTv.Listings
_logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false);
+ var redirectedUrl = response.RequestMessage?.RequestUri?.ToString() ?? info.Path;
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
- return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false);
+ return await UnzipIfNeededAndCopy(redirectedUrl, stream, cacheFile, cancellationToken).ConfigureAwait(false);
}
}
else
@@ -112,7 +113,8 @@ namespace Jellyfin.LiveTv.Listings
await using (fileStream.ConfigureAwait(false))
{
- if (Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gz", StringComparison.OrdinalIgnoreCase))
+ if (Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gz", StringComparison.OrdinalIgnoreCase) ||
+ Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gzip", StringComparison.OrdinalIgnoreCase))
{
try
{
diff --git a/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs
index 9e7323f5b..6a68b8c25 100644
--- a/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs
+++ b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs
@@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.Json;
+using System.Threading;
using Jellyfin.Extensions.Json;
using Microsoft.Extensions.Logging;
@@ -15,7 +16,7 @@ namespace Jellyfin.LiveTv.Timers
where T : class
{
private readonly string _dataPath;
- private readonly object _fileDataLock = new object();
+ private readonly Lock _fileDataLock = new();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private T[]? _items;
diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs
index c8d678e2f..e3afe1513 100644
--- a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs
+++ b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs
@@ -93,7 +93,7 @@ namespace Jellyfin.LiveTv.TunerHosts
}
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{
- var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
+ var channel = GetChannelInfo(extInf, tunerHostId, trimmedLine);
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
channel.Path = trimmedLine;
@@ -106,7 +106,7 @@ namespace Jellyfin.LiveTv.TunerHosts
return channels;
}
- private ChannelInfo GetChannelnfo(string extInf, string tunerHostId, string mediaUrl)
+ private ChannelInfo GetChannelInfo(string extInf, string tunerHostId, string mediaUrl)
{
var channel = new ChannelInfo()
{
diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
index ee79802a1..dc581724a 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
+++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
index c79dcee3c..c826d3d9c 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/src/Jellyfin.Networking/Jellyfin.Networking.csproj b/src/Jellyfin.Networking/Jellyfin.Networking.csproj
index 24b3ecaab..1a146549d 100644
--- a/src/Jellyfin.Networking/Jellyfin.Networking.csproj
+++ b/src/Jellyfin.Networking/Jellyfin.Networking.csproj
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@@ -14,7 +14,4 @@
<ProjectReference Include="..\..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
</ItemGroup>
- <ItemGroup>
- <PackageReference Include="Mono.Nat" />
- </ItemGroup>
</Project>
diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs
index b285b836b..dd01e9533 100644
--- a/src/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -27,7 +27,7 @@ public class NetworkManager : INetworkManager, IDisposable
/// <summary>
/// Threading lock for network properties.
/// </summary>
- private readonly object _initLock;
+ private readonly Lock _initLock;
private readonly ILogger<NetworkManager> _logger;
@@ -35,7 +35,7 @@ public class NetworkManager : INetworkManager, IDisposable
private readonly IConfiguration _startupConfig;
- private readonly object _networkEventLock;
+ private readonly Lock _networkEventLock;
/// <summary>
/// Holds the published server URLs and the IPs to use them on.
@@ -57,7 +57,7 @@ public class NetworkManager : INetworkManager, IDisposable
/// <summary>
/// Dictionary containing interface addresses and their subnets.
/// </summary>
- private IReadOnlyList<IPData> _interfaces;
+ private List<IPData> _interfaces;
/// <summary>
/// Unfiltered user defined LAN subnets (<see cref="NetworkConfiguration.LocalNetworkSubnets"/>)
@@ -81,7 +81,6 @@ public class NetworkManager : INetworkManager, IDisposable
/// <param name="configurationManager">The <see cref="IConfigurationManager"/> instance.</param>
/// <param name="startupConfig">The <see cref="IConfiguration"/> instance holding startup parameters.</param>
/// <param name="logger">Logger to use for messages.</param>
-#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this.
public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger<NetworkManager> logger)
{
ArgumentNullException.ThrowIfNull(logger);
@@ -94,7 +93,7 @@ public class NetworkManager : INetworkManager, IDisposable
_interfaces = new List<IPData>();
_macAddresses = new List<PhysicalAddress>();
_publishedServerUrls = new List<PublishedServerUriOverride>();
- _networkEventLock = new object();
+ _networkEventLock = new();
_remoteAddressFilter = new List<IPNetwork>();
_ = bool.TryParse(startupConfig[DetectNetworkChangeKey], out var detectNetworkChange);
@@ -109,7 +108,6 @@ public class NetworkManager : INetworkManager, IDisposable
_configurationManager.NamedConfigurationUpdated += ConfigurationUpdated;
}
-#pragma warning restore CS8618 // Non-nullable field is uninitialized.
/// <summary>
/// Event triggered on network changes.
@@ -312,6 +310,7 @@ public class NetworkManager : INetworkManager, IDisposable
/// <summary>
/// Initializes internal LAN cache.
/// </summary>
+ [MemberNotNull(nameof(_lanSubnets), nameof(_excludedSubnets))]
private void InitializeLan(NetworkConfiguration config)
{
lock (_initLock)
@@ -591,6 +590,7 @@ public class NetworkManager : INetworkManager, IDisposable
/// Reloads all settings and re-Initializes the instance.
/// </summary>
/// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param>
+ [MemberNotNull(nameof(_lanSubnets), nameof(_excludedSubnets))]
public void UpdateSettings(object configuration)
{
ArgumentNullException.ThrowIfNull(configuration);
@@ -702,7 +702,7 @@ public class NetworkManager : INetworkManager, IDisposable
return false;
}
}
- else if (!_lanSubnets.Any(x => x.Contains(remoteIP)))
+ else if (!IsInLocalNetwork(remoteIP))
{
// Remote not enabled. So everyone should be LAN.
return false;
@@ -973,7 +973,7 @@ public class NetworkManager : INetworkManager, IDisposable
bindPreference = string.Empty;
int? port = null;
- // Only consider subnets including the source IP, prefering specific overrides
+ // Only consider subnets including the source IP, preferring specific overrides
List<PublishedServerUriOverride> validPublishedServerUrls;
if (!isInExternalSubnet)
{
@@ -997,7 +997,9 @@ public class NetworkManager : INetworkManager, IDisposable
// Get interface matching override subnet
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
- if (intf?.Address is not null)
+ if (intf?.Address is not null
+ || (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any))
+ || (data.Data.AddressFamily == AddressFamily.InterNetworkV6 && data.Data.Address.Equals(IPAddress.IPv6Any)))
{
// If matching interface is found, use override
bindPreference = data.OverrideUri;
@@ -1025,6 +1027,7 @@ public class NetworkManager : INetworkManager, IDisposable
}
_logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
+
return true;
}
@@ -1063,6 +1066,7 @@ public class NetworkManager : INetworkManager, IDisposable
// If none exists, this will select the first external interface if there is one.
bindAddress = externalInterfaces
.OrderByDescending(x => x.Subnet.Contains(source))
+ .ThenByDescending(x => x.Subnet.PrefixLength)
.ThenBy(x => x.Index)
.Select(x => x.Address)
.First();
@@ -1080,6 +1084,7 @@ public class NetworkManager : INetworkManager, IDisposable
// If none exists, this will select the first internal interface if there is one.
bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
.OrderByDescending(x => x.Subnet.Contains(source))
+ .ThenByDescending(x => x.Subnet.PrefixLength)
.ThenBy(x => x.Index)
.Select(x => x.Address)
.FirstOrDefault();
diff --git a/src/Jellyfin.Networking/PortForwardingHost.cs b/src/Jellyfin.Networking/PortForwardingHost.cs
deleted file mode 100644
index d01343624..000000000
--- a/src/Jellyfin.Networking/PortForwardingHost.cs
+++ /dev/null
@@ -1,192 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Net;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using Mono.Nat;
-
-namespace Jellyfin.Networking;
-
-/// <summary>
-/// <see cref="IHostedService"/> responsible for UPnP port forwarding.
-/// </summary>
-public sealed class PortForwardingHost : IHostedService, IDisposable
-{
- private readonly IServerApplicationHost _appHost;
- private readonly ILogger<PortForwardingHost> _logger;
- private readonly IServerConfigurationManager _config;
- private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new();
-
- private Timer? _timer;
- private string? _configIdentifier;
- private bool _disposed;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="PortForwardingHost"/> class.
- /// </summary>
- /// <param name="logger">The logger.</param>
- /// <param name="appHost">The application host.</param>
- /// <param name="config">The configuration manager.</param>
- public PortForwardingHost(
- ILogger<PortForwardingHost> logger,
- IServerApplicationHost appHost,
- IServerConfigurationManager config)
- {
- _logger = logger;
- _appHost = appHost;
- _config = config;
- }
-
- private string GetConfigIdentifier()
- {
- const char Separator = '|';
- var config = _config.GetNetworkConfiguration();
-
- return new StringBuilder(32)
- .Append(config.EnableUPnP).Append(Separator)
- .Append(config.PublicHttpPort).Append(Separator)
- .Append(config.PublicHttpsPort).Append(Separator)
- .Append(_appHost.HttpPort).Append(Separator)
- .Append(_appHost.HttpsPort).Append(Separator)
- .Append(_appHost.ListenWithHttps).Append(Separator)
- .Append(config.EnableRemoteAccess).Append(Separator)
- .ToString();
- }
-
- private void OnConfigurationUpdated(object? sender, EventArgs e)
- {
- var oldConfigIdentifier = _configIdentifier;
- _configIdentifier = GetConfigIdentifier();
-
- if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase))
- {
- Stop();
- Start();
- }
- }
-
- /// <inheritdoc />
- public Task StartAsync(CancellationToken cancellationToken)
- {
- Start();
-
- _config.ConfigurationUpdated += OnConfigurationUpdated;
-
- return Task.CompletedTask;
- }
-
- /// <inheritdoc />
- public Task StopAsync(CancellationToken cancellationToken)
- {
- Stop();
-
- return Task.CompletedTask;
- }
-
- private void Start()
- {
- var config = _config.GetNetworkConfiguration();
- if (!config.EnableUPnP || !config.EnableRemoteAccess)
- {
- return;
- }
-
- _logger.LogInformation("Starting NAT discovery");
-
- NatUtility.DeviceFound += OnNatUtilityDeviceFound;
- NatUtility.StartDiscovery();
-
- _timer?.Dispose();
- _timer = new Timer(_ => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
- }
-
- private void Stop()
- {
- _logger.LogInformation("Stopping NAT discovery");
-
- NatUtility.StopDiscovery();
- NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
-
- _timer?.Dispose();
- _timer = null;
- }
-
- private async void OnNatUtilityDeviceFound(object? sender, DeviceEventArgs e)
- {
- ObjectDisposedException.ThrowIf(_disposed, this);
-
- try
- {
- // On some systems the device discovered event seems to fire repeatedly
- // This check will help ensure we're not trying to port map the same device over and over
- if (!_createdRules.TryAdd(e.Device.DeviceEndpoint, 0))
- {
- return;
- }
-
- await Task.WhenAll(CreatePortMaps(e.Device)).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error creating port forwarding rules");
- }
- }
-
- private IEnumerable<Task> CreatePortMaps(INatDevice device)
- {
- var config = _config.GetNetworkConfiguration();
- yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort);
-
- if (_appHost.ListenWithHttps)
- {
- yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort);
- }
- }
-
- private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort)
- {
- _logger.LogDebug(
- "Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}",
- privatePort,
- publicPort,
- device.DeviceEndpoint);
-
- try
- {
- var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name);
- await device.CreatePortMapAsync(mapping).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(
- ex,
- "Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.",
- privatePort,
- publicPort,
- device.DeviceEndpoint);
- }
- }
-
- /// <inheritdoc />
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- _config.ConfigurationUpdated -= OnConfigurationUpdated;
-
- _timer?.Dispose();
- _timer = null;
-
- _disposed = true;
- }
-}