diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 19d65ea0c1..926d1d3224 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -26,6 +26,8 @@ jobs:
BuildConfiguration: linux.amd64-musl
Linux.arm64:
BuildConfiguration: linux.arm64
+ Linux.musl-linux-arm64:
+ BuildConfiguration: linux.musl-linux-arm64
Linux.armhf:
BuildConfiguration: linux.armhf
Windows.amd64:
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 87086a7280..9c08929e78 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -157,6 +157,7 @@
- [jonas-resch](https://github.com/jonas-resch)
- [vgambier](https://github.com/vgambier)
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
+ - [RealGreenDragon](https://github.com/RealGreenDragon)
# Emby Contributors
@@ -225,3 +226,4 @@
- [gnuyent](https://github.com/gnuyent)
- [Matthew Jones](https://github.com/matthew-jones-uk)
- [Jakob Kukla](https://github.com/jakobkukla)
+ - [Utku Özdemir](https://github.com/utkuozdemir)
diff --git a/Dockerfile b/Dockerfile
index c3038b1d26..5ca233b8ff 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -72,7 +72,7 @@ COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# because of changes in docker and systemd we need to not build in parallel at the moment
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
FROM app
diff --git a/Dockerfile.arm b/Dockerfile.arm
index b25e6039f4..8da17ee5fc 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -64,7 +64,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none
FROM app
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index d0be834dd1..790be1c39d 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -55,7 +55,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none
FROM app
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index df6539a5a6..8e3a335c66 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -446,7 +446,7 @@ namespace Emby.Dlna.Didl
///
///
/// If context is a season, this will return a string containing just episode number and name.
- /// Otherwise the result will include series nams and season number.
+ /// Otherwise the result will include series names and season number.
///
/// The episode.
/// Current context.
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index fe78d74ee7..74624334bb 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -123,7 +123,7 @@ namespace Emby.Dlna
///
/// Attempts to match a device with a profile.
/// Rules:
- /// - If the profile field has no value, the field matches irregardless of its contents.
+ /// - If the profile field has no value, the field matches regardless of its contents.
/// - the profile field can be an exact match, or a reg exp.
///
/// The of the device.
diff --git a/Emby.Dlna/IDlnaEventManager.cs b/Emby.Dlna/IDlnaEventManager.cs
index 33cf0896ba..eea030d6d1 100644
--- a/Emby.Dlna/IDlnaEventManager.cs
+++ b/Emby.Dlna/IDlnaEventManager.cs
@@ -16,7 +16,7 @@ namespace Emby.Dlna
///
/// The subscription identifier.
/// The notification type.
- /// The requested timeout as a sting.
+ /// The requested timeout as a string.
/// The callback url.
/// The response.
EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl);
@@ -25,7 +25,7 @@ namespace Emby.Dlna
/// Creates the event subscription.
///
/// The notification type.
- /// The requested timeout as a sting.
+ /// The requested timeout as a string.
/// The callback url.
/// The response.
EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl);
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index 8eb90f4455..0b480f5ab0 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -235,7 +235,7 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("Setting mute");
var value = mute ? 1 : 0;
- await new SsdpHttpClient(_httpClientFactory)
+ await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
@@ -276,7 +276,7 @@ namespace Emby.Dlna.PlayTo
// Remote control will perform better
Volume = value;
- await new SsdpHttpClient(_httpClientFactory)
+ await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
@@ -303,7 +303,7 @@ namespace Emby.Dlna.PlayTo
throw new InvalidOperationException("Unable to find service");
}
- await new SsdpHttpClient(_httpClientFactory)
+ await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
@@ -343,7 +343,7 @@ namespace Emby.Dlna.PlayTo
}
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
- await new SsdpHttpClient(_httpClientFactory)
+ await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
@@ -400,7 +400,8 @@ namespace Emby.Dlna.PlayTo
}
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
- await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken)
+ await new DlnaHttpClient(_logger, _httpClientFactory)
+ .SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken)
.ConfigureAwait(false);
}
@@ -428,7 +429,7 @@ namespace Emby.Dlna.PlayTo
throw new InvalidOperationException("Unable to find service");
}
- return new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
+ return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
@@ -461,7 +462,7 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService();
- await new SsdpHttpClient(_httpClientFactory)
+ await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
@@ -485,7 +486,7 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService();
- await new SsdpHttpClient(_httpClientFactory)
+ await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
@@ -618,7 +619,7 @@ namespace Emby.Dlna.PlayTo
return;
}
- var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
+ var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
@@ -668,7 +669,7 @@ namespace Emby.Dlna.PlayTo
return;
}
- var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
+ var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
@@ -701,7 +702,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
- var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
+ var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
@@ -747,7 +748,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
- var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
+ var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
@@ -819,7 +820,7 @@ namespace Emby.Dlna.PlayTo
return (false, null);
}
- var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
+ var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
@@ -997,7 +998,7 @@ namespace Emby.Dlna.PlayTo
string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
- var httpClient = new SsdpHttpClient(_httpClientFactory);
+ var httpClient = new DlnaHttpClient(_logger, _httpClientFactory);
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
if (document == null)
@@ -1029,7 +1030,7 @@ namespace Emby.Dlna.PlayTo
string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
- var httpClient = new SsdpHttpClient(_httpClientFactory);
+ var httpClient = new DlnaHttpClient(_logger, _httpClientFactory);
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
if (document == null)
@@ -1064,7 +1065,7 @@ namespace Emby.Dlna.PlayTo
public static async Task CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
{
- var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
+ var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory);
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
if (document == null)
diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs
new file mode 100644
index 0000000000..75ff542dd8
--- /dev/null
+++ b/Emby.Dlna/PlayTo/DlnaHttpClient.cs
@@ -0,0 +1,108 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Globalization;
+using System.Net.Http;
+using System.Net.Mime;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.Linq;
+using Emby.Dlna.Common;
+using MediaBrowser.Common.Net;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Dlna.PlayTo
+{
+ public class DlnaHttpClient
+ {
+ private readonly ILogger _logger;
+ private readonly IHttpClientFactory _httpClientFactory;
+
+ public DlnaHttpClient(ILogger logger, IHttpClientFactory httpClientFactory)
+ {
+ _logger = logger;
+ _httpClientFactory = httpClientFactory;
+ }
+
+ private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
+ {
+ // If it's already a complete url, don't stick anything onto the front of it
+ if (serviceUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ {
+ return serviceUrl;
+ }
+
+ if (!serviceUrl.StartsWith('/'))
+ {
+ serviceUrl = "/" + serviceUrl;
+ }
+
+ return baseUrl + serviceUrl;
+ }
+
+ private async Task SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Dlna).SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+ await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ return await XDocument.LoadAsync(
+ stream,
+ LoadOptions.None,
+ cancellationToken).ConfigureAwait(false);
+ }
+ catch (XmlException ex)
+ {
+ _logger.LogError(ex, "Failed to parse response");
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ _logger.LogDebug("Malformed response: {Content}\n", await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
+ }
+
+ return null;
+ }
+ }
+
+ public async Task GetDataAsync(string url, CancellationToken cancellationToken)
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Get, url);
+
+ // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
+ return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
+ }
+
+ public async Task SendCommandAsync(
+ string baseUrl,
+ DeviceService service,
+ string command,
+ string postData,
+ string? header = null,
+ CancellationToken cancellationToken = default)
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Post, NormalizeServiceUrl(baseUrl, service.ControlUrl))
+ {
+ Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml)
+ };
+
+ request.Headers.TryAddWithoutValidation(
+ "SOAPACTION",
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "\"{0}#{1}\"",
+ service.ServiceType,
+ command));
+ request.Headers.Pragma.ParseAdd("no-cache");
+
+ if (!string.IsNullOrEmpty(header))
+ {
+ request.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header);
+ }
+
+ // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
+ return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs
deleted file mode 100644
index cade7b4c2c..0000000000
--- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs
+++ /dev/null
@@ -1,141 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.Net.Http;
-using System.Net.Mime;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Xml.Linq;
-using Emby.Dlna.Common;
-using MediaBrowser.Common.Net;
-
-namespace Emby.Dlna.PlayTo
-{
- public class SsdpHttpClient
- {
- private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
- private const string FriendlyName = "Jellyfin";
-
- private readonly IHttpClientFactory _httpClientFactory;
-
- public SsdpHttpClient(IHttpClientFactory httpClientFactory)
- {
- _httpClientFactory = httpClientFactory;
- }
-
- public async Task SendCommandAsync(
- string baseUrl,
- DeviceService service,
- string command,
- string postData,
- string header = null,
- CancellationToken cancellationToken = default)
- {
- var url = NormalizeServiceUrl(baseUrl, service.ControlUrl);
- using var response = await PostSoapDataAsync(
- url,
- $"\"{service.ServiceType}#{command}\"",
- postData,
- header,
- cancellationToken)
- .ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
-
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- return await XDocument.LoadAsync(
- stream,
- LoadOptions.None,
- cancellationToken).ConfigureAwait(false);
- }
-
- private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
- {
- // If it's already a complete url, don't stick anything onto the front of it
- if (serviceUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
- {
- return serviceUrl;
- }
-
- if (!serviceUrl.StartsWith('/'))
- {
- serviceUrl = "/" + serviceUrl;
- }
-
- return baseUrl + serviceUrl;
- }
-
- public async Task SubscribeAsync(
- string url,
- string ip,
- int port,
- string localIp,
- int eventport,
- int timeOut = 3600)
- {
- using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
- options.Headers.UserAgent.ParseAdd(USERAGENT);
- options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(CultureInfo.InvariantCulture));
- options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">");
- options.Headers.TryAddWithoutValidation("NT", "upnp:event");
- options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(CultureInfo.InvariantCulture));
-
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
- .SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
- .ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
- }
-
- public async Task GetDataAsync(string url, CancellationToken cancellationToken)
- {
- using var options = new HttpRequestMessage(HttpMethod.Get, url);
- options.Headers.UserAgent.ParseAdd(USERAGENT);
- options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- try
- {
- return await XDocument.LoadAsync(
- stream,
- LoadOptions.None,
- cancellationToken).ConfigureAwait(false);
- }
- catch
- {
- return null;
- }
- }
-
- private async Task PostSoapDataAsync(
- string url,
- string soapAction,
- string postData,
- string header,
- CancellationToken cancellationToken)
- {
- if (soapAction[0] != '\"')
- {
- soapAction = $"\"{soapAction}\"";
- }
-
- using var options = new HttpRequestMessage(HttpMethod.Post, url);
- options.Headers.UserAgent.ParseAdd(USERAGENT);
- options.Headers.TryAddWithoutValidation("SOAPACTION", soapAction);
- options.Headers.TryAddWithoutValidation("Pragma", "no-cache");
- options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
-
- if (!string.IsNullOrEmpty(header))
- {
- options.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header);
- }
-
- options.Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml);
-
- return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- }
- }
-}
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index cfbc1eef87..ca002b9816 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -36,7 +36,7 @@
Jellyfin Contributors
Jellyfin.Naming
- 10.8.0
+ 10.9.0
https://github.com/jellyfin/jellyfin
GPL-3.0-only
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 7d82b2cacf..e1688dc6ec 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 32289625fe..91a16c199d 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -83,6 +83,7 @@ using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo;
+using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
@@ -111,7 +112,7 @@ namespace Emby.Server.Implementations
///
/// Class CompositionRoot.
///
- public abstract class ApplicationHost : IServerApplicationHost, IDisposable
+ public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
{
///
/// The environment variable prefixes to log at server startup.
@@ -634,7 +635,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
@@ -1232,5 +1234,49 @@ namespace Emby.Server.Implementations
_disposed = true;
}
+
+ public async ValueTask DisposeAsync()
+ {
+ await DisposeAsyncCore().ConfigureAwait(false);
+ Dispose(false);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Used to perform asynchronous cleanup of managed resources or for cascading calls to .
+ ///
+ /// A ValueTask.
+ protected virtual async ValueTask DisposeAsyncCore()
+ {
+ var type = GetType();
+
+ Logger.LogInformation("Disposing {Type}", type.Name);
+
+ foreach (var (part, _) in _disposableParts)
+ {
+ var partType = part.GetType();
+ if (partType == type)
+ {
+ continue;
+ }
+
+ Logger.LogInformation("Disposing {Type}", partType.Name);
+
+ try
+ {
+ part.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error disposing {Type}", partType.Name);
+ }
+ }
+
+ // used for closing websockets
+ foreach (var session in _sessionManager.Sessions)
+ {
+ await session.DisposeAsync().ConfigureAwait(false);
+ }
+ }
}
}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 4361440d75..1b176e60d7 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -4934,6 +4934,7 @@ SELECT key FROM UserDatas WHERE isFavorite=@IsFavorite AND userId=@UserId)
AND Type = @InternalPersonType)");
statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
statement?.TryBind("@InternalPersonType", typeof(Person).FullName);
+ statement?.TryBind("@UserId", query.User.InternalId);
}
if (!query.ItemId.Equals(default))
@@ -4988,11 +4989,6 @@ AND Type = @InternalPersonType)");
statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
}
- if (query.User != null)
- {
- statement?.TryBind("@UserId", query.User.InternalId);
- }
-
return whereClauses;
}
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index cd24cd8729..24395a1933 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -29,10 +29,10 @@
-
+
-
+
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index b87f1bc226..818ccbb1b8 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.HttpServer
///
/// Class WebSocketConnection.
///
- public class WebSocketConnection : IWebSocketConnection, IDisposable
+ public class WebSocketConnection : IWebSocketConnection
{
///
/// The logger.
@@ -36,6 +36,8 @@ namespace Emby.Server.Implementations.HttpServer
///
private readonly WebSocket _socket;
+ private bool _disposed = false;
+
///
/// Initializes a new instance of the class.
///
@@ -244,10 +246,39 @@ namespace Emby.Server.Implementations.HttpServer
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool dispose)
{
+ if (_disposed)
+ {
+ return;
+ }
+
if (dispose)
{
_socket.Dispose();
}
+
+ _disposed = true;
+ }
+
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ await DisposeAsyncCore().ConfigureAwait(false);
+ Dispose(false);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Used to perform asynchronous cleanup of managed resources or for cascading calls to .
+ ///
+ /// A ValueTask.
+ protected virtual async ValueTask DisposeAsyncCore()
+ {
+ if (_socket.State == WebSocketState.Open)
+ {
+ await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "System Shutdown", CancellationToken.None).ConfigureAwait(false);
+ }
+
+ _socket.Dispose();
}
}
}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index c54945c93b..6796845525 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -2453,6 +2453,12 @@ namespace Emby.Server.Implementations.Library
return RootFolder;
}
+ ///
+ public void QueueLibraryScan()
+ {
+ _taskManager.QueueScheduledTask();
+ }
+
///
public int? GetSeasonNumberFromPath(string path)
=> SeasonPathParser.Parse(path, true, true).SeasonNumber;
@@ -2523,7 +2529,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error reading the episode informations with ffprobe. Episode: {EpisodeInfo}", episodeInfo.Path);
+ _logger.LogError(ex, "Error reading the episode information with ffprobe. Episode: {EpisodeInfo}", episodeInfo.Path);
}
var changed = false;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index fe4ccd6acb..b2f388a667 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -387,7 +387,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (!string.IsNullOrEmpty(item.Path))
{
- // check for imdb id - we use full media path, as we can assume, that this will match in any use case (wither id in parent dir or in file name)
+ // check for imdb id - we use full media path, as we can assume, that this will match in any use case (either id in parent dir or in file name)
var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid");
if (!string.IsNullOrWhiteSpace(imdbid))
@@ -464,7 +464,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
var result = ResolveVideos(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
new MultiItemResolverResult();
- if (result.Items.Count == 1)
+ var isPhotosCollection = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase);
+ if (!isPhotosCollection && result.Items.Count == 1)
{
var videoPath = result.Items[0].Path;
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 2a468e14dc..bcb42e1626 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -196,7 +196,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
IsInfiniteStream = true,
IsRemote = isRemote,
- IgnoreDts = true,
+ IgnoreDts = info.IgnoreDts,
SupportsDirectPlay = supportsDirectPlay,
SupportsDirectStream = supportsDirectStream,
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index 708ff52d79..be06356a4f 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -199,7 +199,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (string.IsNullOrWhiteSpace(numberString))
{
// Using this as a fallback now as this leads to Problems with channels like "5 USA"
- // where 5 isn't ment to be the channel number
+ // where 5 isn't meant to be the channel number
// Check for channel number with the format from SatIp
// #EXTINF:0,84. VOX Schweiz
// #EXTINF:0,84.0 - VOX Schweiz
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 3e2dd5be6e..9dc2fe7996 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -92,22 +92,22 @@
"ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط",
"ValueSpecialEpisodeName": "حلقه خاصه - {0}",
"VersionNumber": "النسخة {0}",
- "TaskCleanCacheDescription": "يحذف ملفات ذاكرة التخزين المؤقت التي لم يعد النظام بحاجة إليها.",
- "TaskCleanCache": "احذف مجلد ذاكرة التخزين المؤقت",
+ "TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.",
+ "TaskCleanCache": "احذف ما بمجلد الملفات المؤقتة",
"TasksChannelsCategory": "قنوات الإنترنت",
"TasksLibraryCategory": "مكتبة",
"TasksMaintenanceCategory": "صيانة",
- "TaskRefreshLibraryDescription": "يقوم بفصح مكتبة الوسائط الخاصة بك بحثًا عن ملفات جديدة وتحديث البيانات الوصفية.",
+ "TaskRefreshLibraryDescription": "يفصح مكتبة الوسائط الخاصة بك بحثًا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.",
"TaskRefreshLibrary": "افحص مكتبة الوسائط",
- "TaskRefreshChapterImagesDescription": "يقوم بانشاء صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.",
+ "TaskRefreshChapterImagesDescription": "يُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.",
"TaskRefreshChapterImages": "استخراج صور الفصل",
"TasksApplicationCategory": "تطبيق",
- "TaskDownloadMissingSubtitlesDescription": "يقوم بالبحث في الإنترنت على الترجمات المفقودة إستنادا على البيانات الوصفية.",
- "TaskDownloadMissingSubtitles": "تحميل الترجمات المفقودة",
- "TaskRefreshChannelsDescription": "يقوم بتحديث معلومات قنوات الإنترنت.",
+ "TaskDownloadMissingSubtitlesDescription": "يبحث في الإنترنت على الترجمات الناقصة استنادا على البيانات الوصفية.",
+ "TaskDownloadMissingSubtitles": "تحميل الترجمات الناقصة",
+ "TaskRefreshChannelsDescription": "يحدث معلومات قنوات الإنترنت.",
"TaskRefreshChannels": "إعادة تحديث القنوات",
- "TaskCleanTranscodeDescription": "يقوم بحذف ملفات الترميز الأقدم من يوم واحد.",
- "TaskCleanTranscode": "حذف سجلات الترميز",
+ "TaskCleanTranscodeDescription": "يحذف ملفات الترميز الأقدم من يوم واحد.",
+ "TaskCleanTranscode": "حذف ما بمجلد الترميز",
"TaskUpdatePluginsDescription": "تحميل وتثبيت الإضافات التي تم تفعيل التحديث التلقائي لها.",
"TaskUpdatePlugins": "تحديث الإضافات",
"TaskRefreshPeopleDescription": "يقوم بتحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.",
@@ -116,12 +116,12 @@
"TaskCleanLogs": "حذف مسار السجل",
"TaskCleanActivityLogDescription": "يحذف سجل الأنشطة الأقدم من الوقت الذي تم تحديده.",
"TaskCleanActivityLog": "حذف سجل الأنشطة",
- "Default": "إفتراضي",
+ "Default": "افتراضي",
"Undefined": "غير معرف",
"Forced": "ملحقة",
"TaskOptimizeDatabaseDescription": "يضغط قاعدة البيانات ويقتطع المساحة الحرة. تشغيل هذه المهمة بعد فحص المكتبة أو إجراء تغييرات أخرى تتضمن تعديلات في قاعدة البيانات قد تؤدي إلى تحسين الأداء.",
"TaskOptimizeDatabase": "تحسين قاعدة البيانات",
- "TaskKeyframeExtractorDescription": "يقوم باستخراج الإطارات الرئيسيه من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. هذه المهمه قد تستمر لاوقات طويلة.",
+ "TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.",
"TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
"External": "خارجي"
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 722b81c8ac..9c278db4d2 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -121,7 +121,7 @@
"Default": "Standard",
"TaskOptimizeDatabaseDescription": "Komprimiert die Datenbank und trimmt den freien Speicherplatz. Die Ausführung dieser Aufgabe nach dem Scannen der Bibliothek oder nach anderen Änderungen, die Datenbankänderungen implizieren, kann die Leistung verbessern.",
"TaskOptimizeDatabase": "Datenbank optimieren",
- "TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Diese Aufgabe kann sehr lange dauern.",
+ "TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.",
"TaskKeyframeExtractor": "Keyframe Extraktor",
"External": "Extern"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 33e80f7ac9..648c878e9c 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -5,7 +5,7 @@
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
"Books": "Livres",
- "CameraImageUploadedFrom": "Une photo a été chargée depuis {0}",
+ "CameraImageUploadedFrom": "Une photo a été téléversée depuis {0}",
"Channels": "Chaînes",
"ChapterNameValue": "Chapitre {0}",
"Collections": "Collections",
@@ -42,13 +42,13 @@
"MusicVideos": "Clips musicaux",
"NameInstallFailed": "{0} échec de l'installation",
"NameSeasonNumber": "Saison {0}",
- "NameSeasonUnknown": "Saison Inconnue",
+ "NameSeasonUnknown": "Saison inconnue",
"NewVersionIsAvailable": "Une nouvelle version de Jellyfin Serveur est disponible au téléchargement.",
"NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible",
"NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée",
"NotificationOptionAudioPlayback": "Lecture audio démarrée",
"NotificationOptionAudioPlaybackStopped": "Lecture audio arrêtée",
- "NotificationOptionCameraImageUploaded": "L'image de l'appareil photo a été transférée",
+ "NotificationOptionCameraImageUploaded": "L'image de l'appareil photo a été téléversée",
"NotificationOptionInstallationFailed": "Échec de l'installation",
"NotificationOptionNewLibraryContent": "Nouveau contenu ajouté",
"NotificationOptionPluginError": "Erreur d'extension",
@@ -92,34 +92,34 @@
"ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
"ValueSpecialEpisodeName": "Spécial - {0}",
"VersionNumber": "Version {0}",
- "TasksChannelsCategory": "Chaines en ligne",
- "TaskDownloadMissingSubtitlesDescription": "Recherche les sous-titres manquants sur internet en se basant sur la configuration des métadonnées.",
+ "TasksChannelsCategory": "Chaînes en ligne",
+ "TaskDownloadMissingSubtitlesDescription": "Recherche les sous-titres manquants sur Internet en se basant sur la configuration des métadonnées.",
"TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquants",
- "TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines en ligne.",
- "TaskRefreshChannels": "Rafraîchir les chaines",
+ "TaskRefreshChannelsDescription": "Actualise les informations des chaînes en ligne.",
+ "TaskRefreshChannels": "Actualiser les chaînes",
"TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.",
- "TaskCleanTranscode": "Nettoyer les dossier des transcodages",
+ "TaskCleanTranscode": "Nettoyer le dossier des transcodages",
"TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurées pour être mises à jour automatiquement.",
"TaskUpdatePlugins": "Mettre à jour les extensions",
- "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.",
- "TaskRefreshPeople": "Rafraîchir les acteurs",
+ "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre médiathèque.",
+ "TaskRefreshPeople": "Actualiser les acteurs",
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
- "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
+ "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et actualise les métadonnées.",
"TaskRefreshLibrary": "Scanner la médiathèque",
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
"TaskCleanCache": "Vider le répertoire cache",
"TasksApplicationCategory": "Application",
- "TasksLibraryCategory": "Bibliothèque",
+ "TasksLibraryCategory": "Médiathèque",
"TasksMaintenanceCategory": "Maintenance",
"TaskCleanActivityLogDescription": "Supprime les entrées du journal d'activité antérieures à l'âge configuré.",
"TaskCleanActivityLog": "Nettoyer le journal d'activité",
"Undefined": "Non défini",
"Forced": "Forcé",
"Default": "Par défaut",
- "TaskOptimizeDatabaseDescription": "Réduit les espaces vides/inutiles et compacte la base de données. Utiliser cette fonction après une mise à jour de la bibliothèque ou toute autre modification de la base de données peut améliorer les performances du serveur.",
+ "TaskOptimizeDatabaseDescription": "Réduit les espaces vides ou inutiles et compacte la base de données. Utiliser cette fonction après une mise à jour de la médiathèque ou toute autre modification de la base de données peut améliorer les performances du serveur.",
"TaskOptimizeDatabase": "Optimiser la base de données",
"TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.",
"TaskKeyframeExtractor": "Extracteur d'image clé",
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index 50d019f906..186ec44d24 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -120,5 +120,8 @@
"Forced": "강제하기",
"Default": "기본 설정",
"TaskOptimizeDatabaseDescription": "데이터베이스를 압축하고 사용 가능한 공간을 늘립니다. 라이브러리를 검색한 후 이 작업을 실행하거나 데이터베이스 수정같은 비슷한 작업을 수행하면 성능이 향상될 수 있습니다.",
- "TaskOptimizeDatabase": "데이터베이스 최적화"
+ "TaskOptimizeDatabase": "데이터베이스 최적화",
+ "TaskKeyframeExtractorDescription": "비디오 파일에서 키프레임을 추출하여 더 정확한 HLS 재생 목록을 만듭니다. 이 작업은 오랫동안 진행될 수 있습니다.",
+ "TaskKeyframeExtractor": "키프레임 추출",
+ "External": "외부"
}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index cb98d8e413..232b3ec934 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -39,7 +39,7 @@
"MixedContent": "Mixed content",
"Movies": "Filmai",
"Music": "Muzika",
- "MusicVideos": "Muzikiniai klipai",
+ "MusicVideos": "Muzikiniai vaizdo įrašai",
"NameInstallFailed": "{0} diegimo klaida",
"NameSeasonNumber": "Sezonas {0}",
"NameSeasonUnknown": "Sezonas neatpažintas",
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index deb28970c0..3d54a5a950 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -61,7 +61,7 @@
"NotificationOptionVideoPlayback": "Ulangmain video bermula",
"NotificationOptionVideoPlaybackStopped": "Ulangmain video dihentikan",
"Photos": "Gambar-gambar",
- "Playlists": "Senarai main",
+ "Playlists": "Senarai ulangmain",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} telah dipasang",
"PluginUninstalledWithName": "{0} telah dinyahpasang",
diff --git a/Emby.Server.Implementations/Localization/Core/my.json b/Emby.Server.Implementations/Localization/Core/my.json
index 2642373fa9..198f7540c8 100644
--- a/Emby.Server.Implementations/Localization/Core/my.json
+++ b/Emby.Server.Implementations/Localization/Core/my.json
@@ -6,97 +6,97 @@
"Artists": "အနုပညာရှင်များ",
"Albums": "သီချင်းအခွေများ",
"TaskOptimizeDatabaseDescription": "ဒေတာဘေ့စ်ကို ကျစ်လစ်စေပြီး နေရာလွတ်များကို ဖြတ်တောက်ပေးသည်။ စာကြည့်တိုက်ကို စကင်န်ဖတ်ပြီးနောက် ဤလုပ်ငန်းကို လုပ်ဆောင်ခြင်း သို့မဟုတ် ဒေတာဘေ့စ်မွမ်းမံမှုများ စွမ်းဆောင်ရည်ကို မြှင့်တင်ပေးနိုင်သည်ဟု ရည်ညွှန်းသော အခြားပြောင်းလဲမှုများကို လုပ်ဆောင်ခြင်း။.",
- "TaskOptimizeDatabase": "ဒေတာဘေ့စ်ကို အကောင်းဆုံးဖြစ်အောင်လုပ်ပါ။",
+ "TaskOptimizeDatabase": "ဒေတာဘေ့စ်ကို အကောင်းဆုံးဖြစ်အောင်လုပ်ပါ",
"TaskDownloadMissingSubtitlesDescription": "မက်တာဒေတာ ဖွဲ့စည်းမှုပုံစံအပေါ် အခြေခံ၍ ပျောက်ဆုံးနေသော စာတန်းထိုးများအတွက် အင်တာနက်ကို ရှာဖွေသည်။",
- "TaskDownloadMissingSubtitles": "ပျောက်ဆုံးနေသော စာတန်းထိုးများကို ဒေါင်းလုဒ်လုပ်ပါ။",
+ "TaskDownloadMissingSubtitles": "ပျောက်ဆုံးနေသော စာတန်းထိုးများကို ဒေါင်းလုဒ်လုပ်ပါ",
"TaskRefreshChannelsDescription": "အင်တာနက်ချန်နယ်အချက်အလက်ကို ပြန်လည်စတင်သည်။",
- "TaskRefreshChannels": "ချန်နယ်များကို ပြန်လည်စတင်ပါ။",
+ "TaskRefreshChannels": "ချန်နယ်များကို ပြန်လည်စတင်ပါ",
"TaskCleanTranscodeDescription": "သက်တမ်း တစ်ရက်ထက်ပိုသော အသွင်ပြောင်းကုဒ်ဖိုင်များကို ဖျက်ပါ။",
- "TaskCleanTranscode": "Transcode လမ်းညွှန်ကို သန့်ရှင်းပါ။",
+ "TaskCleanTranscode": "Transcode လမ်းညွှန်ကို သန့်ရှင်းပါ",
"TaskUpdatePluginsDescription": "အလိုအလျောက် အပ်ဒိတ်လုပ်ရန် စီစဉ်ထားသော ပလပ်အင်များအတွက် အပ်ဒိတ်များကို ဒေါင်းလုဒ်လုပ်ပြီး ထည့်သွင်းပါ။",
- "TaskUpdatePlugins": "ပလပ်အင်များကို အပ်ဒိတ်လုပ်ပါ။",
+ "TaskUpdatePlugins": "ပလပ်အင်များကို အပ်ဒိတ်လုပ်ပါ",
"TaskRefreshPeopleDescription": "သင့်မီဒီယာစာကြည့်တိုက်ရှိ သရုပ်ဆောင်များနှင့် ဒါရိုက်တာများအတွက် မက်တာဒေတာကို အပ်ဒိတ်လုပ်ပါ။",
- "TaskRefreshPeople": "လူများကို ပြန်လည်ဆန်းသစ်ပါ။",
+ "TaskRefreshPeople": "လူများကို ပြန်လည်ဆန်းသစ်ပါ",
"TaskCleanLogsDescription": "{0} ရက်ထက်ပိုသော မှတ်တမ်းဖိုင်များကို ဖျက်သည်။",
- "TaskCleanLogs": "မှတ်တမ်းလမ်းညွှန်ကို သန့်ရှင်းပါ။",
+ "TaskCleanLogs": "မှတ်တမ်းလမ်းညွှန်ကို သန့်ရှင်းပါ",
"TaskRefreshLibraryDescription": "သင့်မီဒီယာဒစ်ဂျစ်တိုက်ကို ဖိုင်အသစ်များရှိမရှိ စကင်န်ဖတ်ပြီး ဖိုင်ရဲ့အကြောင်းအရာများ ကို ပြန်ပြုပြင်မွမ်းမံပါ။",
- "TaskRefreshLibrary": "မီဒီယာစာကြည့်တိုက်ကို စကင်န်ဖတ်ပါ။",
+ "TaskRefreshLibrary": "မီဒီယာစာကြည့်တိုက်ကို စကင်န်ဖတ်ပါ",
"TaskRefreshChapterImagesDescription": "အခန်းများပါရှိသော ဗီဒီယိုများအတွက် ပုံသေးများကို ဖန်တီးပါ။",
- "TaskRefreshChapterImages": "အခန်းတစ်ခုစီ ပုံများကို ထုတ်ယူပါ။",
+ "TaskRefreshChapterImages": "အခန်းတစ်ခုစီ ပုံများကို ထုတ်ယူပါ",
"TaskCleanCacheDescription": "စနစ်မှ မလိုအပ်တော့သော ကက်ရှ်ဖိုင်များကို ဖျက်ပါ။.",
- "TaskCleanCache": "Cache Directory ကို ရှင်းပါ။",
+ "TaskCleanCache": "Cache Directory ကို ရှင်းပါ",
"TaskCleanActivityLogDescription": "စီစဉ်သတ်မှတ်ထားသော အသက်ထက် ပိုကြီးသော လုပ်ဆောင်ချက်မှတ်တမ်းများကို ဖျက်ပါ။",
- "TaskCleanActivityLog": "လုပ်ဆောင်ချက်မှတ်တမ်းကို ရှင်းလင်းပါ။",
+ "TaskCleanActivityLog": "လုပ်ဆောင်ချက်မှတ်တမ်းကို ရှင်းလင်းပါ",
"TasksChannelsCategory": "အင်တာနက် ချန်နယ်လိုင်းများ",
"TasksApplicationCategory": "အပလီကေးရှင်း",
"TasksLibraryCategory": "မီဒီယာတိုက်",
"TasksMaintenanceCategory": "ပြုပြင် ထိန်းသိမ်းခြင်း",
"VersionNumber": "ဗားရှင်း {0}",
"ValueSpecialEpisodeName": "အထူး- {0}",
- "ValueHasBeenAddedToLibrary": "{0} ကို သင့်မီဒီယာဒစ်ဂျစ်တိုက်သို့ ပေါင်းထည့်လိုက်ပါပြီ။",
+ "ValueHasBeenAddedToLibrary": "{0} ကို သင့်မီဒီယာဒစ်ဂျစ်တိုက်သို့ ပေါင်းထည့်လိုက်ပါပြီ",
"UserStoppedPlayingItemWithValues": "{0} သည် {1} ကို {2} တွင် ဖွင့်ပြီးပါပြီ",
"UserStartedPlayingItemWithValues": "{0} သည် {1} ကို {2} တွင် ပြသနေသည်",
"UserPolicyUpdatedWithName": "{0} အတွက် အသုံးပြုသူမူဝါဒကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
"UserPasswordChangedWithName": "အသုံးပြုသူ {0} အတွက် စကားဝှက်ကို ပြောင်းထားသည်",
"UserOnlineFromDevice": "{0} သည် {1} မှ အွန်လိုင်းဖြစ်သည်",
"UserOfflineFromDevice": "{0} သည် {1} မှ ချိတ်ဆက်မှုပြတ်တောက်သွားသည်",
- "UserLockedOutWithName": "အသုံးပြုသူ {0} အား လော့ခ်ချထားသည်။",
+ "UserLockedOutWithName": "အသုံးပြုသူ {0} အား လော့ခ်ချထားသည်",
"UserDownloadingItemWithValues": "{0} သည် {1} ကို ဒေါင်းလုဒ်လုပ်နေသည်",
- "UserDeletedWithName": "အသုံးပြုသူ {0} ကို ဖျက်လိုက်ပါပြီ။",
- "UserCreatedWithName": "အသုံးပြုသူ {0} ကို ဖန်တီးပြီးပါပြီ။",
+ "UserDeletedWithName": "အသုံးပြုသူ {0} ကို ဖျက်လိုက်ပါပြီ",
+ "UserCreatedWithName": "အသုံးပြုသူ {0} ကို ဖန်တီးပြီးပါပြီ",
"User": "အသုံးပြုသူ",
"Undefined": "သတ်မှတ်မထားသော",
"TvShows": "တီဗီ ဇာတ်လမ်းတွဲများ",
"System": "စနစ်",
"Sync": "ထပ်တူကျသည်။",
- "SubtitleDownloadFailureFromForItem": "{1} အတွက် {0} မှ စာတန်းထိုးများ ဒေါင်းလုဒ်လုပ်ခြင်း မအောင်မြင်ပါ။",
+ "SubtitleDownloadFailureFromForItem": "{1} အတွက် {0} မှ စာတန်းထိုးများ ဒေါင်းလုဒ်လုပ်ခြင်း မအောင်မြင်ပါ",
"StartupEmbyServerIsLoading": "Jellyfin ဆာဗာကို အသင့်ပြင်နေပါသည်။ ခဏနေ ထပ်စမ်းကြည့်ပါ။",
"Songs": "သီချင်းများ",
"Shows": "ဇာတ်လမ်းတွဲများ",
- "ServerNameNeedsToBeRestarted": "{0} ကို ပြန်လည်စတင်ရန် လိုအပ်သည်။",
- "ScheduledTaskStartedWithName": "{0} စတင်ခဲ့သည်။",
- "ScheduledTaskFailedWithName": "{0} မအောင်မြင်ပါ။",
+ "ServerNameNeedsToBeRestarted": "{0} ကို ပြန်လည်စတင်ရန် လိုအပ်သည်",
+ "ScheduledTaskStartedWithName": "{0} စတင်ခဲ့သည်",
+ "ScheduledTaskFailedWithName": "{0} မအောင်မြင်ပါ",
"ProviderValue": "ဝန်ဆောင်မှုပေးသူ- {0}",
- "PluginUpdatedWithName": "ပလပ်ခ်အင် {0} ကို အပ်ဒိတ်လုပ်ထားသည်။",
- "PluginUninstalledWithName": "ပလပ်ခ်အင် {0} ကို ဖြုတ်လိုက်ပါပြီ။",
- "PluginInstalledWithName": "ပလပ်ခ်အင် {0} ကို ထည့်သွင်းခဲ့သည်။",
+ "PluginUpdatedWithName": "ပလပ်ခ်အင် {0} ကို အပ်ဒိတ်လုပ်ထားသည်",
+ "PluginUninstalledWithName": "ပလပ်ခ်အင် {0} ကို ဖြုတ်လိုက်ပါပြီ",
+ "PluginInstalledWithName": "ပလပ်ခ်အင် {0} ကို ထည့်သွင်းခဲ့သည်",
"Plugin": "ပလပ်အင်",
"Playlists": "အစီအစဉ်များ",
"Photos": "ဓာတ်ပုံများ",
- "NotificationOptionVideoPlaybackStopped": "ဗီဒီယိုဖွင့်ခြင်း ရပ်သွားသည်။",
- "NotificationOptionVideoPlayback": "ဗီဒီယိုဖွင့်ခြင်း စတင်ပါပြီ။",
- "NotificationOptionUserLockedOut": "အသုံးပြုသူ ဝင်ရန် တားမြစ်ခံရသည်။",
+ "NotificationOptionVideoPlaybackStopped": "ဗီဒီယိုဖွင့်ခြင်း ရပ်သွားသည်",
+ "NotificationOptionVideoPlayback": "ဗီဒီယိုဖွင့်ခြင်း စတင်ပါပြီ",
+ "NotificationOptionUserLockedOut": "အသုံးပြုသူ ဝင်ရန် တားမြစ်ခံရသည်",
"NotificationOptionTaskFailed": "စီစဉ်ထားသော အလုပ်ပျက်ကွက်",
- "NotificationOptionServerRestartRequired": "ဆာဗာ ပြန်လည်စတင်ရန် လိုအပ်သည်။",
- "NotificationOptionPluginUpdateInstalled": "ပလပ်အင် အပ်ဒိတ် ထည့်သွင်းပြီးပါပြီ။",
- "NotificationOptionPluginUninstalled": "ပလပ်အင်ကို ဖြုတ်လိုက်ပါပြီ။",
- "NotificationOptionPluginInstalled": "ပလပ်အင် ထည့်သွင်းထားသည်။",
- "NotificationOptionPluginError": "ပလပ်အင် ချို့ယွင်းခြင်း။",
- "NotificationOptionNewLibraryContent": "အသစ်များ ထပ်ထည့်ထားပါတယ်။",
- "NotificationOptionInstallationFailed": "ထည့်သွင်းမှု မအောင်မြင်ပါ။",
- "NotificationOptionCameraImageUploaded": "ကင်မရာမှ ဓာတ်ပုံ အပ်လုဒ် ပြီးပါပြီ။",
- "NotificationOptionAudioPlaybackStopped": "အသံဖိုင်ဖွင့်ခြင်း ရပ်သွားသည်။",
- "NotificationOptionAudioPlayback": "အသံဖွင့်ခြင်း စတင်ပါပြီ။",
- "NotificationOptionApplicationUpdateInstalled": "အပလီကေးရှင်း အပ်ဒိတ်ကို ထည့်သွင်းထားသည်။",
- "NotificationOptionApplicationUpdateAvailable": "အပလီကေးရှင်း အပ်ဒိတ် ရနိုင်ပါပြီ။",
+ "NotificationOptionServerRestartRequired": "ဆာဗာ ပြန်လည်စတင်ရန် လိုအပ်သည်",
+ "NotificationOptionPluginUpdateInstalled": "ပလပ်အင် အပ်ဒိတ် ထည့်သွင်းပြီးပါပြီ",
+ "NotificationOptionPluginUninstalled": "ပလပ်အင်ကို ဖြုတ်လိုက်ပါပြီ",
+ "NotificationOptionPluginInstalled": "ပလပ်အင် ထည့်သွင်းထားသည်",
+ "NotificationOptionPluginError": "ပလပ်အင် ချို့ယွင်းခြင်း",
+ "NotificationOptionNewLibraryContent": "အသစ်များ ထပ်ထည့်ထားပါတယ်",
+ "NotificationOptionInstallationFailed": "ထည့်သွင်းမှု မအောင်မြင်ပါ",
+ "NotificationOptionCameraImageUploaded": "ကင်မရာမှ ဓာတ်ပုံ အပ်လုဒ် ပြီးပါပြီ",
+ "NotificationOptionAudioPlaybackStopped": "အသံဖိုင်ဖွင့်ခြင်း ရပ်သွားသည်",
+ "NotificationOptionAudioPlayback": "အသံဖွင့်ခြင်း စတင်ပါပြီ",
+ "NotificationOptionApplicationUpdateInstalled": "အပလီကေးရှင်း အပ်ဒိတ်ကို ထည့်သွင်းထားသည်",
+ "NotificationOptionApplicationUpdateAvailable": "အပလီကေးရှင်း အပ်ဒိတ် ရနိုင်ပါပြီ",
"NewVersionIsAvailable": "Jellyfin Server ၏ ဗားရှင်းအသစ်ကို ဒေါင်းလုဒ်လုပ်နိုင်ပါပြီ။",
"NameSeasonUnknown": "ဇာတ်လမ်းတွဲ အပိုင်းမသိ",
"NameSeasonNumber": "ဇာတ်လမ်းတွဲ အပိုင်း {0}",
- "NameInstallFailed": "{0} ထည့်သွင်းမှု မအောင်မြင်ပါ။",
+ "NameInstallFailed": "{0} ထည့်သွင်းမှု မအောင်မြင်ပါ",
"MusicVideos": "ဂီတဗီဒီယိုများ",
"Music": "တေးဂီတ",
"Movies": "ရုပ်ရှင်များ",
"MixedContent": "ရောနှောပါဝင်မှု",
- "MessageServerConfigurationUpdated": "ဆာဗာဖွဲ့စည်းပုံကို အပ်ဒိတ်လုပ်ပြီးပါပြီ။",
- "MessageNamedServerConfigurationUpdatedWithValue": "ဆာဗာဖွဲ့စည်းပုံကဏ္ဍ {0} ကို အပ်ဒိတ်လုပ်ပြီးပါပြီ။",
+ "MessageServerConfigurationUpdated": "ဆာဗာဖွဲ့စည်းပုံကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
+ "MessageNamedServerConfigurationUpdatedWithValue": "ဆာဗာဖွဲ့စည်းပုံကဏ္ဍ {0} ကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
"MessageApplicationUpdatedTo": "Jellyfin ဆာဗာကို {0} သို့ အပ်ဒိတ်လုပ်ထားသည်",
- "MessageApplicationUpdated": "Jellyfin ဆာဗာကို အပ်ဒိတ်လုပ်ပြီးပါပြီ။",
+ "MessageApplicationUpdated": "Jellyfin ဆာဗာကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
"Latest": "နောက်ဆုံး",
"LabelRunningTimeValue": "ကြာချိန် - {0}",
"LabelIpAddressValue": "IP လိပ်စာ- {0}",
- "ItemRemovedWithName": "{0} ကို ဒစ်ဂျစ်တိုက်မှ ဖယ်ရှားခဲ့သည်။",
- "ItemAddedWithName": "{0} ကို စာကြည့်တိုက်သို့ ထည့်ထားသည်။",
- "Inherit": "ဆက်ခံ၍ လုပ်ဆောင်သည်။",
+ "ItemRemovedWithName": "{0} ကို ဒစ်ဂျစ်တိုက်မှ ဖယ်ရှားခဲ့သည်",
+ "ItemAddedWithName": "{0} ကို စာကြည့်တိုက်သို့ ထည့်ထားသည်",
+ "Inherit": "ဆက်ခံ၍ လုပ်ဆောင်သည်",
"HomeVideos": "ကိုယ်တိုင်ရိုက် ဗီဒီယိုများ",
"HeaderRecordingGroups": "အသံဖမ်းအဖွဲ့များ",
"HeaderNextUp": "နောက်ထပ်",
@@ -106,18 +106,18 @@
"HeaderFavoriteEpisodes": "အကြိုက်ဆုံး ဇာတ်လမ်းအပိုင်းများ",
"HeaderFavoriteArtists": "အကြိုက်ဆုံးအနုပညာရှင်များ",
"HeaderFavoriteAlbums": "အကြိုက်ဆုံး အယ်လ်ဘမ်များ",
- "HeaderContinueWatching": "ဆက်လက်ကြည့်ရှုပါ။",
+ "HeaderContinueWatching": "ဆက်လက်ကြည့်ရှုပါ",
"HeaderAlbumArtists": "အယ်လ်ဘမ်အနုပညာရှင်များ",
"Genres": "အမျိုးအစားများ",
"Forced": "အတင်းအကြပ်",
"Folders": "ဖိုလ်ဒါများ",
"Favorites": "အကြိုက်ဆုံးများ",
"FailedLoginAttemptWithUserName": "{0} မှ အကောင့်ဝင်ရန် မအောင်မြင်ပါ",
- "DeviceOnlineWithName": "{0} ကို ချိတ်ဆက်ထားသည်။",
- "DeviceOfflineWithName": "{0} နှင့် အဆက်ပြတ်သွားပါပြီ။",
+ "DeviceOnlineWithName": "{0} ကို ချိတ်ဆက်ထားသည်",
+ "DeviceOfflineWithName": "{0} နှင့် အဆက်ပြတ်သွားပါပြီ",
"ChapterNameValue": "အခန်း {0}",
- "CameraImageUploadedFrom": "ကင်မရာပုံအသစ်ကို {0} မှ ထည့်သွင်းလိုက်သည်။",
- "AuthenticationSucceededWithUserName": "{0} စစ်မှန်ကြောင်း အောင်မြင်စွာ အတည်ပြုပြီးပါပြီ။",
+ "CameraImageUploadedFrom": "ကင်မရာပုံအသစ်ကို {0} မှ ထည့်သွင်းလိုက်သည်",
+ "AuthenticationSucceededWithUserName": "{0} အောင်မြင်စွာ စစ်မှန်ကြောင်း အတည်ပြုပြီးပါပြီ",
"Application": "အပလီကေးရှင်း",
"AppDeviceValues": "အက်ပ်- {0}၊ စက်- {1}",
"External": "ပြင်ပ"
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index aff70ac69a..ea9a82d2bf 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -31,7 +31,7 @@
"ItemRemovedWithName": "{0} - изъято из медиатеки",
"LabelIpAddressValue": "IP-адрес: {0}",
"LabelRunningTimeValue": "Длительность: {0}",
- "Latest": "Крайнее",
+ "Latest": "Новое",
"MessageApplicationUpdated": "Jellyfin Server был обновлён",
"MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index 30b24e9f0d..b2d7ce11df 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -122,5 +122,6 @@
"TaskOptimizeDatabaseDescription": "Stisne bazo podatkov in uredi prazen prostor. Zagon tega opravila po iskanju predstavnosti ali drugih spremembah ki vplivajo na bazo podatkov lahko izboljša hitrost delovanja.",
"TaskOptimizeDatabase": "Optimiziraj bazo podatkov",
"TaskKeyframeExtractor": "Ekstraktor ključnih sličic",
- "External": "Zunanje"
+ "External": "Zunanji",
+ "TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index a41523bbd2..781e93926e 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -120,5 +120,7 @@
"Default": "Подразумевано",
"TaskOptimizeDatabase": "Оптимизуј датабазу",
"TaskOptimizeDatabaseDescription": "Сажима базу података и скраћује слободан простор. Покретање овог задатка након скенирања библиотеке или других промена које подразумевају измене базе података које могу побољшати перформансе.",
- "External": "Спољно"
+ "External": "Спољно",
+ "TaskKeyframeExtractorDescription": "Екстрактује кљулне сличице из видео датотека да би креирао више преицзну HLS плеј-листу. Овај задатак може да потраје дуже време.",
+ "TaskKeyframeExtractor": "Екстрактор кључних сличица"
}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index d25376297f..7f927e2709 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -1242,7 +1242,7 @@ namespace Emby.Server.Implementations.Session
if (item == null)
{
- _logger.LogError("A non-existant item Id {0} was passed into TranslateItemForPlayback", id);
+ _logger.LogError("A non-existent item Id {0} was passed into TranslateItemForPlayback", id);
return Array.Empty();
}
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index a085ee546d..fccf50f60f 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -37,7 +37,7 @@ namespace Emby.Server.Implementations.Session
private const float ForceKeepAliveFactor = 0.75f;
///
- /// Lock used for accesing the KeepAlive cancellation token.
+ /// Lock used for accessing the KeepAlive cancellation token.
///
private readonly object _keepAliveLock = new object();
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index d21b6a929c..1f3248f075 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session
{
- public sealed class WebSocketController : ISessionController, IDisposable
+ public sealed class WebSocketController : ISessionController, IAsyncDisposable, IDisposable
{
private readonly ILogger _logger;
private readonly ISessionManager _sessionManager;
@@ -99,6 +99,23 @@ namespace Emby.Server.Implementations.Session
foreach (var socket in _sockets)
{
socket.Closed -= OnConnectionClosed;
+ socket.Dispose();
+ }
+
+ _disposed = true;
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ foreach (var socket in _sockets)
+ {
+ socket.Closed -= OnConnectionClosed;
+ await socket.DisposeAsync().ConfigureAwait(false);
}
_disposed = true;
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 727b9d4b5c..d7ab9c021e 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -43,9 +43,9 @@ namespace Emby.Server.Implementations.TV
}
string presentationUniqueKey = null;
- if (!string.IsNullOrEmpty(query.SeriesId))
+ if (query.SeriesId.HasValue && !query.SeriesId.Value.Equals(default))
{
- if (_libraryManager.GetItemById(query.SeriesId) is Series series)
+ if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series)
{
presentationUniqueKey = GetUniqueSeriesKey(series);
}
@@ -93,9 +93,9 @@ namespace Emby.Server.Implementations.TV
string presentationUniqueKey = null;
int? limit = null;
- if (!string.IsNullOrEmpty(request.SeriesId))
+ if (request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default))
{
- if (_libraryManager.GetItemById(request.SeriesId) is Series series)
+ if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
{
presentationUniqueKey = GetUniqueSeriesKey(series);
limit = 1;
@@ -153,7 +153,7 @@ namespace Emby.Server.Implementations.TV
// If viewing all next up for all series, remove first episodes
// But if that returns empty, keep those first episodes (avoid completely empty view)
- var alwaysEnableFirstEpisode = !string.IsNullOrEmpty(request.SeriesId);
+ var alwaysEnableFirstEpisode = request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default);
var anyFound = false;
return allNextUp
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 54ac06276e..94f7a7b827 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -207,7 +207,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. The dlna device profile id to utilize.
/// The play session id.
/// The segment container.
- /// The segment lenght.
+ /// The segment length.
/// The minimum number of segments.
/// The media version id, if playing an alternate version.
/// The device id of the client requesting. Used to stop encoding processes when needed.
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 365e44e1a3..3ed80f6621 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -121,7 +121,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. The dlna device profile id to utilize.
/// The play session id.
/// The segment container.
- /// The segment lenght.
+ /// The segment length.
/// The minimum number of segments.
/// The media version id, if playing an alternate version.
/// The device id of the client requesting. Used to stop encoding processes when needed.
@@ -1790,7 +1790,8 @@ namespace Jellyfin.Api.Controllers
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{
if (EncodingHelper.IsCopyCodec(codec)
- && (string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
+ && (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase)))
{
@@ -1831,7 +1832,7 @@ namespace Jellyfin.Api.Controllers
// Set the key frame params for video encoding to match the hls segment time.
args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, isEventPlaylist, startNumber);
- // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
+ // Currently b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
{
args += " -bf 0";
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 58caae9f85..4d09070dbb 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -1,6 +1,7 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
+using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
@@ -9,6 +10,7 @@ using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -32,6 +34,7 @@ namespace Jellyfin.Api.Controllers
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization;
private readonly IDtoService _dtoService;
+ private readonly IAuthorizationContext _authContext;
private readonly ILogger _logger;
private readonly ISessionManager _sessionManager;
@@ -42,6 +45,7 @@ namespace Jellyfin.Api.Controllers
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
+ /// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
public ItemsController(
@@ -49,6 +53,7 @@ namespace Jellyfin.Api.Controllers
ILibraryManager libraryManager,
ILocalizationManager localization,
IDtoService dtoService,
+ IAuthorizationContext authContext,
ILogger logger,
ISessionManager sessionManager)
{
@@ -56,6 +61,7 @@ namespace Jellyfin.Api.Controllers
_libraryManager = libraryManager;
_localization = localization;
_dtoService = dtoService;
+ _authContext = authContext;
_logger = logger;
_sessionManager = sessionManager;
}
@@ -63,7 +69,7 @@ namespace Jellyfin.Api.Controllers
///
/// Gets items based on a query.
///
- /// The user id supplied as query parameter.
+ /// The user id supplied as query parameter; this is required when not using an API key.
/// Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).
/// Optional filter by items with theme songs.
/// Optional filter by items with theme videos.
@@ -151,15 +157,15 @@ namespace Jellyfin.Api.Controllers
/// A with the items.
[HttpGet("Items")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult> GetItems(
- [FromQuery] Guid userId,
+ public async Task>> GetItems(
+ [FromQuery] Guid? userId,
[FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong,
[FromQuery] bool? hasThemeVideo,
[FromQuery] bool? hasSubtitles,
[FromQuery] bool? hasSpecialFeature,
[FromQuery] bool? hasTrailer,
- [FromQuery] string? adjacentTo,
+ [FromQuery] Guid? adjacentTo,
[FromQuery] int? parentIndexNumber,
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
@@ -238,7 +244,19 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
- var user = userId.Equals(default) ? null : _userManager.GetUserById(userId);
+ var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
+
+ // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
+ var user = !auth.IsApiKey && userId.HasValue && !userId.Value.Equals(default)
+ ? _userManager.GetUserById(userId.Value)
+ : null;
+
+ // beyond this point, we're either using an api key or we have a valid user
+ if (!auth.IsApiKey && user is null)
+ {
+ return BadRequest("userId is required");
+ }
+
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -270,30 +288,39 @@ namespace Jellyfin.Api.Controllers
includeItemTypes = new[] { BaseItemKind.Playlist };
}
- var enabledChannels = user!.GetPreferenceValues(PreferenceKind.EnabledChannels);
+ var enabledChannels = auth.IsApiKey
+ ? Array.Empty()
+ : user!.GetPreferenceValues(PreferenceKind.EnabledChannels);
- bool isInEnabledFolder = Array.IndexOf(user.GetPreferenceValues(PreferenceKind.EnabledFolders), item.Id) != -1
+ // api keys are always enabled for all folders
+ bool isInEnabledFolder = auth.IsApiKey
+ || Array.IndexOf(user!.GetPreferenceValues(PreferenceKind.EnabledFolders), item.Id) != -1
// Assume all folders inside an EnabledChannel are enabled
|| Array.IndexOf(enabledChannels, item.Id) != -1
// Assume all items inside an EnabledChannel are enabled
|| Array.IndexOf(enabledChannels, item.ChannelId) != -1;
- var collectionFolders = _libraryManager.GetCollectionFolders(item);
- foreach (var collectionFolder in collectionFolders)
+ if (!isInEnabledFolder)
{
- if (user.GetPreferenceValues(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id))
+ var collectionFolders = _libraryManager.GetCollectionFolders(item);
+ foreach (var collectionFolder in collectionFolders)
{
- isInEnabledFolder = true;
+ // api keys never enter this block, so user is never null
+ if (user!.GetPreferenceValues(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id))
+ {
+ isInEnabledFolder = true;
+ }
}
}
+ // api keys are always enabled for all folders, so user is never null
if (item is not UserRootFolder
&& !isInEnabledFolder
- && !user.HasPermission(PermissionKind.EnableAllFolders)
+ && !user!.HasPermission(PermissionKind.EnableAllFolders)
&& !user.HasPermission(PermissionKind.EnableAllChannels)
&& !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase))
{
- _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
+ _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user.Username, item.Name);
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
}
@@ -606,7 +633,7 @@ namespace Jellyfin.Api.Controllers
/// A with the items.
[HttpGet("Users/{userId}/Items")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult> GetItemsByUserId(
+ public Task>> GetItemsByUserId(
[FromRoute] Guid userId,
[FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong,
@@ -614,7 +641,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasSubtitles,
[FromQuery] bool? hasSpecialFeature,
[FromQuery] bool? hasTrailer,
- [FromQuery] string? adjacentTo,
+ [FromQuery] Guid? adjacentTo,
[FromQuery] int? parentIndexNumber,
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 07e113ad3e..aeed0c0d61 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -60,9 +60,9 @@ namespace Jellyfin.Api.Controllers
/// Optional. The maximum number of records to return.
/// Optional. Supply a user id to search within a user's library or omit to search all.
/// The search term to filter on.
- /// If specified, only results with the specified item types are returned. This allows multiple, comma delimeted.
- /// If specified, results with these item types are filtered out. This allows multiple, comma delimeted.
- /// If specified, only results with the specified media types are returned. This allows multiple, comma delimeted.
+ /// If specified, only results with the specified item types are returned. This allows multiple, comma delimited.
+ /// If specified, results with these item types are filtered out. This allows multiple, comma delimited.
+ /// If specified, only results with the specified media types are returned. This allows multiple, comma delimited.
/// If specified, only children of the parent are returned.
/// Optional filter for movies.
/// Optional filter for series.
@@ -79,7 +79,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[Description("Gets search hints based on a search term")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult Get(
+ public ActionResult GetSearchHints(
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] Guid? userId,
@@ -140,7 +140,7 @@ namespace Jellyfin.Api.Controllers
IndexNumber = item.IndexNumber,
ParentIndexNumber = item.ParentIndexNumber,
Id = item.Id,
- Type = item.GetClientTypeName(),
+ Type = item.GetBaseItemKind(),
MediaType = item.MediaType,
MatchedTerm = hintInfo.MatchedTerm,
RunTimeTicks = item.RunTimeTicks,
@@ -149,8 +149,10 @@ namespace Jellyfin.Api.Controllers
EndDate = item.EndDate
};
- // legacy
+#pragma warning disable CS0618
+ // Kept for compatibility with older clients
result.ItemId = result.Id;
+#pragma warning restore CS0618
if (item.IsFolder)
{
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index 790d6e64d8..cf812fa237 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
@@ -31,7 +32,7 @@ namespace Jellyfin.Api.Controllers
///
/// Finds movies and trailers similar to a given trailer.
///
- /// The user id.
+ /// The user id supplied as query parameter; this is required when not using an API key.
/// Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).
/// Optional filter by items with theme songs.
/// Optional filter by items with theme videos.
@@ -118,15 +119,15 @@ namespace Jellyfin.Api.Controllers
/// A with the trailers.
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult> GetTrailers(
- [FromQuery] Guid userId,
+ public Task>> GetTrailers(
+ [FromQuery] Guid? userId,
[FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong,
[FromQuery] bool? hasThemeVideo,
[FromQuery] bool? hasSubtitles,
[FromQuery] bool? hasSpecialFeature,
[FromQuery] bool? hasTrailer,
- [FromQuery] string? adjacentTo,
+ [FromQuery] Guid? adjacentTo,
[FromQuery] int? parentIndexNumber,
[FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd,
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 179a53fd54..e39d05a6f5 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -77,7 +77,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery] string? seriesId,
+ [FromQuery] Guid? seriesId,
[FromQuery] Guid? parentId,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
@@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? season,
[FromQuery] Guid? seasonId,
[FromQuery] bool? isMissing,
- [FromQuery] string? adjacentTo,
+ [FromQuery] Guid? adjacentTo,
[FromQuery] Guid? startItemId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
@@ -278,9 +278,9 @@ namespace Jellyfin.Api.Controllers
}
// This must be the last filter
- if (!string.IsNullOrEmpty(adjacentTo))
+ if (adjacentTo.HasValue && !adjacentTo.Value.Equals(default))
{
- episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo).ToList();
+ episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList();
}
if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
@@ -326,7 +326,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? isSpecialSeason,
[FromQuery] bool? isMissing,
- [FromQuery] string? adjacentTo,
+ [FromQuery] Guid? adjacentTo,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 6d15d91858..d1109bebc8 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -282,16 +282,19 @@ namespace Jellyfin.Api.Controllers
}
else
{
- var success = await _userManager.AuthenticateUser(
- user.Username,
- request.CurrentPw,
- request.CurrentPw,
- HttpContext.GetNormalizedRemoteIp().ToString(),
- false).ConfigureAwait(false);
-
- if (success == null)
+ if (!HttpContext.User.IsInRole(UserRoles.Administrator))
{
- return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
+ var success = await _userManager.AuthenticateUser(
+ user.Username,
+ request.CurrentPw,
+ request.CurrentPw,
+ HttpContext.GetNormalizedRemoteIp().ToString(),
+ false).ConfigureAwait(false);
+
+ if (success == null)
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
+ }
}
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
@@ -499,7 +502,7 @@ namespace Jellyfin.Api.Controllers
if (isLocal)
{
- _logger.LogWarning("Password reset proccess initiated from outside the local network with IP: {IP}", ip);
+ _logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip);
}
var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index d037ba7123..894d871383 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -17,7 +17,7 @@
-
+
diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
index 192f33ebd1..8182e3c9e8 100644
--- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs
@@ -169,7 +169,7 @@ namespace Jellyfin.Api.Models.StreamingDtos
///
/// Disposes the stream state.
///
- /// Whether the object is currently beeing disposed.
+ /// Whether the object is currently being disposed.
protected virtual void Dispose(bool disposing)
{
if (_disposed)
diff --git a/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs b/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs
index 02ce5a0488..226a584e1d 100644
--- a/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs
+++ b/Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs
@@ -17,9 +17,9 @@ namespace Jellyfin.Api.Models.SyncPlayDtos
}
///
- /// Gets or sets the playlist identifiers ot the items. Ignored when clearing the playlist.
+ /// Gets or sets the playlist identifiers of the items. Ignored when clearing the playlist.
///
- /// The playlist identifiers ot the items.
+ /// The playlist identifiers of the items.
public IReadOnlyList PlaylistItemIds { get; set; }
///
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 89799883fd..47499e038b 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -18,7 +18,7 @@
Jellyfin Contributors
Jellyfin.Data
- 10.8.0
+ 10.9.0
https://github.com/jellyfin/jellyfin
GPL-3.0-only
diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index 94614b4c8c..b64a842927 100644
--- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -18,8 +18,8 @@
-
-
+
+
diff --git a/Jellyfin.Drawing.Skia/SkiaHelper.cs b/Jellyfin.Drawing.Skia/SkiaHelper.cs
index c001c32b8c..0478fc7c31 100644
--- a/Jellyfin.Drawing.Skia/SkiaHelper.cs
+++ b/Jellyfin.Drawing.Skia/SkiaHelper.cs
@@ -13,7 +13,7 @@ namespace Jellyfin.Drawing.Skia
///
/// The current skia encoder.
/// The list of image paths.
- /// The current checked indes.
+ /// The current checked index.
/// The new index.
/// A valid bitmap, or null if no bitmap exists after currentIndex.
public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList paths, int currentIndex, out int newIndex)
diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
index 61db223d92..361dbc8142 100644
--- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
+++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
@@ -193,7 +193,7 @@ namespace Jellyfin.Networking.Configuration
public bool AutoDiscovery { get; set; } = true;
///
- /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with .
+ /// Gets or sets the filter for remote IP connectivity. Used in conjunction with .
///
public string[] RemoteIPFilter { get; set; } = Array.Empty();
diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs
index 4b7b87814c..fd0665dbdd 100644
--- a/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -944,7 +944,7 @@ namespace Jellyfin.Networking.Manager
// Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded.
if (config.IgnoreVirtualInterfaces)
{
- // each virtual interface name must be pre-pended with the exclusion symbol !
+ // each virtual interface name must be prepended with the exclusion symbol !
var virtualInterfaceNames = config.VirtualInterfaceNames.Split(',').Select(p => "!" + p).ToArray();
if (lanAddresses.Length > 0)
{
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 71614dce5d..678f960839 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -27,13 +27,13 @@
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 0a507c9f96..e6bc3fe2b9 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -37,8 +37,8 @@
-
-
+
+
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 2bda8d2905..a6f0b705dc 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -243,7 +243,7 @@ namespace Jellyfin.Server
}
}
- appHost.Dispose();
+ await appHost.DisposeAsync().ConfigureAwait(false);
}
if (_restartOnShutdown)
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 8eb5f21960..1954a5c558 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -1,4 +1,5 @@
using System;
+using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
@@ -103,6 +104,22 @@ namespace Jellyfin.Server
})
.ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
+ services.AddHttpClient(NamedClient.Dlna, c =>
+ {
+ c.DefaultRequestHeaders.UserAgent.ParseAdd(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "{0}/{1} UPnP/1.0 {2}/{3}",
+ MediaBrowser.Common.System.OperatingSystem.Name,
+ Environment.OSVersion,
+ _serverApplicationHost.Name,
+ _serverApplicationHost.ApplicationVersionString));
+
+ c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", _serverApplicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0
+ c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", _serverApplicationHost.FriendlyName); // REVIEW: where does this come from?
+ })
+ .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
+
services.AddHealthChecks()
.AddDbContextCheck();
diff --git a/MediaBrowser.Common/Configuration/EncodingConfigurationExtensions.cs b/MediaBrowser.Common/Configuration/EncodingConfigurationExtensions.cs
index 89740ae084..70a4fe4098 100644
--- a/MediaBrowser.Common/Configuration/EncodingConfigurationExtensions.cs
+++ b/MediaBrowser.Common/Configuration/EncodingConfigurationExtensions.cs
@@ -32,7 +32,7 @@ namespace MediaBrowser.Common.Configuration
var transcodingTempPath = configurationManager.GetEncodingOptions().TranscodingTempPath;
if (string.IsNullOrEmpty(transcodingTempPath))
{
- transcodingTempPath = Path.Combine(configurationManager.CommonApplicationPaths.ProgramDataPath, "transcodes");
+ transcodingTempPath = Path.Combine(configurationManager.CommonApplicationPaths.CachePath, "transcodes");
}
// Make sure the directory exists
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index bbb797ab99..7a55f398a1 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -8,7 +8,7 @@
Jellyfin Contributors
Jellyfin.Common
- 10.8.0
+ 10.9.0
https://github.com/jellyfin/jellyfin
GPL-3.0-only
diff --git a/MediaBrowser.Common/Net/NamedClient.cs b/MediaBrowser.Common/Net/NamedClient.cs
index 0f6161c328..a6cacd4f17 100644
--- a/MediaBrowser.Common/Net/NamedClient.cs
+++ b/MediaBrowser.Common/Net/NamedClient.cs
@@ -14,5 +14,10 @@
/// Gets the value for the MusicBrainz named http client.
///
public const string MusicBrainz = nameof(MusicBrainz);
+
+ ///
+ /// Gets the value for the DLNA named http client.
+ ///
+ public const string Dlna = nameof(Dlna);
}
}
diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
index 03d1f33043..bd397bdd13 100644
--- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
+++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
@@ -169,8 +169,8 @@ namespace MediaBrowser.Controller.Entities.Audio
var childUpdateType = ItemUpdateType.None;
- // Refresh songs
- foreach (var item in items)
+ // Refresh songs only and not m3u files in album folder
+ foreach (var item in items.OfType
diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
deleted file mode 100644
index 08ee5c72e5..0000000000
--- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Microsoft.Extensions.Logging;
-using Nikse.SubtitleEdit.Core.SubtitleFormats;
-
-namespace MediaBrowser.MediaEncoding.Subtitles
-{
- ///
- /// Advanced SubStation Alpha subtitle parser.
- ///
- public class AssParser : SubtitleEditParser
- {
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- public AssParser(ILogger logger) : base(logger)
- {
- }
- }
-}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs
index c0023ebf24..bd13437fb6 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/ISubtitleParser.cs
@@ -1,7 +1,6 @@
#pragma warning disable CS1591
using System.IO;
-using System.Threading;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles
@@ -12,8 +11,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// Parses the specified stream.
///
/// The stream.
- /// The cancellation token.
+ /// The file extension.
/// SubtitleTrackInfo.
- SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken);
+ SubtitleTrackInfo Parse(Stream stream, string fileExtension);
+
+ ///
+ /// Determines whether the file extension is supported by the parser.
+ ///
+ /// The file extension.
+ /// A value indicating whether the file extension is supported.
+ bool SupportsFileExtension(string fileExtension);
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
deleted file mode 100644
index 78d54ca51f..0000000000
--- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Microsoft.Extensions.Logging;
-using Nikse.SubtitleEdit.Core.SubtitleFormats;
-
-namespace MediaBrowser.MediaEncoding.Subtitles
-{
- ///
- /// SubRip subtitle parser.
- ///
- public class SrtParser : SubtitleEditParser
- {
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- public SrtParser(ILogger logger) : base(logger)
- {
- }
- }
-}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
deleted file mode 100644
index 17c2ae40e0..0000000000
--- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Microsoft.Extensions.Logging;
-using Nikse.SubtitleEdit.Core.SubtitleFormats;
-
-namespace MediaBrowser.MediaEncoding.Subtitles
-{
- ///
- /// SubStation Alpha subtitle parser.
- ///
- public class SsaParser : SubtitleEditParser
- {
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- public SsaParser(ILogger logger) : base(logger)
- {
- }
- }
-}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
index 52c1b64677..eb8ff96246 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
@@ -1,12 +1,14 @@
+using System;
+using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Threading;
+using System.Reflection;
using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
using Nikse.SubtitleEdit.Core.Common;
-using ILogger = Microsoft.Extensions.Logging.ILogger;
+using Nikse.SubtitleEdit.Core.SubtitleFormats;
using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat;
namespace MediaBrowser.MediaEncoding.Subtitles
@@ -14,31 +16,57 @@ namespace MediaBrowser.MediaEncoding.Subtitles
///
/// SubStation Alpha subtitle parser.
///
- /// The .
- public abstract class SubtitleEditParser : ISubtitleParser
- where T : SubtitleFormat, new()
+ public class SubtitleEditParser : ISubtitleParser
{
- private readonly ILogger _logger;
+ private readonly ILogger _logger;
+ private readonly Dictionary _subtitleFormats;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The logger.
- protected SubtitleEditParser(ILogger logger)
+ public SubtitleEditParser(ILogger logger)
{
_logger = logger;
+ _subtitleFormats = GetSubtitleFormats()
+ .Where(subtitleFormat => !string.IsNullOrEmpty(subtitleFormat.Extension))
+ .GroupBy(subtitleFormat => subtitleFormat.Extension.TrimStart('.'), StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase);
}
///
- public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken)
+ public SubtitleTrackInfo Parse(Stream stream, string fileExtension)
{
var subtitle = new Subtitle();
- var subRip = new T();
var lines = stream.ReadAllLines().ToList();
- subRip.LoadSubtitle(subtitle, lines, "untitled");
- if (subRip.ErrorCount > 0)
+
+ if (!_subtitleFormats.TryGetValue(fileExtension, out var subtitleFormats))
{
- _logger.LogError("{ErrorCount} errors encountered while parsing subtitle", subRip.ErrorCount);
+ throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension));
+ }
+
+ foreach (var subtitleFormat in subtitleFormats)
+ {
+ _logger.LogDebug(
+ "Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
+ fileExtension,
+ subtitleFormat.Name);
+ subtitleFormat.LoadSubtitle(subtitle, lines, fileExtension);
+ if (subtitleFormat.ErrorCount == 0)
+ {
+ break;
+ }
+
+ _logger.LogError(
+ "{ErrorCount} errors encountered while parsing '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
+ subtitleFormat.ErrorCount,
+ fileExtension,
+ subtitleFormat.Name);
+ }
+
+ if (subtitle.Paragraphs.Count == 0)
+ {
+ throw new ArgumentException("Unsupported format: " + fileExtension);
}
var trackInfo = new SubtitleTrackInfo();
@@ -57,5 +85,36 @@ namespace MediaBrowser.MediaEncoding.Subtitles
trackInfo.TrackEvents = trackEvents;
return trackInfo;
}
+
+ ///
+ public bool SupportsFileExtension(string fileExtension)
+ => _subtitleFormats.ContainsKey(fileExtension);
+
+ private IEnumerable GetSubtitleFormats()
+ {
+ var subtitleFormats = new List();
+ var assembly = typeof(SubtitleFormat).Assembly;
+
+ foreach (var type in assembly.GetTypes())
+ {
+ if (!type.IsSubclassOf(typeof(SubtitleFormat)) || type.IsAbstract)
+ {
+ continue;
+ }
+
+ try
+ {
+ // It shouldn't be null, but the exception is caught if it is
+ var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(type, true)!;
+ subtitleFormats.Add(subtitleFormat);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to create instance of the subtitle format {SubtitleFormatType}", type.Name);
+ }
+ }
+
+ return subtitleFormats;
+ }
}
}
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 7091af734a..8e1acc46c9 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -35,6 +35,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
private readonly IMediaEncoder _mediaEncoder;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly ISubtitleParser _subtitleParser;
///
/// The _semaphoreLocks.
@@ -48,7 +49,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
IFileSystem fileSystem,
IMediaEncoder mediaEncoder,
IHttpClientFactory httpClientFactory,
- IMediaSourceManager mediaSourceManager)
+ IMediaSourceManager mediaSourceManager,
+ ISubtitleParser subtitleParser)
{
_logger = logger;
_appPaths = appPaths;
@@ -56,6 +58,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
_mediaEncoder = mediaEncoder;
_httpClientFactory = httpClientFactory;
_mediaSourceManager = mediaSourceManager;
+ _subtitleParser = subtitleParser;
}
private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
@@ -73,8 +76,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
try
{
- var reader = GetReader(inputFormat);
- var trackInfo = reader.Parse(stream, cancellationToken);
+ var trackInfo = _subtitleParser.Parse(stream, inputFormat);
FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
@@ -233,54 +235,21 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec)
.TrimStart('.');
- if (!TryGetReader(currentFormat, out _))
+ // Fallback to ffmpeg conversion
+ if (!_subtitleParser.SupportsFileExtension(currentFormat))
{
// Convert
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt");
- await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
+ await ConvertTextSubtitleToSrt(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true);
}
- // It's possbile that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs)
+ // It's possible that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs)
return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true);
}
- private bool TryGetReader(string format, [NotNullWhen(true)] out ISubtitleParser? value)
- {
- if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
- {
- value = new SrtParser(_logger);
- return true;
- }
-
- if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
- {
- value = new SsaParser(_logger);
- return true;
- }
-
- if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
- {
- value = new AssParser(_logger);
- return true;
- }
-
- value = null;
- return false;
- }
-
- private ISubtitleParser GetReader(string format)
- {
- if (TryGetReader(format, out var reader))
- {
- return reader;
- }
-
- throw new ArgumentException("Unsupported format: " + format);
- }
-
private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value)
{
if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
@@ -351,13 +320,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
///
/// Converts the text subtitle to SRT.
///
- /// The input path.
- /// The language.
+ /// The subtitle stream.
/// The input mediaSource.
/// The output path.
/// The cancellation token.
/// Task.
- private async Task ConvertTextSubtitleToSrt(string inputPath, string language, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
+ private async Task ConvertTextSubtitleToSrt(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
{
var semaphore = GetLock(outputPath);
@@ -367,7 +335,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
if (!File.Exists(outputPath))
{
- await ConvertTextSubtitleToSrtInternal(inputPath, language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
+ await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
}
}
finally
@@ -379,8 +347,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
///
/// Converts the text subtitle to SRT internal.
///
- /// The input path.
- /// The language.
+ /// The subtitle stream.
/// The input mediaSource.
/// The output path.
/// The cancellation token.
@@ -388,8 +355,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
///
/// The inputPath or outputPath is null.
///
- private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string language, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
+ private async Task ConvertTextSubtitleToSrtInternal(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
{
+ var inputPath = subtitleStream.Path;
if (string.IsNullOrEmpty(inputPath))
{
throw new ArgumentNullException(nameof(inputPath));
@@ -402,7 +370,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)));
- var encodingParam = await GetSubtitleFileCharacterSet(inputPath, language, mediaSource.Protocol, cancellationToken).ConfigureAwait(false);
+ var encodingParam = await GetSubtitleFileCharacterSet(subtitleStream, subtitleStream.Language, mediaSource, cancellationToken).ConfigureAwait(false);
// FFmpeg automatically convert character encoding when it is UTF-16
// If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event"
@@ -420,18 +388,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
int exitCode;
using (var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
{
- StartInfo = new ProcessStartInfo
- {
- CreateNoWindow = true,
- UseShellExecute = false,
- FileName = _mediaEncoder.EncoderPath,
- Arguments = string.Format(CultureInfo.InvariantCulture, "{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath),
- WindowStyle = ProcessWindowStyle.Hidden,
- ErrorDialog = false
- },
- EnableRaisingEvents = true
- })
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ FileName = _mediaEncoder.EncoderPath,
+ Arguments = string.Format(CultureInfo.InvariantCulture, "{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath),
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ },
+ EnableRaisingEvents = true
+ })
{
_logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
@@ -571,7 +539,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var processArgs = string.Format(
CultureInfo.InvariantCulture,
- "-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"",
+ "-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
inputPath,
subtitleStreamIndex,
outputCodec,
@@ -580,18 +548,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
int exitCode;
using (var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
{
- StartInfo = new ProcessStartInfo
- {
- CreateNoWindow = true,
- UseShellExecute = false,
- FileName = _mediaEncoder.EncoderPath,
- Arguments = processArgs,
- WindowStyle = ProcessWindowStyle.Hidden,
- ErrorDialog = false
- },
- EnableRaisingEvents = true
- })
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ FileName = _mediaEncoder.EncoderPath,
+ Arguments = processArgs,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ },
+ EnableRaisingEvents = true
+ })
{
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
@@ -606,7 +574,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw;
}
- var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(5)).ConfigureAwait(false);
+ var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
if (!ranToCompletion)
{
@@ -729,9 +697,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
///
- public async Task GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken)
+ public async Task GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceInfo mediaSource, CancellationToken cancellationToken)
{
- using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false))
+ var subtitleCodec = subtitleStream.Codec;
+ var path = subtitleStream.Path;
+
+ if (path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
+ {
+ path = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec);
+ await ExtractTextSubtitle(mediaSource, subtitleStream, subtitleCodec, path, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ using (var stream = await GetStream(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false))
{
var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName ?? string.Empty;
@@ -754,12 +732,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
switch (protocol)
{
case MediaProtocol.Http:
- {
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
- .GetAsync(new Uri(path), cancellationToken)
- .ConfigureAwait(false);
- return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- }
+ {
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ .GetAsync(new Uri(path), cancellationToken)
+ .ConfigureAwait(false);
+ return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ }
case MediaProtocol.File:
return AsyncFile.OpenRead(path);
diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
index 47c36494bd..c32c1c108c 100644
--- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
+++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
@@ -157,7 +157,7 @@ namespace MediaBrowser.Model.Dlna
flagValue |= DlnaFlags.ByteBasedSeek;
}
- // Time based seek is curently disabled when streaming. On LG CX3 adding DlnaFlags.TimeBasedSeek and orgPn causes the DLNA playback to fail (format not supported). Further investigations are needed before enabling the remaining code paths.
+ // Time based seek is currently disabled when streaming. On LG CX3 adding DlnaFlags.TimeBasedSeek and orgPn causes the DLNA playback to fail (format not supported). Further investigations are needed before enabling the remaining code paths.
// else if (runtimeTicks.HasValue)
// {
// flagValue = flagValue | DlnaFlags.TimeBasedSeek;
diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs
index 094dc73b27..fdb84fa320 100644
--- a/MediaBrowser.Model/Dto/BaseItemDto.cs
+++ b/MediaBrowser.Model/Dto/BaseItemDto.cs
@@ -294,13 +294,13 @@ namespace MediaBrowser.Model.Dto
public NameGuidPair[] GenreItems { get; set; }
///
- /// Gets or sets wether the item has a logo, this will hold the Id of the Parent that has one.
+ /// Gets or sets whether the item has a logo, this will hold the Id of the Parent that has one.
///
/// The parent logo item id.
public Guid? ParentLogoItemId { get; set; }
///
- /// Gets or sets wether the item has any backdrops, this will hold the Id of the Parent that has one.
+ /// Gets or sets whether the item has any backdrops, this will hold the Id of the Parent that has one.
///
/// The parent backdrop item id.
public Guid? ParentBackdropItemId { get; set; }
@@ -506,7 +506,7 @@ namespace MediaBrowser.Model.Dto
public string ParentLogoImageTag { get; set; }
///
- /// Gets or sets wether the item has fan art, this will hold the Id of the Parent that has one.
+ /// Gets or sets whether the item has fan art, this will hold the Id of the Parent that has one.
///
/// The parent art item id.
public Guid? ParentArtItemId { get; set; }
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index 58988b6fae..90a60cf470 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -588,15 +588,26 @@ namespace MediaBrowser.Model.Entities
return Width switch
{
- <= 720 when Height <= 480 => IsInterlaced ? "480i" : "480p",
- // 720x576 (PAL) (768 when rescaled for square pixels)
- <= 768 when Height <= 576 => IsInterlaced ? "576i" : "576p",
- // 960x540 (sometimes 544 which is multiple of 16)
+ // 256x144 (16:9 square pixel format)
+ <= 256 when Height <= 144 => IsInterlaced ? "144i" : "144p",
+ // 426x240 (16:9 square pixel format)
+ <= 426 when Height <= 240 => IsInterlaced ? "240i" : "240p",
+ // 640x360 (16:9 square pixel format)
+ <= 640 when Height <= 360 => IsInterlaced ? "360i" : "360p",
+ // 682x384 (16:9 square pixel format)
+ <= 682 when Height <= 384 => IsInterlaced ? "384i" : "384p",
+ // 720x404 (16:9 square pixel format)
+ <= 720 when Height <= 404 => IsInterlaced ? "404i" : "404p",
+ // 854x480 (16:9 square pixel format)
+ <= 854 when Height <= 480 => IsInterlaced ? "480i" : "480p",
+ // 960x544 (16:9 square pixel format)
<= 960 when Height <= 544 => IsInterlaced ? "540i" : "540p",
+ // 1024x576 (16:9 square pixel format)
+ <= 1024 when Height <= 576 => IsInterlaced ? "576i" : "576p",
// 1280x720
<= 1280 when Height <= 962 => IsInterlaced ? "720i" : "720p",
- // 1920x1080
- <= 1920 when Height <= 1440 => IsInterlaced ? "1080i" : "1080p",
+ // 2560x1080 (FHD ultra wide 21:9) using 1440px width to accommodate WQHD
+ <= 2560 when Height <= 1440 => IsInterlaced ? "1080i" : "1080p",
// 4K
<= 4096 when Height <= 3072 => "4K",
// 8K
diff --git a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs
index 05576a0f8d..a832169c2a 100644
--- a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs
+++ b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs
@@ -8,6 +8,7 @@ namespace MediaBrowser.Model.LiveTv
public TunerHostInfo()
{
AllowHWTranscoding = true;
+ IgnoreDts = true;
}
public string Id { get; set; }
@@ -31,5 +32,7 @@ namespace MediaBrowser.Model.LiveTv
public int TunerCount { get; set; }
public string UserAgent { get; set; }
+
+ public bool IgnoreDts { get; set; }
}
}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index b4e93de19b..4f511f9960 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -8,7 +8,7 @@
Jellyfin Contributors
Jellyfin.Model
- 10.8.0
+ 10.9.0
https://github.com/jellyfin/jellyfin
GPL-3.0-only
diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs
index 133d6a9162..0fb996df97 100644
--- a/MediaBrowser.Model/Querying/NextUpQuery.cs
+++ b/MediaBrowser.Model/Querying/NextUpQuery.cs
@@ -33,7 +33,7 @@ namespace MediaBrowser.Model.Querying
/// Gets or sets the series id.
///
/// The series id.
- public string SeriesId { get; set; }
+ public Guid? SeriesId { get; set; }
///
/// Gets or sets the start index. Use for paging.
diff --git a/MediaBrowser.Model/Search/SearchHint.cs b/MediaBrowser.Model/Search/SearchHint.cs
index 983dbd2bc0..4696c3797b 100644
--- a/MediaBrowser.Model/Search/SearchHint.cs
+++ b/MediaBrowser.Model/Search/SearchHint.cs
@@ -1,8 +1,6 @@
-#nullable disable
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Search
{
@@ -11,12 +9,28 @@ namespace MediaBrowser.Model.Search
///
public class SearchHint
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SearchHint()
+ {
+ Name = string.Empty;
+ MatchedTerm = string.Empty;
+ MediaType = string.Empty;
+ Artists = Array.Empty();
+ }
+
///
/// Gets or sets the item id.
///
/// The item id.
+ [Obsolete("Use Id instead")]
public Guid ItemId { get; set; }
+ ///
+ /// Gets or sets the item id.
+ ///
+ /// The item id.
public Guid Id { get; set; }
///
@@ -53,38 +67,42 @@ namespace MediaBrowser.Model.Search
/// Gets or sets the image tag.
///
/// The image tag.
- public string PrimaryImageTag { get; set; }
+ public string? PrimaryImageTag { get; set; }
///
/// Gets or sets the thumb image tag.
///
/// The thumb image tag.
- public string ThumbImageTag { get; set; }
+ public string? ThumbImageTag { get; set; }
///
/// Gets or sets the thumb image item identifier.
///
/// The thumb image item identifier.
- public string ThumbImageItemId { get; set; }
+ public string? ThumbImageItemId { get; set; }
///
/// Gets or sets the backdrop image tag.
///
/// The backdrop image tag.
- public string BackdropImageTag { get; set; }
+ public string? BackdropImageTag { get; set; }
///
/// Gets or sets the backdrop image item identifier.
///
/// The backdrop image item identifier.
- public string BackdropImageItemId { get; set; }
+ public string? BackdropImageItemId { get; set; }
///
/// Gets or sets the type.
///
/// The type.
- public string Type { get; set; }
+ public BaseItemKind Type { get; set; }
+ ///
+ /// Gets a value indicating whether this instance is folder.
+ ///
+ /// true if this instance is folder; otherwise, false.
public bool? IsFolder { get; set; }
///
@@ -99,31 +117,47 @@ namespace MediaBrowser.Model.Search
/// The type of the media.
public string MediaType { get; set; }
+ ///
+ /// Gets or sets the start date.
+ ///
+ /// The start date.
public DateTime? StartDate { get; set; }
+ ///
+ /// Gets or sets the end date.
+ ///
+ /// The end date.
public DateTime? EndDate { get; set; }
///
/// Gets or sets the series.
///
/// The series.
- public string Series { get; set; }
+ public string? Series { get; set; }
- public string Status { get; set; }
+ ///
+ /// Gets or sets the status.
+ ///
+ /// The status.
+ public string? Status { get; set; }
///
/// Gets or sets the album.
///
/// The album.
- public string Album { get; set; }
+ public string? Album { get; set; }
- public Guid AlbumId { get; set; }
+ ///
+ /// Gets or sets the album id.
+ ///
+ /// The album id.
+ public Guid? AlbumId { get; set; }
///
/// Gets or sets the album artist.
///
/// The album artist.
- public string AlbumArtist { get; set; }
+ public string? AlbumArtist { get; set; }
///
/// Gets or sets the artists.
@@ -147,13 +181,13 @@ namespace MediaBrowser.Model.Search
/// Gets or sets the channel identifier.
///
/// The channel identifier.
- public Guid ChannelId { get; set; }
+ public Guid? ChannelId { get; set; }
///
/// Gets or sets the name of the channel.
///
/// The name of the channel.
- public string ChannelName { get; set; }
+ public string? ChannelName { get; set; }
///
/// Gets or sets the primary image aspect ratio.
diff --git a/MediaBrowser.Model/SyncPlay/GroupStateType.cs b/MediaBrowser.Model/SyncPlay/GroupStateType.cs
index 7aa454f928..96364cacc7 100644
--- a/MediaBrowser.Model/SyncPlay/GroupStateType.cs
+++ b/MediaBrowser.Model/SyncPlay/GroupStateType.cs
@@ -11,7 +11,7 @@ namespace MediaBrowser.Model.SyncPlay
Idle = 0,
///
- /// The group is in wating state. Playback is paused. Will start playing when users are ready.
+ /// The group is in waiting state. Playback is paused. Will start playing when users are ready.
///
Waiting = 1,
diff --git a/MediaBrowser.Model/Tasks/ITaskManager.cs b/MediaBrowser.Model/Tasks/ITaskManager.cs
index a86bf2a1c8..13bebc479e 100644
--- a/MediaBrowser.Model/Tasks/ITaskManager.cs
+++ b/MediaBrowser.Model/Tasks/ITaskManager.cs
@@ -22,7 +22,7 @@ namespace MediaBrowser.Model.Tasks
///
/// Cancels if running and queue.
///
- /// An implementatin of .
+ /// An implementation of .
/// Task options.
void CancelIfRunningAndQueue(TaskOptions options)
where T : IScheduledTask;
@@ -30,21 +30,21 @@ namespace MediaBrowser.Model.Tasks
///
/// Cancels if running and queue.
///
- /// An implementatin of .
+ /// An implementation of .
void CancelIfRunningAndQueue()
where T : IScheduledTask;
///
/// Cancels if running.
///
- /// An implementatin of .
+ /// An implementation of .
void CancelIfRunning()
where T : IScheduledTask;
///
/// Queues the scheduled task.
///
- /// An implementatin of .
+ /// An implementation of .
/// Task options.
void QueueScheduledTask(TaskOptions options)
where T : IScheduledTask;
@@ -52,7 +52,7 @@ namespace MediaBrowser.Model.Tasks
///
/// Queues the scheduled task.
///
- /// An implementatin of .
+ /// An implementation of .
void QueueScheduledTask()
where T : IScheduledTask;
diff --git a/MediaBrowser.Model/Tasks/ITaskTrigger.cs b/MediaBrowser.Model/Tasks/ITaskTrigger.cs
index 8c3ec6626c..0536f4ef77 100644
--- a/MediaBrowser.Model/Tasks/ITaskTrigger.cs
+++ b/MediaBrowser.Model/Tasks/ITaskTrigger.cs
@@ -21,10 +21,10 @@ namespace MediaBrowser.Model.Tasks
///
/// Stars waiting for the trigger action.
///
- /// Result of the last run triggerd task.
+ /// Result of the last run triggered task.
/// The .
/// The name of the task.
- /// Wheter or not this is is fired during startup.
+ /// Whether or not this is is fired during startup.
void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup);
///
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 01ff473f0c..bbb33ddf0e 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -926,7 +926,7 @@ namespace MediaBrowser.Providers.Manager
}
catch (Exception ex)
{
- _logger.LogError(ex, "Error in {0}.Suports", i.GetType().Name);
+ _logger.LogError(ex, "Error in {0}.Supports", i.GetType().Name);
return false;
}
});
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
index 4bf66c0988..915fb97fd2 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
@@ -36,7 +36,7 @@ namespace MediaBrowser.Providers.Music
///
/// The Jellyfin user-agent is unrestricted but source IP must not exceed
/// one request per second, therefore we rate limit to avoid throttling.
- /// Be prudent, use a value slightly above the minimun required.
+ /// Be prudent, use a value slightly above the minimum required.
/// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting.
///
private readonly long _musicBrainzQueryIntervalMs;
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index f49492f337..c09b6d8138 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -8,6 +8,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -40,6 +41,7 @@ namespace MediaBrowser.Providers.TV
{
await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
+ RemoveObsoleteEpisodes(item);
RemoveObsoleteSeasons(item);
await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
}
@@ -121,6 +123,61 @@ namespace MediaBrowser.Providers.TV
}
}
+ private void RemoveObsoleteEpisodes(Series series)
+ {
+ var episodes = series.GetEpisodes(null, new DtoOptions()).OfType().ToList();
+ var numberOfEpisodes = episodes.Count;
+ // TODO: O(n^2), but can it be done faster without overcomplicating it?
+ for (var i = 0; i < numberOfEpisodes; i++)
+ {
+ var currentEpisode = episodes[i];
+ // The outer loop only examines virtual episodes
+ if (!currentEpisode.IsVirtualItem)
+ {
+ continue;
+ }
+
+ // Virtual episodes without an episode number are practically orphaned and should be deleted
+ if (!currentEpisode.IndexNumber.HasValue)
+ {
+ DeleteEpisode(currentEpisode);
+ continue;
+ }
+
+ for (var j = i + 1; j < numberOfEpisodes; j++)
+ {
+ var comparisonEpisode = episodes[j];
+ // The inner loop is only for "physical" episodes
+ if (comparisonEpisode.IsVirtualItem
+ || currentEpisode.ParentIndexNumber != comparisonEpisode.ParentIndexNumber
+ || !comparisonEpisode.ContainsEpisodeNumber(currentEpisode.IndexNumber.Value))
+ {
+ continue;
+ }
+
+ DeleteEpisode(currentEpisode);
+ break;
+ }
+ }
+ }
+
+ private void DeleteEpisode(Episode episode)
+ {
+ Logger.LogInformation(
+ "Removing virtual episode S{SeasonNumber}E{EpisodeNumber} in series {SeriesName}",
+ episode.ParentIndexNumber,
+ episode.IndexNumber,
+ episode.SeriesName);
+
+ LibraryManager.DeleteItem(
+ episode,
+ new DeleteOptions
+ {
+ DeleteFileLocation = true
+ },
+ false);
+ }
+
///
/// Creates seasons for all episodes that aren't in a season folder.
/// If no season number can be determined, a dummy season will be created.
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index 09ff84044d..da348239a1 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -1330,7 +1330,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
};
///
- /// Used to split names of comma or pipe delimeted genres and people.
+ /// Used to split names of comma or pipe delimited genres and people.
///
/// The value.
/// IEnumerable{System.String}.
diff --git a/SharedVersion.cs b/SharedVersion.cs
index 5e2f151a22..238ef83bdc 100644
--- a/SharedVersion.cs
+++ b/SharedVersion.cs
@@ -1,4 +1,4 @@
using System.Reflection;
-[assembly: AssemblyVersion("10.8.0")]
-[assembly: AssemblyFileVersion("10.8.0")]
+[assembly: AssemblyVersion("10.9.0")]
+[assembly: AssemblyFileVersion("10.9.0")]
diff --git a/build.yaml b/build.yaml
index 18434ee003..3f676a5cfa 100644
--- a/build.yaml
+++ b/build.yaml
@@ -1,7 +1,7 @@
---
# We just wrap `build` so this is really it
name: "jellyfin"
-version: "10.8.0"
+version: "10.9.0"
packages:
- debian.amd64
- debian.arm64
diff --git a/debian/changelog b/debian/changelog
index 430594cac2..0d744c02a3 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+jellyfin-server (10.9.0-1) unstable; urgency=medium
+
+ * New upstream version 10.9.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.9.0
+
+ -- Jellyfin Packaging Team Wed, 13 Jul 2022 20:58:08 -0600
+
jellyfin-server (10.8.0-1) unstable; urgency=medium
* Forthcoming stable release
diff --git a/debian/metapackage/jellyfin b/debian/metapackage/jellyfin
index a9a0ae5b01..8787c3a496 100644
--- a/debian/metapackage/jellyfin
+++ b/debian/metapackage/jellyfin
@@ -5,7 +5,7 @@ Homepage: https://jellyfin.org
Standards-Version: 3.9.2
Package: jellyfin
-Version: 10.8.0
+Version: 10.9.0
Maintainer: Jellyfin Packaging Team
Depends: jellyfin-server, jellyfin-web
Description: Provides the Jellyfin Free Software Media System
diff --git a/debian/rules b/debian/rules
index 64e2b48ea8..f55b1807ea 100755
--- a/debian/rules
+++ b/debian/rules
@@ -40,7 +40,7 @@ override_dh_clistrip:
override_dh_auto_build:
dotnet publish -maxcpucount:1 --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \
- "-p:DebugSymbols=false;DebugType=none" Jellyfin.Server
+ -p:DebugSymbols=false -p:DebugType=none Jellyfin.Server
override_dh_auto_clean:
dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true
diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64
index 20847fd258..81d75c1aae 100644
--- a/deployment/Dockerfile.centos.amd64
+++ b/deployment/Dockerfile.centos.amd64
@@ -13,7 +13,7 @@ RUN yum update -yq \
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/77d472e5-194c-421e-992d-e4ca1d08e6cc/56c61ac303ddf1b12026151f4f000a2b/dotnet-sdk-6.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/cd0d0a4d-2a6a-4d0d-b42e-dfd3b880e222/008a93f83aba6d1acf75ded3d2cfba24/dotnet-sdk-6.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.docker.amd64 b/deployment/Dockerfile.docker.amd64
index b2bd40713d..3fd3fa33ce 100644
--- a/deployment/Dockerfile.docker.amd64
+++ b/deployment/Dockerfile.docker.amd64
@@ -10,4 +10,4 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# because of changes in docker and systemd we need to not build in parallel at the moment
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
diff --git a/deployment/Dockerfile.docker.arm64 b/deployment/Dockerfile.docker.arm64
index fc60f16246..e3cc92bcb3 100644
--- a/deployment/Dockerfile.docker.arm64
+++ b/deployment/Dockerfile.docker.arm64
@@ -10,4 +10,4 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# because of changes in docker and systemd we need to not build in parallel at the moment
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none
diff --git a/deployment/Dockerfile.docker.armhf b/deployment/Dockerfile.docker.armhf
index f5cc47d83e..3a5df2e246 100644
--- a/deployment/Dockerfile.docker.armhf
+++ b/deployment/Dockerfile.docker.armhf
@@ -10,4 +10,4 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# because of changes in docker and systemd we need to not build in parallel at the moment
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none
diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64
index 5da6fbf774..4139ed96d3 100644
--- a/deployment/Dockerfile.fedora.amd64
+++ b/deployment/Dockerfile.fedora.amd64
@@ -12,7 +12,7 @@ RUN dnf update -yq \
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make
# Install DotNET SDK
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/77d472e5-194c-421e-992d-e4ca1d08e6cc/56c61ac303ddf1b12026151f4f000a2b/dotnet-sdk-6.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/cd0d0a4d-2a6a-4d0d-b42e-dfd3b880e222/008a93f83aba6d1acf75ded3d2cfba24/dotnet-sdk-6.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index 34ef0c20de..313a3e5cb4 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/77d472e5-194c-421e-992d-e4ca1d08e6cc/56c61ac303ddf1b12026151f4f000a2b/dotnet-sdk-6.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/cd0d0a4d-2a6a-4d0d-b42e-dfd3b880e222/008a93f83aba6d1acf75ded3d2cfba24/dotnet-sdk-6.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64
index f3a7de56d7..693ee7c276 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/77d472e5-194c-421e-992d-e4ca1d08e6cc/56c61ac303ddf1b12026151f4f000a2b/dotnet-sdk-6.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/cd0d0a4d-2a6a-4d0d-b42e-dfd3b880e222/008a93f83aba6d1acf75ded3d2cfba24/dotnet-sdk-6.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf
index fa21daf669..e7765a5b27 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
mmv build-essential lsb-release
# Install dotnet repository
-RUN wget -q https://download.visualstudio.microsoft.com/download/pr/77d472e5-194c-421e-992d-e4ca1d08e6cc/56c61ac303ddf1b12026151f4f000a2b/dotnet-sdk-6.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
+RUN wget -q https://download.visualstudio.microsoft.com/download/pr/cd0d0a4d-2a6a-4d0d-b42e-dfd3b880e222/008a93f83aba6d1acf75ded3d2cfba24/dotnet-sdk-6.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
diff --git a/deployment/build.linux.amd64 b/deployment/build.linux.amd64
index a1e7f661a5..05059e4edf 100755
--- a/deployment/build.linux.amd64
+++ b/deployment/build.linux.amd64
@@ -16,7 +16,7 @@ else
fi
# Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
tar -czf jellyfin-server_${version}_linux-amd64.tar.gz -C dist jellyfin-server_${version}
rm -rf dist/jellyfin-server_${version}
diff --git a/deployment/build.linux.amd64-musl b/deployment/build.linux.amd64-musl
index 72444c05e7..0ee4b05fb3 100755
--- a/deployment/build.linux.amd64-musl
+++ b/deployment/build.linux.amd64-musl
@@ -16,7 +16,7 @@ else
fi
# Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-musl-x64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-musl-x64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
tar -czf jellyfin-server_${version}_linux-amd64-musl.tar.gz -C dist jellyfin-server_${version}
rm -rf dist/jellyfin-server_${version}
diff --git a/deployment/build.linux.arm64 b/deployment/build.linux.arm64
index e362607a76..6e36db0eb8 100755
--- a/deployment/build.linux.arm64
+++ b/deployment/build.linux.arm64
@@ -16,7 +16,7 @@ else
fi
# Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
tar -czf jellyfin-server_${version}_linux-arm64.tar.gz -C dist jellyfin-server_${version}
rm -rf dist/jellyfin-server_${version}
diff --git a/deployment/build.linux.armhf b/deployment/build.linux.armhf
index c0d0607fc7..f83eeebf1c 100755
--- a/deployment/build.linux.armhf
+++ b/deployment/build.linux.armhf
@@ -16,7 +16,7 @@ else
fi
# Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
tar -czf jellyfin-server_${version}_linux-armhf.tar.gz -C dist jellyfin-server_${version}
rm -rf dist/jellyfin-server_${version}
diff --git a/deployment/build.macos b/deployment/build.macos
index 6255c80cb2..01c640c8bc 100755
--- a/deployment/build.macos
+++ b/deployment/build.macos
@@ -16,7 +16,7 @@ else
fi
# Build archives
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
tar -czf jellyfin-server_${version}_macos-amd64.tar.gz -C dist jellyfin-server_${version}
rm -rf dist/jellyfin-server_${version}
diff --git a/deployment/build.portable b/deployment/build.portable
index a6c7418810..27e5e987fe 100755
--- a/deployment/build.portable
+++ b/deployment/build.portable
@@ -16,7 +16,7 @@ else
fi
# Build archives
-dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=false"
+dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=false
tar -czf jellyfin-server_${version}_portable.tar.gz -C dist jellyfin-server_${version}
rm -rf dist/jellyfin-server_${version}
diff --git a/deployment/build.windows.amd64 b/deployment/build.windows.amd64
index a9daa6a239..30b252beb5 100755
--- a/deployment/build.windows.amd64
+++ b/deployment/build.windows.amd64
@@ -23,7 +23,7 @@ fi
output_dir="dist/jellyfin-server_${version}"
# Build binary
-dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output ${output_dir}/ "-p:DebugSymbols=false;DebugType=none;UseAppHost=true"
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output ${output_dir}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
# Prepare addins
addin_build_dir="$( mktemp -d )"
diff --git a/fedora/jellyfin.env b/fedora/jellyfin.env
index 89cad1a2d3..1ccd8196fd 100644
--- a/fedora/jellyfin.env
+++ b/fedora/jellyfin.env
@@ -33,7 +33,7 @@ JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh"
#JELLYFIN_SERVICE_OPT="--service"
# [OPTIONAL] run Jellyfin without the web app
-#JELLYFIN_NOWEBAPP_OPT="--noautorunwebapp"
+#JELLYFIN_NOWEBAPP_OPT="--nowebclient"
# [OPTIONAL] run Jellyfin with ASP.NET Server Garbage Collection (uses more RAM and less CPU than Workstation GC)
# 0 = Workstation
diff --git a/fedora/jellyfin.override.conf b/fedora/jellyfin.override.conf
index 8652450bb4..48b4de1e9a 100644
--- a/fedora/jellyfin.override.conf
+++ b/fedora/jellyfin.override.conf
@@ -5,3 +5,49 @@
[Service]
#User = jellyfin
#EnvironmentFile = /etc/sysconfig/jellyfin
+
+# Service hardening options
+# These were added in PR #6953 to solve issue #6952, but some combination of
+# them causes "restart.sh" functionality to break with the following error:
+# sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the
+# 'nosuid' option set or an NFS file system without root privileges?
+# See issue #7503 for details on the troubleshooting that went into this.
+# Since these were added for NixOS specifically and are above and beyond
+# what 99% of systemd units do, they have been moved here as optional
+# additional flags to set for maximum system security and can be enabled at
+# the administrator's or package maintainer's discretion.
+# Uncomment these only if you know what you're doing, and doing so may cause
+# bugs with in-server Restart and potentially other functionality as well.
+#NoNewPrivileges=true
+#SystemCallArchitectures=native
+#RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
+#RestrictNamespaces=false
+#RestrictRealtime=true
+#RestrictSUIDSGID=true
+#ProtectClock=true
+#ProtectControlGroups=false
+#ProtectHostname=true
+#ProtectKernelLogs=false
+#ProtectKernelModules=false
+#ProtectKernelTunables=false
+#LockPersonality=true
+#PrivateTmp=false
+#PrivateDevices=false
+#PrivateUsers=true
+#RemoveIPC=true
+#SystemCallFilter=~@clock
+#SystemCallFilter=~@aio
+#SystemCallFilter=~@chown
+#SystemCallFilter=~@cpu-emulation
+#SystemCallFilter=~@debug
+#SystemCallFilter=~@keyring
+#SystemCallFilter=~@memlock
+#SystemCallFilter=~@module
+#SystemCallFilter=~@mount
+#SystemCallFilter=~@obsolete
+#SystemCallFilter=~@privileged
+#SystemCallFilter=~@raw-io
+#SystemCallFilter=~@reboot
+#SystemCallFilter=~@setuid
+#SystemCallFilter=~@swap
+#SystemCallErrorNumber=EPERM
diff --git a/fedora/jellyfin.service b/fedora/jellyfin.service
index 1193ddb5be..eb0d640871 100644
--- a/fedora/jellyfin.service
+++ b/fedora/jellyfin.service
@@ -13,39 +13,5 @@ Restart = on-failure
TimeoutSec = 15
SuccessExitStatus=0 143
-NoNewPrivileges=true
-SystemCallArchitectures=native
-RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
-RestrictNamespaces=false
-RestrictRealtime=true
-RestrictSUIDSGID=true
-ProtectClock=true
-ProtectControlGroups=false
-ProtectHostname=true
-ProtectKernelLogs=false
-ProtectKernelModules=false
-ProtectKernelTunables=false
-LockPersonality=true
-PrivateTmp=false
-PrivateDevices=false
-PrivateUsers=true
-RemoveIPC=true
-SystemCallFilter=~@clock
-SystemCallFilter=~@aio
-SystemCallFilter=~@chown
-SystemCallFilter=~@cpu-emulation
-SystemCallFilter=~@debug
-SystemCallFilter=~@keyring
-SystemCallFilter=~@memlock
-SystemCallFilter=~@module
-SystemCallFilter=~@mount
-SystemCallFilter=~@obsolete
-SystemCallFilter=~@privileged
-SystemCallFilter=~@raw-io
-SystemCallFilter=~@reboot
-SystemCallFilter=~@setuid
-SystemCallFilter=~@swap
-SystemCallErrorNumber=EPERM
-
[Install]
WantedBy = multi-user.target
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index 6b7967478b..a6771e3896 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -7,7 +7,7 @@
%endif
Name: jellyfin
-Version: 10.8.0
+Version: 10.9.0
Release: 1%{?dist}
Summary: The Free Software Media System
License: GPLv2
@@ -68,7 +68,7 @@ export DOTNET_CLI_TELEMETRY_OPTOUT=1
export PATH=$PATH:/usr/local/bin
# cannot use --output due to https://github.com/dotnet/sdk/issues/22220
dotnet publish --configuration Release --self-contained --runtime %{dotnet_runtime} \
- "-p:DebugSymbols=false;DebugType=none" Jellyfin.Server
+ -p:DebugSymbols=false -p:DebugType=none Jellyfin.Server
%install
@@ -176,6 +176,8 @@ fi
%systemd_postun_with_restart jellyfin.service
%changelog
+* Wed Jul 13 2022 Jellyfin Packaging Team
+- New upstream version 10.9.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.9.0
* Mon Nov 29 2021 Brian J. Murrell
- Add jellyfin-server-lowports.service drop-in in a server-lowports
subpackage to allow binding to low ports
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index 23f5d7cda0..f99cb04065 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -13,7 +13,7 @@
Jellyfin Contributors
Jellyfin.Extensions
- 10.8.0
+ 10.9.0
https://github.com/jellyfin/jellyfin
GPL-3.0-only
diff --git a/src/Jellyfin.Extensions/SplitStringExtensions.cs b/src/Jellyfin.Extensions/SplitStringExtensions.cs
index 1d1c377f56..a4dc9fc6b1 100644
--- a/src/Jellyfin.Extensions/SplitStringExtensions.cs
+++ b/src/Jellyfin.Extensions/SplitStringExtensions.cs
@@ -55,7 +55,7 @@ namespace Jellyfin.Extensions
public static Enumerator Split(this ReadOnlySpan str, char separator) => new(str, separator);
///
- /// Provides an enumerator for the substrings seperated by the separator.
+ /// Provides an enumerator for the substrings separated by the separator.
///
[StructLayout(LayoutKind.Auto)]
public ref struct Enumerator
diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs
index dadc9f1d5c..59fb038a75 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -40,7 +40,7 @@ namespace Jellyfin.Extensions
}
///
- /// Checks wether or not the specified string has diacritics in it.
+ /// Checks whether or not the specified string has diacritics in it.
///
/// The string to check.
/// True if the string has diacritics, false otherwise.
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index a08220858e..3e610ced91 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
index 3775555dec..e14850eed7 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
{
using (var stream = File.OpenRead("Test Data/example.ass"))
{
- var parsed = new AssParser(new NullLogger()).Parse(stream, CancellationToken.None);
+ var parsed = new SubtitleEditParser(new NullLogger()).Parse(stream, "ass");
Assert.Single(parsed.TrackEvents);
var trackEvent = parsed.TrackEvents[0];
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
index c07c9ea7db..0038b18736 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
@@ -15,7 +15,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
{
using (var stream = File.OpenRead("Test Data/example.srt"))
{
- var parsed = new SrtParser(new NullLogger()).Parse(stream, CancellationToken.None);
+ var parsed = new SubtitleEditParser(new NullLogger()).Parse(stream, "srt");
Assert.Equal(2, parsed.TrackEvents.Count);
var trackEvent1 = parsed.TrackEvents[0];
@@ -37,7 +37,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
{
using (var stream = File.OpenRead("Test Data/example2.srt"))
{
- var parsed = new SrtParser(new NullLogger()).Parse(stream, CancellationToken.None);
+ var parsed = new SubtitleEditParser(new NullLogger()).Parse(stream, "srt");
Assert.Equal(2, parsed.TrackEvents.Count);
var trackEvent1 = parsed.TrackEvents[0];
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
index 56649db8f8..3b9a71690c 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
@@ -13,7 +13,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
{
public class SsaParserTests
{
- private readonly SsaParser _parser = new SsaParser(new NullLogger());
+ private readonly SubtitleEditParser _parser = new SubtitleEditParser(new NullLogger());
[Theory]
[MemberData(nameof(Parse_MultipleDialogues_TestData))]
@@ -21,7 +21,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
{
using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(ssa)))
{
- SubtitleTrackInfo subtitleTrackInfo = _parser.Parse(stream, CancellationToken.None);
+ SubtitleTrackInfo subtitleTrackInfo = _parser.Parse(stream, "ssa");
Assert.Equal(expectedSubtitleTrackEvents.Count, subtitleTrackInfo.TrackEvents.Count);
@@ -76,7 +76,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
{
using (var stream = File.OpenRead("Test Data/example.ssa"))
{
- var parsed = _parser.Parse(stream, CancellationToken.None);
+ var parsed = _parser.Parse(stream, "ssa");
Assert.Single(parsed.TrackEvents);
var trackEvent = parsed.TrackEvents[0];
diff --git a/tests/Jellyfin.Model.Tests/Cryptography/PasswordHashTests.cs b/tests/Jellyfin.Model.Tests/Cryptography/PasswordHashTests.cs
index 6948280a3d..162f53e567 100644
--- a/tests/Jellyfin.Model.Tests/Cryptography/PasswordHashTests.cs
+++ b/tests/Jellyfin.Model.Tests/Cryptography/PasswordHashTests.cs
@@ -152,9 +152,9 @@ namespace Jellyfin.Model.Tests.Cryptography
[InlineData("$PBKDF2$$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Empty segment
[InlineData("$PBKDF2$iterations=1000$$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Empty salt segment
[InlineData("$PBKDF2$iterations=1000$69F420$")] // Empty hash segment
- [InlineData("$PBKDF2$=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parmeter
- [InlineData("$PBKDF2$=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parmeter
- [InlineData("$PBKDF2$iterations=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parmeter
+ [InlineData("$PBKDF2$=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parameter
+ [InlineData("$PBKDF2$=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parameter
+ [InlineData("$PBKDF2$iterations=$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] // Invalid parameter
[InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$")] // Ends on $
[InlineData("$PBKDF2$iterations=1000$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$")] // Extra segment
[InlineData("$PBKDF2$iterations=1000$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D$anotherone")] // Extra segment
diff --git a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs
index 9fcf8189f3..80c38affe1 100644
--- a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs
+++ b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs
@@ -109,26 +109,37 @@ namespace Jellyfin.Model.Tests.Entities
[InlineData(null, null, false, null)]
[InlineData(null, 0, false, null)]
[InlineData(0, null, false, null)]
- [InlineData(640, 480, false, "480p")]
- [InlineData(640, 480, true, "480i")]
- [InlineData(720, 576, false, "576p")]
- [InlineData(720, 576, true, "576i")]
+ [InlineData(256, 144, false, "144p")]
+ [InlineData(256, 144, true, "144i")]
+ [InlineData(426, 240, false, "240p")]
+ [InlineData(426, 240, true, "240i")]
+ [InlineData(640, 360, false, "360p")]
+ [InlineData(640, 360, true, "360i")]
+ [InlineData(854, 480, false, "480p")]
+ [InlineData(854, 480, true, "480i")]
[InlineData(960, 540, false, "540p")]
[InlineData(960, 540, true, "540i")]
+ [InlineData(1024, 576, false, "576p")]
+ [InlineData(1024, 576, true, "576i")]
[InlineData(1280, 720, false, "720p")]
[InlineData(1280, 720, true, "720i")]
- [InlineData(1920, 1080, false, "1080p")]
- [InlineData(1920, 1080, true, "1080i")]
+ [InlineData(2560, 1080, false, "1080p")]
+ [InlineData(2560, 1080, true, "1080i")]
[InlineData(4096, 3072, false, "4K")]
[InlineData(8192, 6144, false, "8K")]
- [InlineData(512, 384, false, "480p")]
- [InlineData(576, 336, false, "480p")]
- [InlineData(624, 352, false, "480p")]
- [InlineData(640, 352, false, "480p")]
- [InlineData(704, 396, false, "480p")]
- [InlineData(720, 404, false, "480p")]
+ [InlineData(512, 384, false, "384p")]
+ [InlineData(576, 336, false, "360p")]
+ [InlineData(576, 336, true, "360i")]
+ [InlineData(624, 352, false, "360p")]
+ [InlineData(640, 352, false, "360p")]
+ [InlineData(640, 480, false, "480p")]
+ [InlineData(704, 396, false, "404p")]
+ [InlineData(720, 404, false, "404p")]
[InlineData(720, 480, false, "480p")]
+ [InlineData(720, 576, false, "576p")]
[InlineData(768, 576, false, "576p")]
+ [InlineData(960, 544, false, "540p")]
+ [InlineData(960, 544, true, "540i")]
[InlineData(960, 720, false, "720p")]
[InlineData(1280, 528, false, "720p")]
[InlineData(1280, 532, false, "720p")]
@@ -140,6 +151,11 @@ namespace Jellyfin.Model.Tests.Entities
[InlineData(1280, 696, false, "720p")]
[InlineData(1280, 716, false, "720p")]
[InlineData(1280, 718, false, "720p")]
+ [InlineData(1920, 1080, false, "1080p")]
+ [InlineData(1440, 1070, false, "1080p")]
+ [InlineData(1440, 1072, false, "1080p")]
+ [InlineData(1440, 1080, false, "1080p")]
+ [InlineData(1440, 1440, false, "1080p")]
[InlineData(1912, 792, false, "1080p")]
[InlineData(1916, 1076, false, "1080p")]
[InlineData(1918, 1080, false, "1080p")]
@@ -153,14 +169,16 @@ namespace Jellyfin.Model.Tests.Entities
[InlineData(1920, 960, false, "1080p")]
[InlineData(1920, 1024, false, "1080p")]
[InlineData(1920, 1040, false, "1080p")]
+ [InlineData(1920, 1070, false, "1080p")]
[InlineData(1920, 1072, false, "1080p")]
- [InlineData(1440, 1072, false, "1080p")]
- [InlineData(1440, 1080, false, "1080p")]
+ [InlineData(1920, 1440, false, "1080p")]
[InlineData(3840, 1600, false, "4K")]
[InlineData(3840, 1606, false, "4K")]
[InlineData(3840, 1608, false, "4K")]
[InlineData(3840, 2160, false, "4K")]
+ [InlineData(4090, 3070, false, "4K")]
[InlineData(7680, 4320, false, "8K")]
+ [InlineData(8190, 6140, false, "8K")]
public void GetResolutionText_Valid(int? width, int? height, bool interlaced, string expected)
{
var mediaStream = new MediaStream()
diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
index 6b93974373..52b0e5a955 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
@@ -393,7 +393,7 @@ namespace Jellyfin.Networking.Tests
// User on external network, internal binding only - so assumption is a proxy forward, return external override.
[InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
- // User on external network, no binding - so result is the 1st external which is overriden.
+ // User on external network, no binding - so result is the 1st external which is overridden.
[InlineData("jellyfin.org", "192.168.1.0/24", "", false, "0.0.0.0 = http://helloworld.com", "http://helloworld.com")]
// User assumed to be internal, no binding - so result is the 1st internal.
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs
index 3b3e38bd1f..e1d2bb2d58 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs
@@ -18,7 +18,7 @@ namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect
}
///
- /// /token reponse.
+ /// /token response.
///
[Fact]
public void Deserialize_Token_Response_Live_Success()
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json
index b766e668e3..fa8fbd8d2c 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/Updates/manifest-stable.json
@@ -253,7 +253,7 @@
"versions": [
{
"version": "5.0.0.0",
- "changelog": "Updated to use NextPVR API v5, no longer compatable with API v4.\n",
+ "changelog": "Updated to use NextPVR API v5, no longer compatible with API v4.\n",
"targetAbi": "10.7.0.0",
"sourceUrl": "https://repo.jellyfin.org/releases/plugin/nextpvr/nextpvr_5.0.0.0.zip",
"checksum": "d70f694d14bf9462ba2b2ebe110068d3",
diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
index 07c5e94ded..9d6776b072 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
index 3e445e012a..f19e330615 100644
--- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
+++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
@@ -10,7 +10,7 @@
-
+