diff options
Diffstat (limited to 'src')
6 files changed, 134 insertions, 74 deletions
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs index da63df8e29..2b52abcb5b 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs @@ -61,7 +61,7 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider var customOptions = databaseConfiguration.CustomProviderOptions?.Options; var sqliteConnectionBuilder = new SqliteConnectionStringBuilder(); - sqliteConnectionBuilder.DataSource = Path.Combine(_applicationPaths.DataPath, "jellyfin.db"); + sqliteConnectionBuilder.DataSource = GetOption(customOptions, "path", e => e, () => Path.Combine(_applicationPaths.DataPath, "jellyfin.db")); sqliteConnectionBuilder.Cache = GetOption(customOptions, "cache", Enum.Parse<SqliteCacheMode>, () => SqliteCacheMode.Default); sqliteConnectionBuilder.Pooling = GetOption(customOptions, "pooling", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), () => true); sqliteConnectionBuilder.DefaultTimeout = GetOption(customOptions, "command-timeout", int.Parse, () => 30); diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index c6eab92ead..3f7ae4d2cd 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -24,61 +24,29 @@ public class SkiaEncoder : IImageEncoder 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; - private static readonly SKTypeface[] _typefaces; + private static readonly SKTypeface?[] _typefaces = InitializeTypefaces(); + private static readonly SKImageFilter _imageFilter = SKImageFilter.CreateMatrixConvolution( + new SKSizeI(3, 3), + [ + 0, -.1f, 0, + -.1f, 1.4f, -.1f, + 0, -.1f, 0 + ], + 1f, + 0f, + new SKPointI(1, 1), + SKShaderTileMode.Clamp, + true); /// <summary> /// The default sampling options, equivalent to old high quality filter settings when upscaling. /// </summary> - public static readonly SKSamplingOptions UpscaleSamplingOptions; + public static readonly SKSamplingOptions UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell); /// <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 - { - var kernel = new[] - { - 0, -.1f, 0, - -.1f, 1.4f, -.1f, - 0, -.1f, 0, - }; - - var kernelSize = new SKSizeI(3, 3); - var kernelOffset = new SKPointI(1, 1); - _imageFilter = SKImageFilter.CreateMatrixConvolution( - kernelSize, - kernel, - 1f, - 0f, - kernelOffset, - SKShaderTileMode.Clamp, - true); - - // Initialize the list of typefaces - // We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point - // But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, π΅π»ββοΈ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F) - _typefaces = - [ - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ιΈ‘'), // CJK Simplified Chinese - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ι'), // CJK Traditional Chinese - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'γ'), // CJK Japanese - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'κ°'), // CJK Korean - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 128169), // Emojis, 128169 is the π©emoji - SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'Χ'), // Hebrew - 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); - } + public static readonly SKSamplingOptions DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear); /// <summary> /// Initializes a new instance of the <see cref="SkiaEncoder"/> class. @@ -132,7 +100,7 @@ public class SkiaEncoder : IImageEncoder /// <summary> /// Gets the default typeface to use. /// </summary> - public static SKTypeface DefaultTypeFace => _typefaces.Last(); + public static SKTypeface? DefaultTypeFace => _typefaces.Last(); /// <summary> /// Check if the native lib is available. @@ -153,6 +121,40 @@ public class SkiaEncoder : IImageEncoder } /// <summary> + /// Initialize the list of typefaces + /// We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point + /// But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, π΅π»ββοΈ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F). + /// </summary> + /// <returns>The list of typefaces.</returns> + private static SKTypeface?[] InitializeTypefaces() + { + int[] chars = [ + 'ιΈ‘', // CJK Simplified Chinese + 'ι', // CJK Traditional Chinese + 'γ', // CJK Japanese + 'κ°', // CJK Korean + 128169, // Emojis, 128169 is the Pile of Poo (π©) emoji + 'Χ', // Hebrew + 'Ω' // Arabic + ]; + var fonts = new List<SKTypeface>(chars.Length + 1); + foreach (var ch in chars) + { + var font = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, ch); + if (font is not null) + { + fonts.Add(font); + } + } + + // Default font + fonts.Add(SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) + ?? SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'a')); + + return fonts.ToArray(); + } + + /// <summary> /// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>. /// </summary> /// <param name="selectedFormat">The format to convert.</param> @@ -809,7 +811,7 @@ public class SkiaEncoder : IImageEncoder { foreach (var typeface in _typefaces) { - if (typeface.ContainsGlyphs(c)) + if (typeface is not null && typeface.ContainsGlyphs(c)) { return typeface; } diff --git a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs index be7ff52977..d877a0d124 100644 --- a/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs +++ b/src/Jellyfin.LiveTv/IO/EncodedRecorder.cs @@ -156,6 +156,13 @@ namespace Jellyfin.LiveTv.IO if (mediaSource.ReadAtNativeFramerate) { inputModifier += " -re"; + + // Set a larger catchup value to revert to the old behavior, + // otherwise, remuxing might stall due to this new option + if (_mediaEncoder.EncoderVersion >= new Version(8, 0)) + { + inputModifier += " -readrate_catchup 100"; + } } if (mediaSource.RequiresLooping) diff --git a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs index 7938b7a6e4..318c3a2d36 100644 --- a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs +++ b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -62,21 +60,21 @@ namespace Jellyfin.LiveTv.Listings _logger.LogInformation("xmltv path: {Path}", info.Path); string cacheFilename = info.Id + ".xml"; - string cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename); - - if (File.Exists(cacheFile) && File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge)) - { - return cacheFile; - } + string cacheDir = Path.Join(_config.ApplicationPaths.CachePath, "xmltv"); + string cacheFile = Path.Join(cacheDir, cacheFilename); - // Must check if file exists as parent directory may not exist. if (File.Exists(cacheFile)) { + if (File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge)) + { + return cacheFile; + } + File.Delete(cacheFile); } else { - Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)); + Directory.CreateDirectory(cacheDir); } if (info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) @@ -154,33 +152,37 @@ namespace Jellyfin.LiveTv.Listings private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info) { - string episodeTitle = program.Episode.Title; + string? episodeTitle = program.Episode?.Title; var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList(); + var imageUrl = program.Icons.FirstOrDefault()?.Source; + var rating = program.Ratings.FirstOrDefault()?.Value; + var starRating = program.StarRatings?.FirstOrDefault()?.StarRating; var programInfo = new ProgramInfo { ChannelId = program.ChannelId, EndDate = program.EndDate.UtcDateTime, - EpisodeNumber = program.Episode.Episode, + EpisodeNumber = program.Episode?.Episode, EpisodeTitle = episodeTitle, Genres = programCategories, StartDate = program.StartDate.UtcDateTime, Name = program.Title, Overview = program.Description, ProductionYear = program.CopyrightDate?.Year, - SeasonNumber = program.Episode.Series, - IsSeries = program.Episode.Episode is not null, + SeasonNumber = program.Episode?.Series, + IsSeries = program.Episode?.Episode is not null, IsRepeat = program.IsPreviouslyShown && !program.IsNew, IsPremiere = program.Premiere is not null, + IsLive = program.IsLive, IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), - ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source, - HasImage = !string.IsNullOrEmpty(program.Icon?.Source), - OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value, - CommunityRating = program.StarRating, - SeriesId = program.Episode.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture) + ImageUrl = string.IsNullOrEmpty(imageUrl) ? null : imageUrl, + HasImage = !string.IsNullOrEmpty(imageUrl), + OfficialRating = string.IsNullOrEmpty(rating) ? null : rating, + CommunityRating = starRating is null ? null : (float)starRating.Value, + SeriesId = program.Episode?.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture) }; if (string.IsNullOrWhiteSpace(program.ProgramId)) @@ -261,7 +263,7 @@ namespace Jellyfin.LiveTv.Listings { Id = c.Id, Name = c.DisplayName, - ImageUrl = string.IsNullOrEmpty(c.Icon?.Source) ? null : c.Icon.Source, + ImageUrl = string.IsNullOrEmpty(c.Icons.FirstOrDefault()?.Source) ? null : c.Icons.FirstOrDefault()!.Source, Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number }).ToList(); } diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs index 2270758454..5da7762f6f 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs @@ -93,6 +93,13 @@ namespace Jellyfin.LiveTv.TunerHosts } else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#')) { + if (!IsValidChannelUrl(trimmedLine)) + { + _logger.LogWarning("Skipping M3U channel entry with non-HTTP path: {Path}", trimmedLine); + extInf = string.Empty; + continue; + } + var channel = GetChannelInfo(extInf, tunerHostId, trimmedLine); channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture); @@ -247,6 +254,16 @@ namespace Jellyfin.LiveTv.TunerHosts return numberString; } + private static bool IsValidChannelUrl(string url) + { + return Uri.TryCreate(url, UriKind.Absolute, out var uri) + && (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase) + || string.Equals(uri.Scheme, "udp", StringComparison.OrdinalIgnoreCase)); + } + private static bool IsValidChannelNumber(string numberString) { if (string.IsNullOrWhiteSpace(numberString) diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index a9136aad48..6a8a91fa51 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -747,12 +747,13 @@ public class NetworkManager : INetworkManager, IDisposable /// <inheritdoc/> public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false) { - return NetworkManager.GetAllBindInterfaces(individualInterfaces, _configurationManager, _interfaces, IsIPv4Enabled, IsIPv6Enabled); + return NetworkManager.GetAllBindInterfaces(_logger, individualInterfaces, _configurationManager, _interfaces, IsIPv4Enabled, IsIPv6Enabled); } /// <summary> /// Reads the jellyfin configuration of the configuration manager and produces a list of interfaces that should be bound. /// </summary> + /// <param name="logger">Logger to use for messages.</param> /// <param name="individualInterfaces">Defines that only known interfaces should be used.</param> /// <param name="configurationManager">The ConfigurationManager.</param> /// <param name="knownInterfaces">The known interfaces that gets returned if possible or instructed.</param> @@ -760,6 +761,7 @@ public class NetworkManager : INetworkManager, IDisposable /// <param name="readIpv6">Include IPV6 type interfaces.</param> /// <returns>A list of ip address of which jellyfin should bind to.</returns> public static IReadOnlyList<IPData> GetAllBindInterfaces( + ILogger<NetworkManager> logger, bool individualInterfaces, IConfigurationManager configurationManager, IReadOnlyList<IPData> knownInterfaces, @@ -773,6 +775,13 @@ public class NetworkManager : INetworkManager, IDisposable return knownInterfaces; } + // TODO: remove when upgrade to dotnet 11 is done + if (readIpv6 && !Socket.OSSupportsIPv6) + { + logger.LogWarning("IPv6 Unsupported by OS, not listening on IPv6"); + readIpv6 = false; + } + // No bind address and no exclusions, so listen on all interfaces. var result = new List<IPData>(); if (readIpv4 && readIpv6) @@ -869,7 +878,20 @@ public class NetworkManager : INetworkManager, IDisposable if (availableInterfaces.Count == 0) { // There isn't any others, so we'll use the loopback. - result = IsIPv4Enabled && !IsIPv6Enabled ? "127.0.0.1" : "::1"; + // Prefer loopback address matching the source's address family + if (source is not null && source.AddressFamily == AddressFamily.InterNetwork && IsIPv4Enabled) + { + result = "127.0.0.1"; + } + else if (source is not null && source.AddressFamily == AddressFamily.InterNetworkV6 && IsIPv6Enabled) + { + result = "::1"; + } + else + { + result = IsIPv4Enabled ? "127.0.0.1" : "::1"; + } + _logger.LogWarning("{Source}: Only loopback {Result} returned, using that as bind address.", source, result); return result; } @@ -894,9 +916,19 @@ public class NetworkManager : INetworkManager, IDisposable } } - // Fallback to first available interface + // Fallback to an interface matching the source's address family, or first available + var preferredInterface = availableInterfaces + .FirstOrDefault(x => x.Address.AddressFamily == source.AddressFamily); + + if (preferredInterface is not null) + { + result = NetworkUtils.FormatIPString(preferredInterface.Address); + _logger.LogDebug("{Source}: No matching subnet found, using interface with matching address family: {Result}", source, result); + return result; + } + result = NetworkUtils.FormatIPString(availableInterfaces[0].Address); - _logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result); + _logger.LogDebug("{Source}: No matching interfaces found, using first available interface as bind address: {Result}", source, result); return result; } |
