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..8daaae4d94 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -36,6 +36,7 @@
- [dmitrylyzo](https://github.com/dmitrylyzo)
- [DMouse10462](https://github.com/DMouse10462)
- [DrPandemic](https://github.com/DrPandemic)
+ - [eglia](https://github.com/eglia)
- [EraYaN](https://github.com/EraYaN)
- [escabe](https://github.com/escabe)
- [excelite](https://github.com/excelite)
@@ -147,6 +148,7 @@
- [xosdy](https://github.com/xosdy)
- [XVicarious](https://github.com/XVicarious)
- [YouKnowBlom](https://github.com/YouKnowBlom)
+ - [ZachPhelan](https://github.com/ZachPhelan)
- [KristupasSavickas](https://github.com/KristupasSavickas)
- [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
@@ -157,6 +159,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 +228,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..219b958935 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -31,7 +31,7 @@ ARG LEVEL_ZERO_VERSION=1.3.22549
# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
# curl: healthcheck
RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https curl \
+ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget curl \
&& wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
&& echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
&& apt-get update \
@@ -53,7 +53,7 @@ RUN apt-get update \
&& dpkg -i *.deb \
&& cd .. \
&& rm -rf intel-compute-runtime \
- && apt-get remove gnupg wget apt-transport-https -y \
+ && apt-get remove gnupg wget -y \
&& apt-get clean autoclean -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \
@@ -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..8e0ba7af53 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -38,9 +38,6 @@ RUN apt-get update \
libssl-dev \
libfontconfig1 \
libfreetype6 \
- libomxil-bellagio0 \
- libomxil-bellagio-bin \
- libraspberrypi0 \
vainfo \
libva2 \
locales \
@@ -64,7 +61,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/AudioBook/AudioBookFilePathParserResult.cs b/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
index 48ab8b57dc..ae8c8a39bc 100644
--- a/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
+++ b/Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
@@ -3,7 +3,7 @@ namespace Emby.Naming.AudioBook
///
/// Data object for passing result of audiobook part/chapter extraction.
///
- public struct AudioBookFilePathParserResult
+ public record struct AudioBookFilePathParserResult
{
///
/// Gets or sets optional number of path extracted from audiobook filename.
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/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 2a4a8fb132..c42cec593a 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -365,11 +365,7 @@ namespace Emby.Server.Implementations.AppBase
validatingStore.Validate(currentConfiguration, configuration);
}
- NamedConfigurationUpdating?.Invoke(this, new ConfigurationUpdateEventArgs
- {
- Key = key,
- NewConfiguration = configuration
- });
+ NamedConfigurationUpdating?.Invoke(this, new ConfigurationUpdateEventArgs(key, configuration));
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
@@ -391,11 +387,7 @@ namespace Emby.Server.Implementations.AppBase
/// The old configuration.
protected virtual void OnNamedConfigurationUpdated(string key, object configuration)
{
- NamedConfigurationUpdated?.Invoke(this, new ConfigurationUpdateEventArgs
- {
- Key = key,
- NewConfiguration = configuration
- });
+ NamedConfigurationUpdated?.Invoke(this, new ConfigurationUpdateEventArgs(key, configuration));
}
///
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 56f997b3b5..dc214da876 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();
@@ -1233,5 +1235,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/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 09ba368514..3d2b8f7f63 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.People))
{
- AttachPeople(dto, item);
+ AttachPeople(dto, item, user);
}
if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
@@ -503,7 +503,8 @@ namespace Emby.Server.Implementations.Dto
///
/// The dto.
/// The item.
- private void AttachPeople(BaseItemDto dto, BaseItem item)
+ /// The requesting user.
+ private void AttachPeople(BaseItemDto dto, BaseItem item, User user = null)
{
// Ordering by person type to ensure actors and artists are at the front.
// This is taking advantage of the fact that they both begin with A
@@ -560,6 +561,9 @@ namespace Emby.Server.Implementations.Dto
return null;
}
}).Where(i => i != null)
+ .Where(i => user == null ?
+ true :
+ i.IsVisible(user))
.GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.Select(x => x.First())
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index cd24cd8729..2792a4c7c5 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/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
index 15ab363fe6..e0d20e2100 100644
--- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
@@ -1,6 +1,5 @@
#pragma warning disable CS1591
-using System;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index b87f1bc226..d095248fab 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -11,7 +11,6 @@ using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session;
-using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.HttpServer
@@ -19,7 +18,7 @@ namespace Emby.Server.Implementations.HttpServer
///
/// Class WebSocketConnection.
///
- public class WebSocketConnection : IWebSocketConnection, IDisposable
+ public class WebSocketConnection : IWebSocketConnection
{
///
/// The logger.
@@ -36,6 +35,8 @@ namespace Emby.Server.Implementations.HttpServer
///
private readonly WebSocket _socket;
+ private bool _disposed = false;
+
///
/// Initializes a new instance of the class.
///
@@ -244,10 +245,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..67f9c57658 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -46,7 +46,6 @@ using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Caching.Memory;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
@@ -2453,6 +2452,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 +2528,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;
@@ -2760,7 +2765,8 @@ namespace Emby.Server.Implementations.Library
public List GetPeopleItems(InternalPeopleQuery query)
{
- return _itemRepository.GetPeopleNames(query).Select(i =>
+ return _itemRepository.GetPeopleNames(query)
+ .Select(i =>
{
try
{
@@ -2771,7 +2777,12 @@ namespace Emby.Server.Implementations.Library
_logger.LogError(ex, "Error getting person");
return null;
}
- }).Where(i => i != null).ToList();
+ })
+ .Where(i => i != null)
+ .Where(i => query.User == null ?
+ true :
+ i.IsVisible(query.User))
+ .ToList();
}
public List GetPeopleNames(InternalPeopleQuery query)
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/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 2753cf1778..065309688c 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -995,7 +995,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
- throw new Exception("Tuner not found.");
+ throw new ResourceNotFoundException("Tuner not found.");
}
public async Task> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index 6e05598410..08534de59d 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -13,6 +13,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
+using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -297,7 +298,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
else
{
_taskCompletionSource.TrySetException(
- new Exception(
+ new FfmpegException(
string.Format(
CultureInfo.InvariantCulture,
"Recording for {0} failed. Exit code {1}",
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index ffa0d9b6a0..4311db28d2 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -20,6 +20,7 @@ using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@@ -591,13 +592,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
catch (HttpRequestException ex)
{
- if (ex.StatusCode.HasValue)
+ if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
{
- if ((int)ex.StatusCode.Value == 400)
- {
- _tokens.Clear();
- _lastErrorResponse = DateTime.UtcNow;
- }
+ _tokens.Clear();
+ _lastErrorResponse = DateTime.UtcNow;
}
throw;
@@ -662,7 +660,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return root.Token;
}
- throw new Exception("Could not authenticate with Schedules Direct Error: " + root.Message);
+ throw new AuthenticationException("Could not authenticate with Schedules Direct Error: " + root.Message);
}
private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken)
@@ -697,7 +695,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
if (string.IsNullOrEmpty(token))
{
- throw new Exception("token required");
+ throw new ArgumentException("token required");
}
_logger.LogInformation("Headends on account ");
@@ -768,14 +766,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
var listingsId = info.ListingsId;
if (string.IsNullOrEmpty(listingsId))
{
- throw new Exception("ListingsId required");
+ throw new ArgumentException("ListingsId required");
}
var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(token))
{
- throw new Exception("token required");
+ throw new ArgumentException("token required");
}
using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId);
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/et.json b/Emby.Server.Implementations/Localization/Core/et.json
index 8db6a0b388..da44e53d00 100644
--- a/Emby.Server.Implementations/Localization/Core/et.json
+++ b/Emby.Server.Implementations/Localization/Core/et.json
@@ -119,5 +119,6 @@
"SubtitleDownloadFailureFromForItem": "Subtiitrite allalaadimine {0} > {1} nurjus",
"UserPolicyUpdatedWithName": "Kasutaja {0} õigusi värskendati",
"UserStoppedPlayingItemWithValues": "{0} lõpetas {1} taasesituse seadmes {2}",
- "UserOnlineFromDevice": "{0} on ühendatud seadmest {1}"
+ "UserOnlineFromDevice": "{0} on ühendatud seadmest {1}",
+ "External": "Väline"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 46f637050a..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",
@@ -93,33 +93,33 @@
"ValueSpecialEpisodeName": "Spécial - {0}",
"VersionNumber": "Version {0}",
"TasksChannelsCategory": "Chaînes en ligne",
- "TaskDownloadMissingSubtitlesDescription": "Recherche les sous-titres manquants sur internet en se basant sur la configuration des métadonnées.",
+ "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 chaînes en ligne.",
- "TaskRefreshChannels": "Rafraîchir les chaînes",
+ "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 ou 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/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index d2b5122b26..c63cd2b94f 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -122,5 +122,6 @@
"TaskOptimizeDatabase": "Optimiziraj bazu podataka",
"External": "Vanjski",
"TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.",
- "TaskKeyframeExtractor": "Izvoditelj ključnog okvira"
+ "TaskKeyframeExtractor": "Izvoditelj ključnog okvira",
+ "TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka."
}
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/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index 443a74a10f..e460fd7197 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -84,7 +84,7 @@
"CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}",
"Books": "Grāmatas",
"Artists": "Izpildītāji",
- "Albums": "Albumi",
+ "Albums": "Albūmi",
"ProviderValue": "Provider: {0}",
"HeaderFavoriteSongs": "Dziesmu Favorīti",
"HeaderFavoriteShows": "Raidījumu Favorīti",
@@ -117,7 +117,8 @@
"TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
"TaskCleanActivityLog": "Notīrīt Darbību Žurnālu",
"Undefined": "Nenoteikts",
- "Default": "Noklusējums",
+ "Default": "Noklusējuma",
"TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
- "TaskOptimizeDatabase": "Optimizēt datubāzi"
+ "TaskOptimizeDatabase": "Optimizēt datubāzi",
+ "External": "Ārējais"
}
diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json
index 279734c5ee..cbccad87ff 100644
--- a/Emby.Server.Implementations/Localization/Core/mk.json
+++ b/Emby.Server.Implementations/Localization/Core/mk.json
@@ -64,9 +64,9 @@
"CameraImageUploadedFrom": "Нова слика од камера беше поставена од {0}",
"Books": "Книги",
"AuthenticationSucceededWithUserName": "{0} успешно поврзан",
- "Artists": "Изведувач",
+ "Artists": "Изведувачи",
"Application": "Апликација",
- "AppDeviceValues": "Аплиакција: {0}, Уред: {1}",
+ "AppDeviceValues": "Апликација: {0}, Уред: {1}",
"Albums": "Албуми",
"VersionNumber": "Верзија {0}",
"ValueSpecialEpisodeName": "Специјално - {0}",
@@ -100,5 +100,27 @@
"TasksMaintenanceCategory": "Одржување",
"Undefined": "Недефинирано",
"Forced": "Принудно",
- "Default": "Зададено"
+ "Default": "Зададено",
+ "TaskKeyframeExtractorDescription": "Извлекува клучни рамки од видео фајлови за да се направат попрецизни HLS плејлисти. Оваа задача може да работи многу долго време.",
+ "TaskKeyframeExtractor": "Извлекувач на клучни рамки",
+ "TaskOptimizeDatabaseDescription": "Компактира датабазата и смалува празното место. Извршувањето на оваа задача по скенирање на библиотеката или правење други промени што прават модификации на датабазата може да подобри перформансите.",
+ "TaskOptimizeDatabase": "Оптимизирај датабаза",
+ "TaskDownloadMissingSubtitlesDescription": "Пребарува интернет за преводи што недостиваат според метадата конфигурација.",
+ "TaskDownloadMissingSubtitles": "Симни преводи што недостигаат",
+ "TaskRefreshChannelsDescription": "Ажурирај информации за интернет канали.",
+ "TaskRefreshChannels": "Ажурирај Канали",
+ "TaskCleanTranscodeDescription": "Избриши транскодирани фајлови постари од еден ден.",
+ "TaskCleanTranscode": "Исчисти Директориум за Транскодирање",
+ "TaskUpdatePluginsDescription": "Симни и инсталирај ажурирања за плагини што се конфигурирани за автоматско ажурирање.",
+ "TaskUpdatePlugins": "Ажурирај Плагини",
+ "TaskRefreshPeopleDescription": "Ажурирај метадата за акери и директори во вашата медиска библиотека.",
+ "TaskRefreshPeople": "Ажурирајте ги Луѓето",
+ "TaskCleanLogsDescription": "Избриши лог фајлови постари од {0} денови.",
+ "TaskCleanLogs": "Избриши Директориум на Логови",
+ "TaskRefreshLibraryDescription": "Скенирајте ја вашата медиска библиотека за нови фајлови и ажурирај метадата.",
+ "TaskRefreshLibrary": "Скенирај Медиумска Библиотека",
+ "TaskRefreshChapterImagesDescription": "Создава тамбнеил за видеата шти имаат поглавја.",
+ "TaskCleanActivityLogDescription": "Избришува логови на активности постари од определеното време.",
+ "TaskCleanActivityLog": "Избриши Лог на Активности",
+ "External": "Надворешен"
}
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/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index 44600374b1..7047f1c282 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -8,15 +8,15 @@
"CameraImageUploadedFrom": "Uma nova imagem de câmara foi enviada a partir de {0}",
"Channels": "Canais",
"ChapterNameValue": "Capítulo {0}",
- "Collections": "Coleções",
+ "Collections": "Colecções",
"DeviceOfflineWithName": "{0} desligou-se",
"DeviceOnlineWithName": "{0} ligou-se",
"FailedLoginAttemptWithUserName": "Tentativa de login falhada a partir de {0}",
"Favorites": "Favoritos",
"Folders": "Pastas",
"Genres": "Géneros",
- "HeaderAlbumArtists": "Artistas do Álbum",
- "HeaderContinueWatching": "Continuar a Ver",
+ "HeaderAlbumArtists": "Artistas do álbum",
+ "HeaderContinueWatching": "Continuar a ver",
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos",
"HeaderFavoriteEpisodes": "Episódios Favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 69bc4c90f7..c2c77ccab1 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -1,5 +1,5 @@
{
- "HeaderLiveTV": "TV Ao Vivo",
+ "HeaderLiveTV": "TV Em Direto",
"Collections": "Coleções",
"Books": "Livros",
"Artists": "Artistas",
@@ -10,9 +10,9 @@
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteEpisodes": "Episódios Favoritos",
"HeaderFavoriteShows": "Séries Favoritas",
- "HeaderContinueWatching": "Continuar assistindo",
+ "HeaderContinueWatching": "Continuar a ver",
"HeaderAlbumArtists": "Artistas do Álbum",
- "Genres": "Gêneros",
+ "Genres": "Géneros",
"Folders": "Diretórios",
"Favorites": "Favoritos",
"Channels": "Canais",
@@ -74,7 +74,7 @@
"ItemRemovedWithName": "{0} foi removido da biblioteca",
"ItemAddedWithName": "{0} foi adicionado à biblioteca",
"Inherit": "Herdar",
- "HomeVideos": "Vídeos principais",
+ "HomeVideos": "Vídeos Caseiros",
"HeaderRecordingGroups": "Grupos de Gravação",
"ValueSpecialEpisodeName": "Episódio Especial - {0}",
"Sync": "Sincronização",
@@ -83,14 +83,14 @@
"Playlists": "Listas de Reprodução",
"Photos": "Fotografias",
"Movies": "Filmes",
- "FailedLoginAttemptWithUserName": "Tentativa falha de login a partir de {0}",
- "DeviceOnlineWithName": "{0} está conectado",
- "DeviceOfflineWithName": "{0} desconectou-se",
+ "FailedLoginAttemptWithUserName": "Tentativa de início de sessão falhada a partir de {0}",
+ "DeviceOnlineWithName": "{0} está ligado",
+ "DeviceOfflineWithName": "{0} desligou-se",
"ChapterNameValue": "Capítulo {0}",
"CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
- "Application": "Aplicativo",
- "AppDeviceValues": "Aplicativo {0}, Dispositivo: {1}",
+ "Application": "Aplicação",
+ "AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
"TaskCleanCache": "Limpar Diretório de Cache",
"TasksApplicationCategory": "Aplicativo",
"TasksLibraryCategory": "Biblioteca",
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index 30b24e9f0d..d845accac2 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -90,7 +90,7 @@
"UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
- "ValueSpecialEpisodeName": "Bonus - {0}",
+ "ValueSpecialEpisodeName": "Posebna epizoda - {0}",
"VersionNumber": "Različica {0}",
"TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise",
"TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.",
@@ -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..1be8867f47 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -86,7 +86,7 @@
"Channels": "Канали",
"CameraImageUploadedFrom": "Нова фотографија је учитана са {0}",
"Books": "Књиге",
- "AuthenticationSucceededWithUserName": "{0} Успешна аутентикација",
+ "AuthenticationSucceededWithUserName": "{0} Успешна аутентификација",
"Artists": "Извођачи",
"Application": "Апликација",
"AppDeviceValues": "Апликација: {0}, Уређај: {1}",
@@ -118,7 +118,9 @@
"Undefined": "Недефинисано",
"Forced": "Принудно",
"Default": "Подразумевано",
- "TaskOptimizeDatabase": "Оптимизуј датабазу",
+ "TaskOptimizeDatabase": "Оптимизуј банку података",
"TaskOptimizeDatabaseDescription": "Сажима базу података и скраћује слободан простор. Покретање овог задатка након скенирања библиотеке или других промена које подразумевају измене базе података које могу побољшати перформансе.",
- "External": "Спољно"
+ "External": "Спољно",
+ "TaskKeyframeExtractorDescription": "Екстрактује кљулне сличице из видео датотека да би креирао више преицзну HLS плеј-листу. Овај задатак може да потраје дуже време.",
+ "TaskKeyframeExtractor": "Екстрактор кључних сличица"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ug.json b/Emby.Server.Implementations/Localization/Core/ug.json
index aea93c7fa4..509a6fc8d9 100644
--- a/Emby.Server.Implementations/Localization/Core/ug.json
+++ b/Emby.Server.Implementations/Localization/Core/ug.json
@@ -3,7 +3,16 @@
"Channels": "قانال",
"CameraImageUploadedFrom": "{0} ئورۇندىن يېڭى سۈرەت چىقىرىلدى",
"Books": "كىتاب",
- "AuthenticationSucceededWithUserName": "تىزىملىتىش مۇۋەپپەقىيەتلىك بول",
+ "AuthenticationSucceededWithUserName": "{0} تەستىقلاش مۇۋاپىقىيەتلىك بولدى",
"Artists": "سەنئەتكار",
- "Albums": "پىلاستىنكا"
+ "Albums": "پىلاستىنكا",
+ "DeviceOnlineWithName": "{0} ئۇلاندى",
+ "DeviceOfflineWithName": "{0} ئۈزۈلدى",
+ "Collections": "توپلام",
+ "Application": "ئەپ",
+ "AppDeviceValues": "ئەپ: {0}، ئۈسكۈنە: {1}",
+ "HeaderLiveTV": "تور تېلېۋىزىيەسى",
+ "Default": "سۈكۈتتىكى",
+ "Folders": "ھۆججەت خالتىسى",
+ "Favorites": "ساقلىغۇچ"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index ac74da67d1..6c8bf76274 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -120,5 +120,8 @@
"Default": "預設",
"TaskOptimizeDatabaseDescription": "壓縮數據庫並截斷可用空間。在掃描媒體庫或執行其他數據庫的修改後運行此任務可能會提高性能。",
"TaskOptimizeDatabase": "最佳化數據庫",
- "TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。"
+ "TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。",
+ "TaskKeyframeExtractorDescription": "提取關鍵格以創建更準確的HLS播放列表。次指示可能用時很長。",
+ "TaskKeyframeExtractor": "關鍵幀提取器",
+ "External": "外部"
}
diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.csv b/Emby.Server.Implementations/Localization/Ratings/fi.csv
new file mode 100644
index 0000000000..782785890f
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/fi.csv
@@ -0,0 +1,10 @@
+FI-S,1
+FI-T,1
+FI-7,4
+FI-12,5
+FI-16,8
+FI-18,9
+FI-K7,4
+FI-K12,5
+FI-K16,8
+FI-K18,9
diff --git a/Emby.Server.Implementations/Localization/Ratings/no.csv b/Emby.Server.Implementations/Localization/Ratings/no.csv
new file mode 100644
index 0000000000..127407be86
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/no.csv
@@ -0,0 +1,6 @@
+NO-A,1
+NO-6,3
+NO-9,4
+NO-12,5
+NO-15,8
+NO-18,9
diff --git a/Emby.Server.Implementations/Localization/Ratings/se.csv b/Emby.Server.Implementations/Localization/Ratings/se.csv
new file mode 100644
index 0000000000..1443c07df7
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/se.csv
@@ -0,0 +1,5 @@
+SE-Btl,1
+SE-Barntillåten,1
+SE-7,3
+SE-11,5
+SE-15,8
diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs
index bbbca4fc08..2c20daa57f 100644
--- a/Emby.Server.Implementations/Net/UdpSocket.cs
+++ b/Emby.Server.Implementations/Net/UdpSocket.cs
@@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.Net
}
else
{
- tcs.TrySetException(new Exception("SocketError: " + e.SocketError));
+ tcs.TrySetException(new SocketException((int)e.SocketError));
}
}
}
@@ -114,7 +114,7 @@ namespace Emby.Server.Implementations.Net
}
else
{
- tcs.TrySetException(new Exception("SocketError: " + e.SocketError));
+ tcs.TrySetException(new SocketException((int)e.SocketError));
}
}
}
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..35fd5caae7 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -6,7 +6,6 @@ using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Net;
@@ -37,7 +36,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/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs
index 4d89cfa8b2..c917daaad1 100644
--- a/Emby.Server.Implementations/Sorting/StudioComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs
@@ -3,7 +3,6 @@
#pragma warning disable CS1591
using System;
-using System.Linq;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index 727b9d4b5c..6005896ad9 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;
@@ -135,25 +135,20 @@ namespace Emby.Server.Implementations.TV
return GetResult(episodes, request);
}
- public IEnumerable GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList seriesKeys, DtoOptions dtoOptions)
+ private IEnumerable GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList seriesKeys, DtoOptions dtoOptions)
{
- // Avoid implicitly captured closure
- var currentUser = user;
-
- var allNextUp = seriesKeys
- .Select(i => GetNextUp(i, currentUser, dtoOptions, false));
+ var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false));
if (request.EnableRewatching)
{
allNextUp = allNextUp.Concat(
- seriesKeys.Select(i => GetNextUp(i, currentUser, dtoOptions, true))
- )
- .OrderByDescending(i => i.Item1);
+ seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, true)))
+ .OrderByDescending(i => i.LastWatchedDate);
}
// 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
@@ -161,23 +156,18 @@ namespace Emby.Server.Implementations.TV
{
if (request.DisableFirstEpisode)
{
- return i.Item1 != DateTime.MinValue;
+ return i.LastWatchedDate != DateTime.MinValue;
}
- if (alwaysEnableFirstEpisode || (i.Item1 != DateTime.MinValue && i.Item1.Date >= request.NextUpDateCutoff))
+ if (alwaysEnableFirstEpisode || (i.LastWatchedDate != DateTime.MinValue && i.LastWatchedDate.Date >= request.NextUpDateCutoff))
{
anyFound = true;
return true;
}
- if (!anyFound && i.Item1 == DateTime.MinValue)
- {
- return true;
- }
-
- return false;
+ return !anyFound && i.LastWatchedDate == DateTime.MinValue;
})
- .Select(i => i.Item2())
+ .Select(i => i.GetEpisodeFunction())
.Where(i => i != null);
}
@@ -195,14 +185,14 @@ namespace Emby.Server.Implementations.TV
/// Gets the next up.
///
/// Task{Episode}.
- private Tuple> GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
+ private (DateTime LastWatchedDate, Func GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
{
var lastQuery = new InternalItemsQuery(user)
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Episode },
- OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Descending) },
+ OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) },
IsPlayed = true,
Limit = 1,
ParentIndexNumberNotEquals = 0,
@@ -216,24 +206,23 @@ namespace Emby.Server.Implementations.TV
if (rewatching)
{
// find last watched by date played, not by newest episode watched
- lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) };
+ lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
}
var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast().FirstOrDefault();
- Func getEpisode = () =>
+ Episode GetEpisode()
{
var nextQuery = new InternalItemsQuery(user)
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Episode },
- OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
+ OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
Limit = 1,
IsPlayed = rewatching,
IsVirtualItem = false,
ParentIndexNumberNotEquals = 0,
- MinSortName = lastWatchedEpisode?.SortName,
DtoOptions = dtoOptions
};
@@ -297,7 +286,7 @@ namespace Emby.Server.Implementations.TV
}
return nextEpisode;
- };
+ }
if (lastWatchedEpisode != null)
{
@@ -305,11 +294,11 @@ namespace Emby.Server.Implementations.TV
var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
- return new Tuple>(lastWatchedDate, getEpisode);
+ return (lastWatchedDate, GetEpisode);
}
// Return the first episode
- return new Tuple>(DateTime.MinValue, getEpisode);
+ return (DateTime.MinValue, GetEpisode);
}
private static QueryResult GetResult(IEnumerable items, NextUpQuery query)
diff --git a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
index af8727552c..7ac089a344 100644
--- a/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
+++ b/Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
@@ -25,11 +25,6 @@ namespace Jellyfin.Api.Attributes
/// The route template. May not be null.
public HttpSubscribeAttribute(string template)
: base(_supportedMethods, template)
- {
- if (template == null)
- {
- throw new ArgumentNullException(nameof(template));
- }
- }
+ => ArgumentNullException.ThrowIfNull(template, nameof(template));
}
}
diff --git a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
index 1c0b70e719..16b3d08160 100644
--- a/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
+++ b/Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
@@ -25,11 +25,6 @@ namespace Jellyfin.Api.Attributes
/// The route template. May not be null.
public HttpUnsubscribeAttribute(string template)
: base(_supportedMethods, template)
- {
- if (template == null)
- {
- throw new ArgumentNullException(nameof(template));
- }
- }
+ => ArgumentNullException.ThrowIfNull(template, nameof(template));
}
}
diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs
index 59d6b75137..0c63d24b70 100644
--- a/Jellyfin.Api/BaseJellyfinApiController.cs
+++ b/Jellyfin.Api/BaseJellyfinApiController.cs
@@ -1,4 +1,6 @@
+using System.Collections.Generic;
using System.Net.Mime;
+using Jellyfin.Api.Results;
using Jellyfin.Extensions.Json;
using Microsoft.AspNetCore.Mvc;
@@ -15,5 +17,40 @@ namespace Jellyfin.Api
JsonDefaults.PascalCaseMediaType)]
public class BaseJellyfinApiController : ControllerBase
{
+ ///
+ /// Create a new .
+ ///
+ /// The value to return.
+ /// The type to return.
+ /// The .
+ protected ActionResult> Ok(List value)
+ => new OkResult>(value);
+
+ ///
+ /// Create a new .
+ ///
+ /// The value to return.
+ /// The type to return.
+ /// The .
+ protected ActionResult> Ok(IReadOnlyList value)
+ => new OkResult>(value);
+
+ ///
+ /// Create a new .
+ ///
+ /// The value to return.
+ /// The type to return.
+ /// The .
+ protected ActionResult> Ok(IEnumerable? value)
+ => new OkResult?>(value);
+
+ ///
+ /// Create a new .
+ ///
+ /// The value to return.
+ /// The type to return.
+ /// The .
+ protected ActionResult Ok(T value)
+ => new OkResult(value);
}
}
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/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index 464fadc060..bbe1633120 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -2,7 +2,6 @@ using System;
using System.ComponentModel.DataAnnotations;
using System.Net.Mime;
using System.Text.Json;
-using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.ConfigurationDtos;
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 27eb223390..64ee5680ce 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -89,12 +89,9 @@ namespace Jellyfin.Api.Controllers
// Load all custom display preferences
var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
- if (customDisplayPreferences != null)
+ foreach (var (key, value) in customDisplayPreferences)
{
- foreach (var (key, value) in customDisplayPreferences)
- {
- dto.CustomPrefs.TryAdd(key, value);
- }
+ dto.CustomPrefs.TryAdd(key, value);
}
// This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 401c0197ac..8859d60207 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -54,7 +54,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
- public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
+ public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
{
var url = GetAbsoluteUri();
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
@@ -77,7 +77,7 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
+ public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
{
return Ok(_contentDirectory.GetServiceXml());
}
@@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
+ public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
{
return Ok(_mediaReceiverRegistrar.GetServiceXml());
}
@@ -117,7 +117,7 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
- public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
+ public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
{
return Ok(_connectionManager.GetServiceXml());
}
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/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index 75df18204d..d2852ed011 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -12,7 +12,6 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.MediaInfo;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs
index 420dd99233..4669447049 100644
--- a/Jellyfin.Api/Controllers/MoviesController.cs
+++ b/Jellyfin.Api/Controllers/MoviesController.cs
@@ -170,7 +170,7 @@ namespace Jellyfin.Api.Controllers
}
}
- return Ok(categories.OrderBy(i => i.RecommendationType));
+ return Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable());
}
private IEnumerable GetWithDirector(
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index be4b9eded5..33f1aea394 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -98,7 +98,10 @@ namespace Jellyfin.Api.Controllers
Limit = limit ?? 0
});
- return new QueryResult(peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray());
+ return new QueryResult(
+ peopleItems
+ .Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user))
+ .ToArray());
}
///
diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs
index 87b78fe93f..1df26355f0 100644
--- a/Jellyfin.Api/Controllers/QuickConnectController.cs
+++ b/Jellyfin.Api/Controllers/QuickConnectController.cs
@@ -39,7 +39,7 @@ namespace Jellyfin.Api.Controllers
/// Whether Quick Connect is enabled on the server or not.
[HttpGet("Enabled")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult GetEnabled()
+ public ActionResult GetQuickConnectEnabled()
{
return _quickConnect.IsEnabled;
}
@@ -52,7 +52,7 @@ namespace Jellyfin.Api.Controllers
/// A with a secret and code for future use or an error message.
[HttpGet("Initiate")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task> Initiate()
+ public async Task> InitiateQuickConnect()
{
try
{
@@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Connect")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult Connect([FromQuery, Required] string secret)
+ public ActionResult GetQuickConnectState([FromQuery, Required] string secret)
{
try
{
@@ -102,7 +102,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task> Authorize([FromQuery, Required] string code)
+ public async Task> AuthorizeQuickConnect([FromQuery, Required] string code)
{
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
if (!userId.HasValue)
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/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 6fcafd426c..c463fb08aa 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -10,13 +10,11 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -31,7 +29,6 @@ namespace Jellyfin.Api.Controllers
public class UniversalAudioController : BaseJellyfinApiController
{
private readonly IAuthorizationContext _authorizationContext;
- private readonly IDeviceManager _deviceManager;
private readonly ILibraryManager _libraryManager;
private readonly ILogger _logger;
private readonly MediaInfoHelper _mediaInfoHelper;
@@ -42,7 +39,6 @@ namespace Jellyfin.Api.Controllers
/// Initializes a new instance of the class.
///
/// Instance of the interface.
- /// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
/// Instance of .
@@ -50,7 +46,6 @@ namespace Jellyfin.Api.Controllers
/// Instance of .
public UniversalAudioController(
IAuthorizationContext authorizationContext,
- IDeviceManager deviceManager,
ILibraryManager libraryManager,
ILogger logger,
MediaInfoHelper mediaInfoHelper,
@@ -58,7 +53,6 @@ namespace Jellyfin.Api.Controllers
DynamicHlsHelper dynamicHlsHelper)
{
_authorizationContext = authorizationContext;
- _deviceManager = deviceManager;
_libraryManager = libraryManager;
_logger = logger;
_mediaInfoHelper = mediaInfoHelper;
@@ -117,76 +111,61 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool enableRedirection = true)
{
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
- (await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId = deviceId;
+ var authorizationInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
+ authorizationInfo.DeviceId = deviceId;
+
+ if (!userId.HasValue || userId.Value.Equals(Guid.Empty))
+ {
+ userId = authorizationInfo.UserId;
+ }
var authInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
- if (deviceProfile == null)
- {
- var clientCapabilities = _deviceManager.GetCapabilities(authInfo.DeviceId);
- if (clientCapabilities != null)
- {
- deviceProfile = clientCapabilities.DeviceProfile;
- }
- }
-
var info = await _mediaInfoHelper.GetPlaybackInfo(
itemId,
userId,
mediaSourceId)
.ConfigureAwait(false);
- if (deviceProfile != null)
+ // set device specific data
+ var item = _libraryManager.GetItemById(itemId);
+
+ foreach (var sourceInfo in info.MediaSources)
{
- // set device specific data
- var item = _libraryManager.GetItemById(itemId);
-
- foreach (var sourceInfo in info.MediaSources)
- {
- _mediaInfoHelper.SetDeviceSpecificData(
- item,
- sourceInfo,
- deviceProfile,
- authInfo,
- maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
- startTimeTicks ?? 0,
- mediaSourceId ?? string.Empty,
- null,
- null,
- maxAudioChannels,
- info.PlaySessionId!,
- userId ?? Guid.Empty,
- true,
- true,
- true,
- true,
- true,
- Request.HttpContext.GetNormalizedRemoteIp());
- }
-
- _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
+ _mediaInfoHelper.SetDeviceSpecificData(
+ item,
+ sourceInfo,
+ deviceProfile,
+ authInfo,
+ maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
+ startTimeTicks ?? 0,
+ mediaSourceId ?? string.Empty,
+ null,
+ null,
+ maxAudioChannels,
+ info.PlaySessionId!,
+ userId ?? Guid.Empty,
+ true,
+ true,
+ true,
+ true,
+ true,
+ Request.HttpContext.GetNormalizedRemoteIp());
}
- if (info.MediaSources != null)
+ _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
+
+ foreach (var source in info.MediaSources)
{
- foreach (var source in info.MediaSources)
- {
- _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile!, DlnaProfileType.Video);
- }
+ _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile, DlnaProfileType.Video);
}
- var mediaSource = info.MediaSources![0];
- if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
+ var mediaSource = info.MediaSources[0];
+ if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http && enableRedirection && mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value)
{
- if (enableRedirection)
- {
- if (mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value)
- {
- return Redirect(mediaSource.Path);
- }
- }
+ return Redirect(mediaSource.Path);
}
var isStatic = mediaSource.SupportsDirectStream;
@@ -249,7 +228,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames,
AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels,
- AudioBitRate = isStatic ? (int?)null : (audioBitRate ?? maxStreamingBitrate),
+ AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = maxAudioChannels,
CopyTimestamps = true,
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/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index e45f9b58cd..940fa27a70 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -8,7 +8,6 @@ using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
-using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -234,7 +233,8 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions().AddClientFields(Request);
return Ok(item
- .GetExtras(BaseItem.DisplayExtraTypes)
+ .GetExtras()
+ .Where(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value))
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
}
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index 5cc8c906fb..04732ccf25 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
-using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
@@ -11,9 +10,7 @@ using Jellyfin.Api.Models.UserViewDtos;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
@@ -32,7 +29,6 @@ namespace Jellyfin.Api.Controllers
private readonly IUserManager _userManager;
private readonly IUserViewManager _userViewManager;
private readonly IDtoService _dtoService;
- private readonly IAuthorizationContext _authContext;
private readonly ILibraryManager _libraryManager;
///
@@ -41,19 +37,16 @@ namespace Jellyfin.Api.Controllers
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
- /// Instance of the interface.
/// Instance of the interface.
public UserViewsController(
IUserManager userManager,
IUserViewManager userViewManager,
IDtoService dtoService,
- IAuthorizationContext authContext,
ILibraryManager libraryManager)
{
_userManager = userManager;
_userViewManager = userViewManager;
_dtoService = dtoService;
- _authContext = authContext;
_libraryManager = libraryManager;
}
@@ -138,7 +131,8 @@ namespace Jellyfin.Api.Controllers
Name = i.Name,
Id = i.Id.ToString("N", CultureInfo.InvariantCulture)
})
- .OrderBy(i => i.Name));
+ .OrderBy(i => i.Name)
+ .AsEnumerable());
}
}
}
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 13dc878c1b..2adb006e48 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -654,7 +654,7 @@ namespace Jellyfin.Api.Helpers
{
if (EnableThrottling(state))
{
- transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger(new LoggerFactory()), _serverConfigurationManager, _fileSystem);
+ transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger(new LoggerFactory()), _serverConfigurationManager, _fileSystem, _mediaEncoder);
transcodingJob.TranscodingThrottler.Start();
}
}
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index d037ba7123..7e64cf645b 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -17,10 +17,10 @@
-
+
-
+
diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
index 7a1ca252cf..99376873c0 100644
--- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
+++ b/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
@@ -2,6 +2,7 @@
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
@@ -17,6 +18,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
private readonly ILogger _logger;
private readonly IConfigurationManager _config;
private readonly IFileSystem _fileSystem;
+ private readonly IMediaEncoder _mediaEncoder;
private Timer? _timer;
private bool _isPaused;
@@ -27,12 +29,14 @@ namespace Jellyfin.Api.Models.PlaybackDtos
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
- public TranscodingThrottler(TranscodingJobDto job, ILogger logger, IConfigurationManager config, IFileSystem fileSystem)
+ /// Instance of the interface.
+ public TranscodingThrottler(TranscodingJobDto job, ILogger logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder)
{
_job = job;
_logger = logger;
_config = config;
_fileSystem = fileSystem;
+ _mediaEncoder = mediaEncoder;
}
///
@@ -55,7 +59,8 @@ namespace Jellyfin.Api.Models.PlaybackDtos
try
{
- await _job.Process!.StandardInput.WriteLineAsync().ConfigureAwait(false);
+ var resumeKey = _mediaEncoder.IsPkeyPauseSupported ? "u" : Environment.NewLine;
+ await _job.Process!.StandardInput.WriteAsync(resumeKey).ConfigureAwait(false);
_isPaused = false;
}
catch (Exception ex)
@@ -125,11 +130,13 @@ namespace Jellyfin.Api.Models.PlaybackDtos
{
if (!_isPaused)
{
- _logger.LogDebug("Sending pause command to ffmpeg");
+ var pauseKey = _mediaEncoder.IsPkeyPauseSupported ? "p" : "c";
+
+ _logger.LogDebug("Sending pause command [{Key}] to ffmpeg", pauseKey);
try
{
- await _job.Process!.StandardInput.WriteAsync("c").ConfigureAwait(false);
+ await _job.Process!.StandardInput.WriteAsync(pauseKey).ConfigureAwait(false);
_isPaused = true;
}
catch (Exception ex)
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.Api/Results/OkResultOfT.cs b/Jellyfin.Api/Results/OkResultOfT.cs
new file mode 100644
index 0000000000..f60cbbceea
--- /dev/null
+++ b/Jellyfin.Api/Results/OkResultOfT.cs
@@ -0,0 +1,21 @@
+#pragma warning disable SA1649 // File name should match type name.
+
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Results;
+
+///
+/// Ok result with type specified.
+///
+/// The type to return.
+public class OkResult : OkObjectResult
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value to return.
+ public OkResult(T value)
+ : base(value)
+ {
+ }
+}
diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index 94614b4c8c..cbbeee024e 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/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 6875282318..13f155f357 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -145,9 +145,11 @@ namespace Jellyfin.Drawing.Skia
/// The file at the specified path could not be used to generate a codec.
public string GetImageBlurHash(int xComp, int yComp, string path)
{
- if (path == null)
+ ArgumentNullException.ThrowIfNull(path, nameof(path));
+
+ if (path.Length == 0)
{
- throw new ArgumentNullException(nameof(path));
+ throw new ArgumentException("String can't be empty", nameof(path));
}
var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
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.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 71614dce5d..83b2262782 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.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
index 5e84255f91..4fda8f5a41 100644
--- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
+++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
@@ -67,7 +67,7 @@ namespace Jellyfin.Server.Implementations.Users
else if (string.Equals(
spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal),
pin.Replace("-", string.Empty, StringComparison.Ordinal),
- StringComparison.OrdinalIgnoreCase))
+ StringComparison.Ordinal))
{
var resetUser = userManager.GetUserByName(spr.UserName)
?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
index f5d38db20f..65edb30ad2 100644
--- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -79,7 +79,7 @@ namespace Jellyfin.Server.Implementations.Users
}
///
- public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary customPreferences)
+ public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary customPreferences)
{
var existingPrefs = _dbContext.CustomItemDisplayPreferences
.AsQueryable()
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 2100fa6d59..6a2ef74dd2 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -326,10 +326,10 @@ namespace Jellyfin.Server.Implementations.Users
EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay,
RememberSubtitleSelections = user.RememberSubtitleSelections,
SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty,
- OrderedViews = user.GetPreference(PreferenceKind.OrderedViews),
- GroupedFolders = user.GetPreference(PreferenceKind.GroupedFolders),
- MyMediaExcludes = user.GetPreference(PreferenceKind.MyMediaExcludes),
- LatestItemsExcludes = user.GetPreference(PreferenceKind.LatestItemExcludes)
+ OrderedViews = user.GetPreferenceValues(PreferenceKind.OrderedViews),
+ GroupedFolders = user.GetPreferenceValues(PreferenceKind.GroupedFolders),
+ MyMediaExcludes = user.GetPreferenceValues(PreferenceKind.MyMediaExcludes),
+ LatestItemsExcludes = user.GetPreferenceValues(PreferenceKind.LatestItemExcludes)
},
Policy = new UserPolicy
{
diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
index 7e7e4ca95b..28ed3894fd 100644
--- a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
+++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
@@ -69,15 +69,8 @@ namespace Jellyfin.Server.Infrastructure
///
protected override Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
{
- if (context == null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- if (result == null)
- {
- throw new ArgumentNullException(nameof(result));
- }
+ ArgumentNullException.ThrowIfNull(context, nameof(context));
+ ArgumentNullException.ThrowIfNull(result, nameof(result));
if (range != null && rangeLength == 0)
{
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 0a507c9f96..b2d79050b1 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -37,18 +37,18 @@
-
-
+
+
-
+
-
+
-
+
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 438994851d..a77e790f96 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/ConfigurationUpdateEventArgs.cs b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs
index 2df87d8792..90b1ff70c2 100644
--- a/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs
+++ b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs
@@ -1,22 +1,33 @@
-#nullable disable
-#pragma warning disable CS1591
-
using System;
namespace MediaBrowser.Common.Configuration
{
+ ///
+ /// for the ConfigurationUpdated event.
+ ///
public class ConfigurationUpdateEventArgs : EventArgs
{
///
- /// Gets or sets the key.
+ /// Initializes a new instance of the class.
///
- /// The key.
- public string Key { get; set; }
+ /// The configuration key.
+ /// The new configuration.
+ public ConfigurationUpdateEventArgs(string key, object newConfiguration)
+ {
+ Key = key;
+ NewConfiguration = newConfiguration;
+ }
///
- /// Gets or sets the new configuration.
+ /// Gets the key.
+ ///
+ /// The key.
+ public string Key { get; }
+
+ ///
+ /// Gets the new configuration.
///
/// The new configuration.
- public object NewConfiguration { get; set; }
+ public object NewConfiguration { get; }
}
}
diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs
index 1370e6d79e..57c6546675 100644
--- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs
+++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace MediaBrowser.Common.Configuration
{
///
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/Probing/MediaStreamInfoSideData.cs b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs
index 095757bef7..5dbc438e44 100644
--- a/MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs
+++ b/MediaBrowser.MediaEncoding/Probing/MediaStreamInfoSideData.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MediaBrowser.MediaEncoding.Probing
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 74d7341e91..6cae7f5582 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -44,16 +44,28 @@ namespace MediaBrowser.MediaEncoding.Probing
private IReadOnlyList SplitWhitelist => _splitWhiteList ??= new string[]
{
"AC/DC",
+ "A/T/O/S",
"As/Hi Soundworks",
"Au/Ra",
"Bremer/McCoy",
+ "b/bqスタヂオ",
+ "DOV/S",
+ "DJ'TEKINA//SOMETHING",
+ "IX/ON",
+ "J-CORE SLi//CER",
+ "M(a/u)SH",
+ "Kaoru/Brilliance",
+ "signum/ii",
+ "Richiter(LORB/DUGEM DI BARAT)",
"이달의 소녀 1/3",
"R!N / Gemie",
"LOONA 1/3",
"LOONA / yyxy",
"LOONA / ODD EYE CIRCLE",
"K/DA",
- "22/7"
+ "22/7",
+ "諭吉佳作/men",
+ "//dARTH nULL"
};
public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol)
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..0d4489517e 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
@@ -1,12 +1,12 @@
+using System;
+using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Threading;
using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
using Nikse.SubtitleEdit.Core.Common;
-using ILogger = Microsoft.Extensions.Logging.ILogger;
using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat;
namespace MediaBrowser.MediaEncoding.Subtitles
@@ -14,31 +14,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 +83,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..115f085ff8 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);
@@ -768,7 +746,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
- internal readonly struct SubtitleInfo
+ public readonly struct SubtitleInfo
{
public SubtitleInfo(string path, MediaProtocol protocol, string format, bool isExternal)
{
diff --git a/MediaBrowser.Model/Branding/BrandingOptions.cs b/MediaBrowser.Model/Branding/BrandingOptions.cs
index a0adb56ef7..695267d46d 100644
--- a/MediaBrowser.Model/Branding/BrandingOptions.cs
+++ b/MediaBrowser.Model/Branding/BrandingOptions.cs
@@ -1,5 +1,4 @@
using System.Text.Json.Serialization;
-using System.Xml.Serialization;
namespace MediaBrowser.Model.Branding;
diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs
index 73ebfba70e..f4cd2f0065 100644
--- a/MediaBrowser.Model/Configuration/EncodingOptions.cs
+++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs
@@ -1,5 +1,3 @@
-using System;
-
#nullable disable
#pragma warning disable CS1591
diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs
index 81359462c3..94f3546608 100644
--- a/MediaBrowser.Model/Configuration/UserConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs
@@ -22,10 +22,10 @@ namespace MediaBrowser.Model.Configuration
HidePlayedInLatest = true;
PlayDefaultAudioTrack = true;
- LatestItemsExcludes = Array.Empty();
- OrderedViews = Array.Empty();
- MyMediaExcludes = Array.Empty();
- GroupedFolders = Array.Empty();
+ LatestItemsExcludes = Array.Empty();
+ OrderedViews = Array.Empty();
+ MyMediaExcludes = Array.Empty();
+ GroupedFolders = Array.Empty();
}
///
@@ -48,7 +48,7 @@ namespace MediaBrowser.Model.Configuration
public bool DisplayMissingEpisodes { get; set; }
- public string[] GroupedFolders { get; set; }
+ public Guid[] GroupedFolders { get; set; }
public SubtitlePlaybackMode SubtitleMode { get; set; }
@@ -56,11 +56,11 @@ namespace MediaBrowser.Model.Configuration
public bool EnableLocalPassword { get; set; }
- public string[] OrderedViews { get; set; }
+ public Guid[] OrderedViews { get; set; }
- public string[] LatestItemsExcludes { get; set; }
+ public Guid[] LatestItemsExcludes { get; set; }
- public string[] MyMediaExcludes { get; set; }
+ public Guid[] MyMediaExcludes { get; set; }
public bool HidePlayedInLatest { get; set; }
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/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs
index 0c66351c70..5cfa2e7e3c 100644
--- a/MediaBrowser.Model/Dlna/StreamInfo.cs
+++ b/MediaBrowser.Model/Dlna/StreamInfo.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.Linq;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
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 4f511f9960..ad2ff1ba29 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -34,13 +34,13 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
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/README.md b/README.md
index a5b57d537d..e81ea64c06 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@
-
+
@@ -36,13 +36,10 @@
-
+
-
-
-
-
+
diff --git a/debian/jellyfin.service b/debian/jellyfin.service
index 2f97c46549..1150924a08 100644
--- a/debian/jellyfin.service
+++ b/debian/jellyfin.service
@@ -8,7 +8,7 @@ EnvironmentFile = /etc/default/jellyfin
User = jellyfin
Group = jellyfin
WorkingDirectory = /var/lib/jellyfin
-ExecStart = /usr/bin/jellyfin ${JELLYFIN_WEB_OPT} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} ${JELLYFIN_ADDITIONAL_OPTS}
+ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
Restart = on-failure
TimeoutSec = 15
SuccessExitStatus=0 143
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..0bae42bc85 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/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-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.debian.amd64 b/deployment/Dockerfile.debian.amd64
index daba0eb7d1..c7bb5f7687 100644
--- a/deployment/Dockerfile.debian.amd64
+++ b/deployment/Dockerfile.debian.amd64
@@ -12,7 +12,7 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends \
- apt-transport-https debhelper gnupg devscripts build-essential mmv \
+ debhelper gnupg devscripts build-essential mmv \
libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev \
libssl1.1 liblttng-ust0
diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64
index db4e7f8178..a0ca9b3f35 100644
--- a/deployment/Dockerfile.debian.arm64
+++ b/deployment/Dockerfile.debian.arm64
@@ -12,7 +12,7 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends \
- apt-transport-https debhelper gnupg devscripts build-essential mmv
+ debhelper gnupg devscripts build-essential mmv
# Prepare the cross-toolchain
RUN dpkg --add-architecture arm64 \
diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf
index 9b008e7fb0..42a55ebfee 100644
--- a/deployment/Dockerfile.debian.armhf
+++ b/deployment/Dockerfile.debian.armhf
@@ -12,7 +12,7 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends \
- apt-transport-https debhelper gnupg devscripts build-essential mmv
+ debhelper gnupg devscripts build-essential mmv
# Prepare the cross-toolchain
RUN dpkg --add-architecture armhf \
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..20aa777b69 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/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-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.linux.amd64 b/deployment/Dockerfile.linux.amd64
index 2c7e41cace..14b580d11d 100644
--- a/deployment/Dockerfile.linux.amd64
+++ b/deployment/Dockerfile.linux.amd64
@@ -12,7 +12,7 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends \
- apt-transport-https debhelper gnupg devscripts unzip \
+ debhelper gnupg devscripts unzip \
mmv libcurl4-openssl-dev libfontconfig1-dev \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
diff --git a/deployment/Dockerfile.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl
index e903cf1d32..672c3f2696 100644
--- a/deployment/Dockerfile.linux.amd64-musl
+++ b/deployment/Dockerfile.linux.amd64-musl
@@ -12,7 +12,7 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends \
- apt-transport-https debhelper gnupg devscripts unzip \
+ debhelper gnupg devscripts unzip \
mmv libcurl4-openssl-dev libfontconfig1-dev \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
diff --git a/deployment/Dockerfile.linux.arm64 b/deployment/Dockerfile.linux.arm64
index 0dd3c5e4e8..f2a178be37 100644
--- a/deployment/Dockerfile.linux.arm64
+++ b/deployment/Dockerfile.linux.arm64
@@ -12,7 +12,7 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends \
- apt-transport-https debhelper gnupg devscripts unzip \
+ debhelper gnupg devscripts unzip \
mmv libcurl4-openssl-dev libfontconfig1-dev \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
diff --git a/deployment/Dockerfile.linux.armhf b/deployment/Dockerfile.linux.armhf
index 16a8218e18..025716f457 100644
--- a/deployment/Dockerfile.linux.armhf
+++ b/deployment/Dockerfile.linux.armhf
@@ -12,7 +12,7 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends \
- apt-transport-https debhelper gnupg devscripts unzip \
+ debhelper gnupg devscripts unzip \
mmv libcurl4-openssl-dev libfontconfig1-dev \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
diff --git a/deployment/Dockerfile.linux.musl-linux-arm64 b/deployment/Dockerfile.linux.musl-linux-arm64
new file mode 100644
index 0000000000..2da72e4ae0
--- /dev/null
+++ b/deployment/Dockerfile.linux.musl-linux-arm64
@@ -0,0 +1,26 @@
+FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG ARTIFACT_DIR=/dist
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+ENV DEB_BUILD_OPTIONS=noddebs
+ENV ARCH=arm64
+ENV IS_DOCKER=YES
+
+# Prepare Debian build environment
+RUN apt-get update -yqq \
+ && apt-get install -yqq --no-install-recommends \
+ apt-transport-https debhelper gnupg devscripts unzip \
+ mmv libcurl4-openssl-dev libfontconfig1-dev \
+ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
+
+# Link to docker-build script
+RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.musl-linux-arm64 /build.sh
+
+VOLUME ${SOURCE_DIR}/
+
+VOLUME ${ARTIFACT_DIR}/
+
+ENTRYPOINT ["/build.sh"]
diff --git a/deployment/Dockerfile.macos b/deployment/Dockerfile.macos
index 699ab2d408..f63dc29065 100644
--- a/deployment/Dockerfile.macos
+++ b/deployment/Dockerfile.macos
@@ -12,7 +12,7 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends \
- apt-transport-https debhelper gnupg devscripts \
+ debhelper gnupg devscripts \
mmv libcurl4-openssl-dev libfontconfig1-dev \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable
index b567d7bcea..e48e2d41a0 100644
--- a/deployment/Dockerfile.portable
+++ b/deployment/Dockerfile.portable
@@ -11,7 +11,7 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends \
- apt-transport-https debhelper gnupg devscripts \
+ debhelper gnupg devscripts \
mmv libcurl4-openssl-dev libfontconfig1-dev \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64
index 34ef0c20de..ccc0f76cdc 100644
--- a/deployment/Dockerfile.ubuntu.amd64
+++ b/deployment/Dockerfile.ubuntu.amd64
@@ -12,12 +12,12 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends \
- apt-transport-https debhelper gnupg wget ca-certificates devscripts \
+ debhelper gnupg wget ca-certificates devscripts \
mmv build-essential libcurl4-openssl-dev libfontconfig1-dev \
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/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-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..8931809746 100644
--- a/deployment/Dockerfile.ubuntu.arm64
+++ b/deployment/Dockerfile.ubuntu.arm64
@@ -12,11 +12,11 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends \
- apt-transport-https debhelper gnupg wget ca-certificates devscripts \
+ debhelper gnupg wget ca-certificates devscripts \
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/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-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..bf1edf7777 100644
--- a/deployment/Dockerfile.ubuntu.armhf
+++ b/deployment/Dockerfile.ubuntu.armhf
@@ -12,11 +12,11 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends \
- apt-transport-https debhelper gnupg wget ca-certificates devscripts \
+ debhelper gnupg wget ca-certificates devscripts \
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/8159607a-e686-4ead-ac99-b4c97290a5fd/ec6070b1b2cc0651ebe57cf1bd411315/dotnet-sdk-6.0.401-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.windows.amd64 b/deployment/Dockerfile.windows.amd64
index b9543a7c91..655300d477 100644
--- a/deployment/Dockerfile.windows.amd64
+++ b/deployment/Dockerfile.windows.amd64
@@ -11,7 +11,7 @@ ENV IS_DOCKER=YES
# Prepare Debian build environment
RUN apt-get update -yqq \
&& apt-get install -yqq --no-install-recommends \
- apt-transport-https debhelper gnupg devscripts unzip \
+ debhelper gnupg devscripts unzip \
mmv libcurl4-openssl-dev libfontconfig1-dev \
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 zip
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.linux.musl-linux-arm64 b/deployment/build.linux.musl-linux-arm64
new file mode 100755
index 0000000000..38826ae7fc
--- /dev/null
+++ b/deployment/build.linux.musl-linux-arm64
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+#= Generic Linux musl-linux-arm64 .tar.gz
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Get version
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+ version="${BUILD_ID}"
+else
+ version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+fi
+
+# Build archives
+dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-musl-arm64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true
+tar -czf jellyfin-server_${version}_linux-arm64-musl.tar.gz -C dist jellyfin-server_${version}
+rm -rf dist/jellyfin-server_${version}
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/
+mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/
+
+if [[ ${IS_DOCKER} == YES ]]; then
+ chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR}
+fi
+
+popd
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..2fb9f30e00 100644
--- a/fedora/jellyfin.service
+++ b/fedora/jellyfin.service
@@ -8,44 +8,10 @@ EnvironmentFile = /etc/sysconfig/jellyfin
User = jellyfin
Group = jellyfin
WorkingDirectory = /var/lib/jellyfin
-ExecStart = /usr/bin/jellyfin ${JELLYFIN_WEB_OPT} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT} ${JELLYFIN_ADDITIONAL_OPTS}
+ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
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 13f21ea1ff..a6771e3896 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -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
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index 1c834de822..5ac5f49239 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -84,6 +84,8 @@
+
+
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..b19be071bf 100644
--- a/src/Jellyfin.Extensions/StringExtensions.cs
+++ b/src/Jellyfin.Extensions/StringExtensions.cs
@@ -1,5 +1,4 @@
using System;
-using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
@@ -40,7 +39,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/src/Jellyfin.MediaEncoding.Hls/Extractors/IKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Hls/Extractors/IKeyframeExtractor.cs
index 497210f417..083e93de1a 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Extractors/IKeyframeExtractor.cs
+++ b/src/Jellyfin.MediaEncoding.Hls/Extractors/IKeyframeExtractor.cs
@@ -1,4 +1,3 @@
-using System;
using System.Diagnostics.CodeAnalysis;
using Jellyfin.MediaEncoding.Keyframes;
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs
index 320604e101..79aa8a3549 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs
+++ b/src/Jellyfin.MediaEncoding.Keyframes/FfProbe/FfProbeKeyframeExtractor.cs
@@ -11,7 +11,7 @@ namespace Jellyfin.MediaEncoding.Keyframes.FfProbe;
///
public static class FfProbeKeyframeExtractor
{
- private const string DefaultArguments = "-v error -skip_frame nokey -show_entries format=duration -show_entries stream=duration -show_entries packet=pts_time,flags -select_streams v -of csv \"{0}\"";
+ private const string DefaultArguments = "-fflags +genpts -v error -skip_frame nokey -show_entries format=duration -show_entries stream=duration -show_entries packet=pts_time,flags -select_streams v -of csv \"{0}\"";
///
/// Extracts the keyframes using the ffprobe executable at the specified path.
@@ -62,12 +62,17 @@ public static class FfProbeKeyframeExtractor
var rest = line[(firstComma + 1)..];
if (lineType.Equals("packet", StringComparison.OrdinalIgnoreCase))
{
- if (rest.EndsWith(",K_"))
+ // Split time and flags from the packet line. Example line: packet,7169.079000,K_
+ var secondComma = rest.IndexOf(',');
+ var ptsTime = rest[..secondComma];
+ var flags = rest[(secondComma + 1)..];
+ if (flags.StartsWith("K_"))
{
- // Trim the flags from the packet line. Example line: packet,7169.079000,K_
- var keyframe = double.Parse(rest[..^3], NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture);
- // Have to manually convert to ticks to avoid rounding errors as TimeSpan is only precise down to 1 ms when converting double.
- keyframes.Add(Convert.ToInt64(keyframe * TimeSpan.TicksPerSecond));
+ if (double.TryParse(ptsTime, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var keyframe))
+ {
+ // Have to manually convert to ticks to avoid rounding errors as TimeSpan is only precise down to 1 ms when converting double.
+ keyframes.Add(Convert.ToInt64(keyframe * TimeSpan.TicksPerSecond));
+ }
}
}
else if (lineType.Equals("stream", StringComparison.OrdinalIgnoreCase))
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
index 29cdf561f7..9585cb60c6 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
@@ -21,7 +21,7 @@
-
+
diff --git a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
index 23c51999fa..7c85ddd620 100644
--- a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
+++ b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs
@@ -62,7 +62,7 @@ namespace Jellyfin.Api.Tests.Auth.DefaultAuthorizationPolicy
}
}
- private static TheoryData> GetParts_ValidAuthHeader_Success_Data()
+ public static TheoryData> GetParts_ValidAuthHeader_Success_Data()
{
var data = new TheoryData>();
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index a08220858e..a20c9690f0 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -15,16 +15,16 @@
-
+
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs
index 3ae6ae5bdd..e37c9d91f3 100644
--- a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs
+++ b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs
@@ -192,7 +192,9 @@ namespace Jellyfin.Api.Tests.ModelBinders
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
- Assert.Empty((IReadOnlyList?)bindingContextMock.Object.Result.Model);
+ var listResult = (IReadOnlyList?)bindingContextMock.Object.Result.Model;
+ Assert.NotNull(listResult);
+ Assert.Empty(listResult);
}
[Fact]
@@ -220,7 +222,9 @@ namespace Jellyfin.Api.Tests.ModelBinders
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
- Assert.Single((IReadOnlyList?)bindingContextMock.Object.Result.Model);
+ var listResult = (IReadOnlyList?)bindingContextMock.Object.Result.Model;
+ Assert.NotNull(listResult);
+ Assert.Single(listResult);
}
}
}
diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs
index 938d19a154..7c05ee0362 100644
--- a/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs
+++ b/tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs
@@ -192,7 +192,9 @@ namespace Jellyfin.Api.Tests.ModelBinders
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
- Assert.Empty((IReadOnlyList?)bindingContextMock.Object.Result.Model);
+ var listResult = (IReadOnlyList?)bindingContextMock.Object.Result.Model;
+ Assert.NotNull(listResult);
+ Assert.Empty(listResult);
}
[Fact]
@@ -220,7 +222,9 @@ namespace Jellyfin.Api.Tests.ModelBinders
await modelBinder.BindModelAsync(bindingContextMock.Object);
Assert.True(bindingContextMock.Object.Result.IsModelSet);
- Assert.Single((IReadOnlyList?)bindingContextMock.Object.Result.Model);
+ var listResult = (IReadOnlyList?)bindingContextMock.Object.Result.Model;
+ Assert.NotNull(listResult);
+ Assert.Single(listResult);
}
}
}
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 82d7529019..95fc1d9176 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -12,8 +12,8 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index da52b93fc8..d95747206d 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -12,9 +12,9 @@
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
index a7ad79def5..1444d6faf7 100644
--- a/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
+++ b/tests/Jellyfin.Dlna.Tests/Jellyfin.Dlna.Tests.csproj
@@ -7,9 +7,9 @@
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
index 9f0f1c4de0..9e3bef8818 100644
--- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
+++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
@@ -7,7 +7,7 @@
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonStringConverterTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonStringConverterTests.cs
index 345f37cbe0..77717af703 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonStringConverterTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonStringConverterTests.cs
@@ -32,7 +32,7 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
const string? input = "123";
const int output = 123;
var deserialized = JsonSerializer.Deserialize(input, _jsonSerializerOptions);
- Assert.Equal(deserialized, output);
+ Assert.Equal(output, deserialized);
}
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
index 21206fb718..83ea1907c0 100644
--- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
@@ -7,8 +7,8 @@
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs b/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs
index 79648c4f6e..bbacdcd629 100644
--- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Playlist/DynamicHlsPlaylistGeneratorTests.cs
@@ -53,7 +53,7 @@ namespace Jellyfin.MediaEncoding.Hls.Tests.Playlist
Assert.False(DynamicHlsPlaylistGenerator.IsExtractionAllowedForFile(filePath, allowedExtensions));
}
- private static TheoryData ComputeEqualLengthSegments_Valid_Success_Data()
+ public static TheoryData ComputeEqualLengthSegments_Valid_Success_Data()
{
var data = new TheoryData
{
@@ -67,7 +67,7 @@ namespace Jellyfin.MediaEncoding.Hls.Tests.Playlist
return data;
}
- private static TheoryData ComputeSegments_Valid_Success_Data()
+ public static TheoryData ComputeSegments_Valid_Success_Data()
{
var data = new TheoryData
{
diff --git a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
index 67fb00b1c0..84a069424e 100644
--- a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
@@ -8,8 +8,8 @@
-
-
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index 3da515bfc8..4cff2143c4 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -22,9 +22,9 @@
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
index 3775555dec..fe0d7fc90f 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/AssParserTests.cs
@@ -1,7 +1,6 @@
using System;
using System.Globalization;
using System.IO;
-using System.Threading;
using MediaBrowser.MediaEncoding.Subtitles;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
@@ -15,7 +14,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..2aebee5562 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SrtParserTests.cs
@@ -1,7 +1,6 @@
using System;
using System.Globalization;
using System.IO;
-using System.Threading;
using MediaBrowser.MediaEncoding.Subtitles;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
@@ -15,7 +14,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 +36,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..6abf2d26cb 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SsaParserTests.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
-using System.Threading;
using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging.Abstractions;
@@ -13,7 +12,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 +20,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 +75,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.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
index 639c364df2..2431274383 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/SubtitleEncoderTests.cs
@@ -12,7 +12,7 @@ namespace Jellyfin.MediaEncoding.Subtitles.Tests
{
public class SubtitleEncoderTests
{
- internal static TheoryData GetReadableFile_Valid_TestData()
+ public static TheoryData GetReadableFile_Valid_TestData()
{
var data = new TheoryData();
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/Drawing/ImageFormatExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs
index 7c3a7ff6c7..a5bdb42d89 100644
--- a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs
+++ b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs
@@ -7,7 +7,7 @@ namespace Jellyfin.Model.Drawing;
public static class ImageFormatExtensionsTests
{
- private static TheoryData GetAllImageFormats()
+ public static TheoryData GetAllImageFormats()
{
var theoryTypes = new TheoryData();
foreach (var x in Enum.GetValues())
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.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index ad3cca77aa..fbcaa66f4b 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -7,9 +7,9 @@
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index 04c69b130f..da0e9f5b1d 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -12,9 +12,9 @@
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
index 9a9a57be47..79f2366b89 100644
--- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs
@@ -2,7 +2,6 @@ using System.Collections.Generic;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
-using MediaBrowser.Model.IO;
using Xunit;
namespace Jellyfin.Naming.Tests.Video
diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
index b761878426..cc9cfdd7dd 100644
--- a/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
+++ b/tests/Jellyfin.Naming.Tests/Video/VideoListResolverTests.cs
@@ -3,7 +3,6 @@ using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using Xunit;
namespace Jellyfin.Naming.Tests.Video
diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index 8e5cbb282e..8ec0262bd5 100644
--- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -12,15 +12,15 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
index 7bbd21048f..1942297376 100644
--- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
+++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
@@ -13,9 +13,9 @@
-
-
-
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
index c0931dbcf1..08b343cd89 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
@@ -44,7 +44,7 @@ namespace Jellyfin.Providers.Tests.Manager
ValidateImages_Test(ImageType.Primary, 0, true, 0, false, 0);
}
- private static TheoryData GetImageTypesWithCount()
+ public static TheoryData GetImageTypesWithCount()
{
var theoryTypes = new TheoryData
{
diff --git a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs
index b74b331b7f..28b2e1d8f2 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs
@@ -132,7 +132,7 @@ namespace Jellyfin.Providers.Tests.Manager
Assert.True(TestMergeBaseItemData
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
index 7c99520308..988abce812 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;