Merge branch 'master' into network-rewrite

This commit is contained in:
Shadowghost 2022-10-01 19:59:00 +02:00
commit 4fc52a840c
225 changed files with 1532 additions and 1035 deletions

View File

@ -26,6 +26,8 @@ jobs:
BuildConfiguration: linux.amd64-musl BuildConfiguration: linux.amd64-musl
Linux.arm64: Linux.arm64:
BuildConfiguration: linux.arm64 BuildConfiguration: linux.arm64
Linux.musl-linux-arm64:
BuildConfiguration: linux.musl-linux-arm64
Linux.armhf: Linux.armhf:
BuildConfiguration: linux.armhf BuildConfiguration: linux.armhf
Windows.amd64: Windows.amd64:

View File

@ -36,6 +36,7 @@
- [dmitrylyzo](https://github.com/dmitrylyzo) - [dmitrylyzo](https://github.com/dmitrylyzo)
- [DMouse10462](https://github.com/DMouse10462) - [DMouse10462](https://github.com/DMouse10462)
- [DrPandemic](https://github.com/DrPandemic) - [DrPandemic](https://github.com/DrPandemic)
- [eglia](https://github.com/eglia)
- [EraYaN](https://github.com/EraYaN) - [EraYaN](https://github.com/EraYaN)
- [escabe](https://github.com/escabe) - [escabe](https://github.com/escabe)
- [excelite](https://github.com/excelite) - [excelite](https://github.com/excelite)
@ -147,6 +148,7 @@
- [xosdy](https://github.com/xosdy) - [xosdy](https://github.com/xosdy)
- [XVicarious](https://github.com/XVicarious) - [XVicarious](https://github.com/XVicarious)
- [YouKnowBlom](https://github.com/YouKnowBlom) - [YouKnowBlom](https://github.com/YouKnowBlom)
- [ZachPhelan](https://github.com/ZachPhelan)
- [KristupasSavickas](https://github.com/KristupasSavickas) - [KristupasSavickas](https://github.com/KristupasSavickas)
- [Pusta](https://github.com/pusta) - [Pusta](https://github.com/pusta)
- [nielsvanvelzen](https://github.com/nielsvanvelzen) - [nielsvanvelzen](https://github.com/nielsvanvelzen)
@ -157,6 +159,7 @@
- [jonas-resch](https://github.com/jonas-resch) - [jonas-resch](https://github.com/jonas-resch)
- [vgambier](https://github.com/vgambier) - [vgambier](https://github.com/vgambier)
- [MinecraftPlaye](https://github.com/MinecraftPlaye) - [MinecraftPlaye](https://github.com/MinecraftPlaye)
- [RealGreenDragon](https://github.com/RealGreenDragon)
# Emby Contributors # Emby Contributors
@ -225,3 +228,4 @@
- [gnuyent](https://github.com/gnuyent) - [gnuyent](https://github.com/gnuyent)
- [Matthew Jones](https://github.com/matthew-jones-uk) - [Matthew Jones](https://github.com/matthew-jones-uk)
- [Jakob Kukla](https://github.com/jakobkukla) - [Jakob Kukla](https://github.com/jakobkukla)
- [Utku Özdemir](https://github.com/utkuozdemir)

View File

@ -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. # mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
# curl: healthcheck # curl: healthcheck
RUN apt-get update \ 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 - \ && 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 \ && 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 \ && apt-get update \
@ -53,7 +53,7 @@ RUN apt-get update \
&& dpkg -i *.deb \ && dpkg -i *.deb \
&& cd .. \ && cd .. \
&& rm -rf intel-compute-runtime \ && 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 clean autoclean -y \
&& apt-get autoremove -y \ && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
@ -72,7 +72,7 @@ COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# because of changes in docker and systemd we need to not build in parallel at the moment # 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 # 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 FROM app

View File

@ -38,9 +38,6 @@ RUN apt-get update \
libssl-dev \ libssl-dev \
libfontconfig1 \ libfontconfig1 \
libfreetype6 \ libfreetype6 \
libomxil-bellagio0 \
libomxil-bellagio-bin \
libraspberrypi0 \
vainfo \ vainfo \
libva2 \ libva2 \
locales \ locales \
@ -64,7 +61,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists # Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r RUN find . -type d -name obj | xargs -r rm -r
# Build # 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 FROM app

View File

@ -55,7 +55,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists # Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r RUN find . -type d -name obj | xargs -r rm -r
# Build # 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 FROM app

View File

@ -446,7 +446,7 @@ namespace Emby.Dlna.Didl
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// If context is a season, this will return a string containing just episode number and name. /// 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.
/// </remarks> /// </remarks>
/// <param name="episode">The episode.</param> /// <param name="episode">The episode.</param>
/// <param name="context">Current context.</param> /// <param name="context">Current context.</param>

View File

@ -123,7 +123,7 @@ namespace Emby.Dlna
/// <summary> /// <summary>
/// Attempts to match a device with a profile. /// Attempts to match a device with a profile.
/// Rules: /// 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 profile field can be an exact match, or a reg exp.
/// </summary> /// </summary>
/// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param> /// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>

View File

@ -16,7 +16,7 @@ namespace Emby.Dlna
/// </summary> /// </summary>
/// <param name="subscriptionId">The subscription identifier.</param> /// <param name="subscriptionId">The subscription identifier.</param>
/// <param name="notificationType">The notification type.</param> /// <param name="notificationType">The notification type.</param>
/// <param name="requestedTimeoutString">The requested timeout as a sting.</param> /// <param name="requestedTimeoutString">The requested timeout as a string.</param>
/// <param name="callbackUrl">The callback url.</param> /// <param name="callbackUrl">The callback url.</param>
/// <returns>The response.</returns> /// <returns>The response.</returns>
EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl); EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl);
@ -25,7 +25,7 @@ namespace Emby.Dlna
/// Creates the event subscription. /// Creates the event subscription.
/// </summary> /// </summary>
/// <param name="notificationType">The notification type.</param> /// <param name="notificationType">The notification type.</param>
/// <param name="requestedTimeoutString">The requested timeout as a sting.</param> /// <param name="requestedTimeoutString">The requested timeout as a string.</param>
/// <param name="callbackUrl">The callback url.</param> /// <param name="callbackUrl">The callback url.</param>
/// <returns>The response.</returns> /// <returns>The response.</returns>
EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl); EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl);

View File

@ -235,7 +235,7 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("Setting mute"); _logger.LogDebug("Setting mute");
var value = mute ? 1 : 0; var value = mute ? 1 : 0;
await new SsdpHttpClient(_httpClientFactory) await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync( .SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
@ -276,7 +276,7 @@ namespace Emby.Dlna.PlayTo
// Remote control will perform better // Remote control will perform better
Volume = value; Volume = value;
await new SsdpHttpClient(_httpClientFactory) await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync( .SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
@ -303,7 +303,7 @@ namespace Emby.Dlna.PlayTo
throw new InvalidOperationException("Unable to find service"); throw new InvalidOperationException("Unable to find service");
} }
await new SsdpHttpClient(_httpClientFactory) await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync( .SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
@ -343,7 +343,7 @@ namespace Emby.Dlna.PlayTo
} }
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
await new SsdpHttpClient(_httpClientFactory) await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync( .SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
@ -400,7 +400,8 @@ namespace Emby.Dlna.PlayTo
} }
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary); 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); .ConfigureAwait(false);
} }
@ -428,7 +429,7 @@ namespace Emby.Dlna.PlayTo
throw new InvalidOperationException("Unable to find service"); throw new InvalidOperationException("Unable to find service");
} }
return new SsdpHttpClient(_httpClientFactory).SendCommandAsync( return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
@ -461,7 +462,7 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService(); var service = GetAvTransportService();
await new SsdpHttpClient(_httpClientFactory) await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync( .SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
@ -485,7 +486,7 @@ namespace Emby.Dlna.PlayTo
var service = GetAvTransportService(); var service = GetAvTransportService();
await new SsdpHttpClient(_httpClientFactory) await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync( .SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
@ -618,7 +619,7 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
@ -668,7 +669,7 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
@ -701,7 +702,7 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
@ -747,7 +748,7 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
@ -819,7 +820,7 @@ namespace Emby.Dlna.PlayTo
return (false, null); return (false, null);
} }
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync( var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
@ -997,7 +998,7 @@ namespace Emby.Dlna.PlayTo
string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); 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); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
if (document == null) if (document == null)
@ -1029,7 +1030,7 @@ namespace Emby.Dlna.PlayTo
string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
var httpClient = new SsdpHttpClient(_httpClientFactory); var httpClient = new DlnaHttpClient(_logger, _httpClientFactory);
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync"); _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
if (document == null) if (document == null)
@ -1064,7 +1065,7 @@ namespace Emby.Dlna.PlayTo
public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken) public static async Task<Device> 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); var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
if (document == null) if (document == null)

View File

@ -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<XDocument?> 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<XDocument?> 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<XDocument?> 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);
}
}
}

View File

@ -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<XDocument> 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<XDocument> 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<HttpResponseMessage> 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);
}
}
}

View File

@ -3,7 +3,7 @@ namespace Emby.Naming.AudioBook
/// <summary> /// <summary>
/// Data object for passing result of audiobook part/chapter extraction. /// Data object for passing result of audiobook part/chapter extraction.
/// </summary> /// </summary>
public struct AudioBookFilePathParserResult public record struct AudioBookFilePathParserResult
{ {
/// <summary> /// <summary>
/// Gets or sets optional number of path extracted from audiobook filename. /// Gets or sets optional number of path extracted from audiobook filename.

View File

@ -15,7 +15,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="TagLibSharp" Version="2.2.0" /> <PackageReference Include="TagLibSharp" Version="2.3.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>

View File

@ -365,11 +365,7 @@ namespace Emby.Server.Implementations.AppBase
validatingStore.Validate(currentConfiguration, configuration); validatingStore.Validate(currentConfiguration, configuration);
} }
NamedConfigurationUpdating?.Invoke(this, new ConfigurationUpdateEventArgs NamedConfigurationUpdating?.Invoke(this, new ConfigurationUpdateEventArgs(key, configuration));
{
Key = key,
NewConfiguration = configuration
});
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration); _configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
@ -391,11 +387,7 @@ namespace Emby.Server.Implementations.AppBase
/// <param name="configuration">The old configuration.</param> /// <param name="configuration">The old configuration.</param>
protected virtual void OnNamedConfigurationUpdated(string key, object configuration) protected virtual void OnNamedConfigurationUpdated(string key, object configuration)
{ {
NamedConfigurationUpdated?.Invoke(this, new ConfigurationUpdateEventArgs NamedConfigurationUpdated?.Invoke(this, new ConfigurationUpdateEventArgs(key, configuration));
{
Key = key,
NewConfiguration = configuration
});
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -83,6 +83,7 @@ using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Controller.TV; using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
@ -111,7 +112,7 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// Class CompositionRoot. /// Class CompositionRoot.
/// </summary> /// </summary>
public abstract class ApplicationHost : IServerApplicationHost, IDisposable public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
{ {
/// <summary> /// <summary>
/// The environment variable prefixes to log at server startup. /// The environment variable prefixes to log at server startup.
@ -634,7 +635,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IAuthService, AuthService>(); serviceCollection.AddSingleton<IAuthService, AuthService>();
serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>(); serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>();
serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>();
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
@ -1233,5 +1235,49 @@ namespace Emby.Server.Implementations
_disposed = true; _disposed = true;
} }
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);
Dispose(false);
GC.SuppressFinalize(this);
}
/// <summary>
/// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
/// </summary>
/// <returns>A ValueTask.</returns>
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);
}
}
} }
} }

View File

@ -4934,6 +4934,7 @@ SELECT key FROM UserDatas WHERE isFavorite=@IsFavorite AND userId=@UserId)
AND Type = @InternalPersonType)"); AND Type = @InternalPersonType)");
statement?.TryBind("@IsFavorite", query.IsFavorite.Value); statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
statement?.TryBind("@InternalPersonType", typeof(Person).FullName); statement?.TryBind("@InternalPersonType", typeof(Person).FullName);
statement?.TryBind("@UserId", query.User.InternalId);
} }
if (!query.ItemId.Equals(default)) if (!query.ItemId.Equals(default))
@ -4988,11 +4989,6 @@ AND Type = @InternalPersonType)");
statement?.TryBind("@NameContains", "%" + query.NameContains + "%"); statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
} }
if (query.User != null)
{
statement?.TryBind("@UserId", query.User.InternalId);
}
return whereClauses; return whereClauses;
} }

View File

@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.People)) if (options.ContainsField(ItemFields.People))
{ {
AttachPeople(dto, item); AttachPeople(dto, item, user);
} }
if (options.ContainsField(ItemFields.PrimaryImageAspectRatio)) if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
@ -503,7 +503,8 @@ namespace Emby.Server.Implementations.Dto
/// </summary> /// </summary>
/// <param name="dto">The dto.</param> /// <param name="dto">The dto.</param>
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
private void AttachPeople(BaseItemDto dto, BaseItem item) /// <param name="user">The requesting user.</param>
private void AttachPeople(BaseItemDto dto, BaseItem item, User user = null)
{ {
// Ordering by person type to ensure actors and artists are at the front. // 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 // This is taking advantage of the fact that they both begin with A
@ -560,6 +561,9 @@ namespace Emby.Server.Implementations.Dto
return null; return null;
} }
}).Where(i => i != null) }).Where(i => i != null)
.Where(i => user == null ?
true :
i.IsVisible(user))
.GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase) .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
.Select(x => x.First()) .Select(x => x.First())
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);

View File

@ -29,10 +29,10 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
<PackageReference Include="Mono.Nat" Version="3.0.3" /> <PackageReference Include="Mono.Nat" Version="3.0.3" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.4" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.4" />
<PackageReference Include="sharpcompress" Version="0.32.1" /> <PackageReference Include="sharpcompress" Version="0.32.2" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" /> <PackageReference Include="DotNet.Glob" Version="3.1.3" />
</ItemGroup> </ItemGroup>

View File

@ -1,6 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;

View File

@ -11,7 +11,6 @@ using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.HttpServer namespace Emby.Server.Implementations.HttpServer
@ -19,7 +18,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary> /// <summary>
/// Class WebSocketConnection. /// Class WebSocketConnection.
/// </summary> /// </summary>
public class WebSocketConnection : IWebSocketConnection, IDisposable public class WebSocketConnection : IWebSocketConnection
{ {
/// <summary> /// <summary>
/// The logger. /// The logger.
@ -36,6 +35,8 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary> /// </summary>
private readonly WebSocket _socket; private readonly WebSocket _socket;
private bool _disposed = false;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class. /// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
/// </summary> /// </summary>
@ -244,10 +245,39 @@ namespace Emby.Server.Implementations.HttpServer
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool dispose) protected virtual void Dispose(bool dispose)
{ {
if (_disposed)
{
return;
}
if (dispose) if (dispose)
{ {
_socket.Dispose(); _socket.Dispose();
} }
_disposed = true;
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);
Dispose(false);
GC.SuppressFinalize(this);
}
/// <summary>
/// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
/// </summary>
/// <returns>A ValueTask.</returns>
protected virtual async ValueTask DisposeAsyncCore()
{
if (_socket.State == WebSocketState.Open)
{
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "System Shutdown", CancellationToken.None).ConfigureAwait(false);
}
_socket.Dispose();
} }
} }
} }

View File

@ -46,7 +46,6 @@ using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo; using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
@ -2453,6 +2452,12 @@ namespace Emby.Server.Implementations.Library
return RootFolder; return RootFolder;
} }
/// <inheritdoc />
public void QueueLibraryScan()
{
_taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
}
/// <inheritdoc /> /// <inheritdoc />
public int? GetSeasonNumberFromPath(string path) public int? GetSeasonNumberFromPath(string path)
=> SeasonPathParser.Parse(path, true, true).SeasonNumber; => SeasonPathParser.Parse(path, true, true).SeasonNumber;
@ -2523,7 +2528,7 @@ namespace Emby.Server.Implementations.Library
} }
catch (Exception ex) 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; var changed = false;
@ -2760,7 +2765,8 @@ namespace Emby.Server.Implementations.Library
public List<Person> GetPeopleItems(InternalPeopleQuery query) public List<Person> GetPeopleItems(InternalPeopleQuery query)
{ {
return _itemRepository.GetPeopleNames(query).Select(i => return _itemRepository.GetPeopleNames(query)
.Select(i =>
{ {
try try
{ {
@ -2771,7 +2777,12 @@ namespace Emby.Server.Implementations.Library
_logger.LogError(ex, "Error getting person"); _logger.LogError(ex, "Error getting person");
return null; return null;
} }
}).Where(i => i != null).ToList(); })
.Where(i => i != null)
.Where(i => query.User == null ?
true :
i.IsVisible(query.User))
.ToList();
} }
public List<string> GetPeopleNames(InternalPeopleQuery query) public List<string> GetPeopleNames(InternalPeopleQuery query)

View File

@ -387,7 +387,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (!string.IsNullOrEmpty(item.Path)) 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"); var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid");
if (!string.IsNullOrWhiteSpace(imdbid)) if (!string.IsNullOrWhiteSpace(imdbid))
@ -464,7 +464,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ?? var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
new MultiItemResolverResult(); 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 videoPath = result.Items[0].Path;
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name)); var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));

View File

@ -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<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken) public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)

View File

@ -13,6 +13,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -297,7 +298,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
else else
{ {
_taskCompletionSource.TrySetException( _taskCompletionSource.TrySetException(
new Exception( new FfmpegException(
string.Format( string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"Recording for {0} failed. Exit code {1}", "Recording for {0} failed. Exit code {1}",

View File

@ -20,6 +20,7 @@ using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -591,13 +592,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
} }
catch (HttpRequestException ex) 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; throw;
@ -662,7 +660,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return root.Token; 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) private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken)
@ -697,7 +695,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
{ {
throw new Exception("token required"); throw new ArgumentException("token required");
} }
_logger.LogInformation("Headends on account "); _logger.LogInformation("Headends on account ");
@ -768,14 +766,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
var listingsId = info.ListingsId; var listingsId = info.ListingsId;
if (string.IsNullOrEmpty(listingsId)) if (string.IsNullOrEmpty(listingsId))
{ {
throw new Exception("ListingsId required"); throw new ArgumentException("ListingsId required");
} }
var token = await GetToken(info, cancellationToken).ConfigureAwait(false); var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
{ {
throw new Exception("token required"); throw new ArgumentException("token required");
} }
using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId); using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId);

View File

@ -196,7 +196,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
IsInfiniteStream = true, IsInfiniteStream = true,
IsRemote = isRemote, IsRemote = isRemote,
IgnoreDts = true, IgnoreDts = info.IgnoreDts,
SupportsDirectPlay = supportsDirectPlay, SupportsDirectPlay = supportsDirectPlay,
SupportsDirectStream = supportsDirectStream, SupportsDirectStream = supportsDirectStream,

View File

@ -199,7 +199,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (string.IsNullOrWhiteSpace(numberString)) if (string.IsNullOrWhiteSpace(numberString))
{ {
// Using this as a fallback now as this leads to Problems with channels like "5 USA" // 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 // Check for channel number with the format from SatIp
// #EXTINF:0,84. VOX Schweiz // #EXTINF:0,84. VOX Schweiz
// #EXTINF:0,84.0 - VOX Schweiz // #EXTINF:0,84.0 - VOX Schweiz

View File

@ -92,22 +92,22 @@
"ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط", "ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط",
"ValueSpecialEpisodeName": "حلقه خاصه - {0}", "ValueSpecialEpisodeName": "حلقه خاصه - {0}",
"VersionNumber": "النسخة {0}", "VersionNumber": "النسخة {0}",
"TaskCleanCacheDescription": "يحذف ملفات ذاكرة التخزين المؤقت التي لم يعد النظام بحاجة إليها.", "TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.",
"TaskCleanCache": "احذف مجلد ذاكرة التخزين المؤقت", "TaskCleanCache": "احذف ما بمجلد الملفات المؤقتة",
"TasksChannelsCategory": "قنوات الإنترنت", "TasksChannelsCategory": "قنوات الإنترنت",
"TasksLibraryCategory": "مكتبة", "TasksLibraryCategory": "مكتبة",
"TasksMaintenanceCategory": "صيانة", "TasksMaintenanceCategory": "صيانة",
"TaskRefreshLibraryDescription": قوم بفصح مكتبة الوسائط الخاصة بك بحثًا عن ملفات جديدة وتحديث البيانات الوصفية.", "TaskRefreshLibraryDescription": فصح مكتبة الوسائط الخاصة بك بحثًا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.",
"TaskRefreshLibrary": "افحص مكتبة الوسائط", "TaskRefreshLibrary": "افحص مكتبة الوسائط",
"TaskRefreshChapterImagesDescription": قوم بانشاء صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.", "TaskRefreshChapterImagesDescription": ُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.",
"TaskRefreshChapterImages": "استخراج صور الفصل", "TaskRefreshChapterImages": "استخراج صور الفصل",
"TasksApplicationCategory": "تطبيق", "TasksApplicationCategory": "تطبيق",
"TaskDownloadMissingSubtitlesDescription": قوم بالبحث في الإنترنت على الترجمات المفقودة إستنادا على البيانات الوصفية.", "TaskDownloadMissingSubtitlesDescription": بحث في الإنترنت على الترجمات الناقصة استنادا على البيانات الوصفية.",
"TaskDownloadMissingSubtitles": "تحميل الترجمات المفقودة", "TaskDownloadMissingSubtitles": "تحميل الترجمات الناقصة",
"TaskRefreshChannelsDescription": قوم بتحديث معلومات قنوات الإنترنت.", "TaskRefreshChannelsDescription": حدث معلومات قنوات الإنترنت.",
"TaskRefreshChannels": "إعادة تحديث القنوات", "TaskRefreshChannels": "إعادة تحديث القنوات",
"TaskCleanTranscodeDescription": قوم بحذف ملفات الترميز الأقدم من يوم واحد.", "TaskCleanTranscodeDescription": حذف ملفات الترميز الأقدم من يوم واحد.",
"TaskCleanTranscode": "حذف سجلات الترميز", "TaskCleanTranscode": "حذف ما بمجلد الترميز",
"TaskUpdatePluginsDescription": "تحميل وتثبيت الإضافات التي تم تفعيل التحديث التلقائي لها.", "TaskUpdatePluginsDescription": "تحميل وتثبيت الإضافات التي تم تفعيل التحديث التلقائي لها.",
"TaskUpdatePlugins": "تحديث الإضافات", "TaskUpdatePlugins": "تحديث الإضافات",
"TaskRefreshPeopleDescription": "يقوم بتحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.", "TaskRefreshPeopleDescription": "يقوم بتحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.",
@ -116,12 +116,12 @@
"TaskCleanLogs": "حذف مسار السجل", "TaskCleanLogs": "حذف مسار السجل",
"TaskCleanActivityLogDescription": "يحذف سجل الأنشطة الأقدم من الوقت الذي تم تحديده.", "TaskCleanActivityLogDescription": "يحذف سجل الأنشطة الأقدم من الوقت الذي تم تحديده.",
"TaskCleanActivityLog": "حذف سجل الأنشطة", "TaskCleanActivityLog": "حذف سجل الأنشطة",
"Default": "إفتراضي", "Default": "افتراضي",
"Undefined": "غير معرف", "Undefined": "غير معرف",
"Forced": "ملحقة", "Forced": "ملحقة",
"TaskOptimizeDatabaseDescription": "يضغط قاعدة البيانات ويقتطع المساحة الحرة. تشغيل هذه المهمة بعد فحص المكتبة أو إجراء تغييرات أخرى تتضمن تعديلات في قاعدة البيانات قد تؤدي إلى تحسين الأداء.", "TaskOptimizeDatabaseDescription": "يضغط قاعدة البيانات ويقتطع المساحة الحرة. تشغيل هذه المهمة بعد فحص المكتبة أو إجراء تغييرات أخرى تتضمن تعديلات في قاعدة البيانات قد تؤدي إلى تحسين الأداء.",
"TaskOptimizeDatabase": "تحسين قاعدة البيانات", "TaskOptimizeDatabase": "تحسين قاعدة البيانات",
"TaskKeyframeExtractorDescription": قوم باستخراج الإطارات الرئيسيه من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. هذه المهمه قد تستمر لاوقات طويلة.", "TaskKeyframeExtractorDescription": ستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.",
"TaskKeyframeExtractor": "مستخرج الإطار الرئيسي", "TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
"External": "خارجي" "External": "خارجي"
} }

View File

@ -119,5 +119,6 @@
"SubtitleDownloadFailureFromForItem": "Subtiitrite allalaadimine {0} > {1} nurjus", "SubtitleDownloadFailureFromForItem": "Subtiitrite allalaadimine {0} > {1} nurjus",
"UserPolicyUpdatedWithName": "Kasutaja {0} õigusi värskendati", "UserPolicyUpdatedWithName": "Kasutaja {0} õigusi värskendati",
"UserStoppedPlayingItemWithValues": "{0} lõpetas {1} taasesituse seadmes {2}", "UserStoppedPlayingItemWithValues": "{0} lõpetas {1} taasesituse seadmes {2}",
"UserOnlineFromDevice": "{0} on ühendatud seadmest {1}" "UserOnlineFromDevice": "{0} on ühendatud seadmest {1}",
"External": "Väline"
} }

View File

@ -5,7 +5,7 @@
"Artists": "Artistes", "Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès", "AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
"Books": "Livres", "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", "Channels": "Chaînes",
"ChapterNameValue": "Chapitre {0}", "ChapterNameValue": "Chapitre {0}",
"Collections": "Collections", "Collections": "Collections",
@ -42,13 +42,13 @@
"MusicVideos": "Clips musicaux", "MusicVideos": "Clips musicaux",
"NameInstallFailed": "{0} échec de l'installation", "NameInstallFailed": "{0} échec de l'installation",
"NameSeasonNumber": "Saison {0}", "NameSeasonNumber": "Saison {0}",
"NameSeasonUnknown": "Saison Inconnue", "NameSeasonUnknown": "Saison inconnue",
"NewVersionIsAvailable": "Une nouvelle version de Jellyfin Serveur est disponible au téléchargement.", "NewVersionIsAvailable": "Une nouvelle version de Jellyfin Serveur est disponible au téléchargement.",
"NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible", "NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible",
"NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée", "NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée",
"NotificationOptionAudioPlayback": "Lecture audio démarrée", "NotificationOptionAudioPlayback": "Lecture audio démarrée",
"NotificationOptionAudioPlaybackStopped": "Lecture audio arrêté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", "NotificationOptionInstallationFailed": "Échec de l'installation",
"NotificationOptionNewLibraryContent": "Nouveau contenu ajouté", "NotificationOptionNewLibraryContent": "Nouveau contenu ajouté",
"NotificationOptionPluginError": "Erreur d'extension", "NotificationOptionPluginError": "Erreur d'extension",
@ -93,33 +93,33 @@
"ValueSpecialEpisodeName": "Spécial - {0}", "ValueSpecialEpisodeName": "Spécial - {0}",
"VersionNumber": "Version {0}", "VersionNumber": "Version {0}",
"TasksChannelsCategory": "Chaînes en ligne", "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", "TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquants",
"TaskRefreshChannelsDescription": "Rafraîchit les informations des chaînes en ligne.", "TaskRefreshChannelsDescription": "Actualise les informations des chaînes en ligne.",
"TaskRefreshChannels": "Rafraîchir les chaînes", "TaskRefreshChannels": "Actualiser les chaînes",
"TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.", "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.", "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurées pour être mises à jour automatiquement.",
"TaskUpdatePlugins": "Mettre à jour les extensions", "TaskUpdatePlugins": "Mettre à jour les extensions",
"TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.", "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre médiathèque.",
"TaskRefreshPeople": "Rafraîchir les acteurs", "TaskRefreshPeople": "Actualiser les acteurs",
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.", "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux", "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", "TaskRefreshLibrary": "Scanner la médiathèque",
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.", "TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre", "TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.", "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
"TaskCleanCache": "Vider le répertoire cache", "TaskCleanCache": "Vider le répertoire cache",
"TasksApplicationCategory": "Application", "TasksApplicationCategory": "Application",
"TasksLibraryCategory": "Bibliothèque", "TasksLibraryCategory": "Médiathèque",
"TasksMaintenanceCategory": "Maintenance", "TasksMaintenanceCategory": "Maintenance",
"TaskCleanActivityLogDescription": "Supprime les entrées du journal d'activité antérieures à l'âge configuré.", "TaskCleanActivityLogDescription": "Supprime les entrées du journal d'activité antérieures à l'âge configuré.",
"TaskCleanActivityLog": "Nettoyer le journal d'activité", "TaskCleanActivityLog": "Nettoyer le journal d'activité",
"Undefined": "Non défini", "Undefined": "Non défini",
"Forced": "Forcé", "Forced": "Forcé",
"Default": "Par défaut", "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", "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.", "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é", "TaskKeyframeExtractor": "Extracteur d'image clé",

View File

@ -122,5 +122,6 @@
"TaskOptimizeDatabase": "Optimiziraj bazu podataka", "TaskOptimizeDatabase": "Optimiziraj bazu podataka",
"External": "Vanjski", "External": "Vanjski",
"TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.", "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."
} }

View File

@ -120,5 +120,8 @@
"Forced": "강제하기", "Forced": "강제하기",
"Default": "기본 설정", "Default": "기본 설정",
"TaskOptimizeDatabaseDescription": "데이터베이스를 압축하고 사용 가능한 공간을 늘립니다. 라이브러리를 검색한 후 이 작업을 실행하거나 데이터베이스 수정같은 비슷한 작업을 수행하면 성능이 향상될 수 있습니다.", "TaskOptimizeDatabaseDescription": "데이터베이스를 압축하고 사용 가능한 공간을 늘립니다. 라이브러리를 검색한 후 이 작업을 실행하거나 데이터베이스 수정같은 비슷한 작업을 수행하면 성능이 향상될 수 있습니다.",
"TaskOptimizeDatabase": "데이터베이스 최적화" "TaskOptimizeDatabase": "데이터베이스 최적화",
"TaskKeyframeExtractorDescription": "비디오 파일에서 키프레임을 추출하여 더 정확한 HLS 재생 목록을 만듭니다. 이 작업은 오랫동안 진행될 수 있습니다.",
"TaskKeyframeExtractor": "키프레임 추출",
"External": "외부"
} }

View File

@ -39,7 +39,7 @@
"MixedContent": "Mixed content", "MixedContent": "Mixed content",
"Movies": "Filmai", "Movies": "Filmai",
"Music": "Muzika", "Music": "Muzika",
"MusicVideos": "Muzikiniai klipai", "MusicVideos": "Muzikiniai vaizdo įrašai",
"NameInstallFailed": "{0} diegimo klaida", "NameInstallFailed": "{0} diegimo klaida",
"NameSeasonNumber": "Sezonas {0}", "NameSeasonNumber": "Sezonas {0}",
"NameSeasonUnknown": "Sezonas neatpažintas", "NameSeasonUnknown": "Sezonas neatpažintas",

View File

@ -84,7 +84,7 @@
"CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}", "CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}",
"Books": "Grāmatas", "Books": "Grāmatas",
"Artists": "Izpildītāji", "Artists": "Izpildītāji",
"Albums": "Albumi", "Albums": "Albūmi",
"ProviderValue": "Provider: {0}", "ProviderValue": "Provider: {0}",
"HeaderFavoriteSongs": "Dziesmu Favorīti", "HeaderFavoriteSongs": "Dziesmu Favorīti",
"HeaderFavoriteShows": "Raidījumu 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.", "TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
"TaskCleanActivityLog": "Notīrīt Darbību Žurnālu", "TaskCleanActivityLog": "Notīrīt Darbību Žurnālu",
"Undefined": "Nenoteikts", "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.", "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"
} }

View File

@ -64,9 +64,9 @@
"CameraImageUploadedFrom": "Нова слика од камера беше поставена од {0}", "CameraImageUploadedFrom": "Нова слика од камера беше поставена од {0}",
"Books": "Книги", "Books": "Книги",
"AuthenticationSucceededWithUserName": "{0} успешно поврзан", "AuthenticationSucceededWithUserName": "{0} успешно поврзан",
"Artists": "Изведувач", "Artists": "Изведувачи",
"Application": "Апликација", "Application": "Апликација",
"AppDeviceValues": "Аплиакција: {0}, Уред: {1}", "AppDeviceValues": "Апликација: {0}, Уред: {1}",
"Albums": "Албуми", "Albums": "Албуми",
"VersionNumber": "Верзија {0}", "VersionNumber": "Верзија {0}",
"ValueSpecialEpisodeName": "Специјално - {0}", "ValueSpecialEpisodeName": "Специјално - {0}",
@ -100,5 +100,27 @@
"TasksMaintenanceCategory": "Одржување", "TasksMaintenanceCategory": "Одржување",
"Undefined": "Недефинирано", "Undefined": "Недефинирано",
"Forced": "Принудно", "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": "Надворешен"
} }

View File

@ -61,7 +61,7 @@
"NotificationOptionVideoPlayback": "Ulangmain video bermula", "NotificationOptionVideoPlayback": "Ulangmain video bermula",
"NotificationOptionVideoPlaybackStopped": "Ulangmain video dihentikan", "NotificationOptionVideoPlaybackStopped": "Ulangmain video dihentikan",
"Photos": "Gambar-gambar", "Photos": "Gambar-gambar",
"Playlists": "Senarai main", "Playlists": "Senarai ulangmain",
"Plugin": "Plugin", "Plugin": "Plugin",
"PluginInstalledWithName": "{0} telah dipasang", "PluginInstalledWithName": "{0} telah dipasang",
"PluginUninstalledWithName": "{0} telah dinyahpasang", "PluginUninstalledWithName": "{0} telah dinyahpasang",

View File

@ -6,97 +6,97 @@
"Artists": "အနုပညာရှင်များ", "Artists": "အနုပညာရှင်များ",
"Albums": "သီချင်းအခွေများ", "Albums": "သီချင်းအခွေများ",
"TaskOptimizeDatabaseDescription": "ဒေတာဘေ့စ်ကို ကျစ်လစ်စေပြီး နေရာလွတ်များကို ဖြတ်တောက်ပေးသည်။ စာကြည့်တိုက်ကို စကင်န်ဖတ်ပြီးနောက် ဤလုပ်ငန်းကို လုပ်ဆောင်ခြင်း သို့မဟုတ် ဒေတာဘေ့စ်မွမ်းမံမှုများ စွမ်းဆောင်ရည်ကို မြှင့်တင်ပေးနိုင်သည်ဟု ရည်ညွှန်းသော အခြားပြောင်းလဲမှုများကို လုပ်ဆောင်ခြင်း။.", "TaskOptimizeDatabaseDescription": "ဒေတာဘေ့စ်ကို ကျစ်လစ်စေပြီး နေရာလွတ်များကို ဖြတ်တောက်ပေးသည်။ စာကြည့်တိုက်ကို စကင်န်ဖတ်ပြီးနောက် ဤလုပ်ငန်းကို လုပ်ဆောင်ခြင်း သို့မဟုတ် ဒေတာဘေ့စ်မွမ်းမံမှုများ စွမ်းဆောင်ရည်ကို မြှင့်တင်ပေးနိုင်သည်ဟု ရည်ညွှန်းသော အခြားပြောင်းလဲမှုများကို လုပ်ဆောင်ခြင်း။.",
"TaskOptimizeDatabase": "ဒေတာဘေ့စ်ကို အကောင်းဆုံးဖြစ်အောင်လုပ်ပါ", "TaskOptimizeDatabase": "ဒေတာဘေ့စ်ကို အကောင်းဆုံးဖြစ်အောင်လုပ်ပါ",
"TaskDownloadMissingSubtitlesDescription": "မက်တာဒေတာ ဖွဲ့စည်းမှုပုံစံအပေါ် အခြေခံ၍ ပျောက်ဆုံးနေသော စာတန်းထိုးများအတွက် အင်တာနက်ကို ရှာဖွေသည်။", "TaskDownloadMissingSubtitlesDescription": "မက်တာဒေတာ ဖွဲ့စည်းမှုပုံစံအပေါ် အခြေခံ၍ ပျောက်ဆုံးနေသော စာတန်းထိုးများအတွက် အင်တာနက်ကို ရှာဖွေသည်။",
"TaskDownloadMissingSubtitles": "ပျောက်ဆုံးနေသော စာတန်းထိုးများကို ဒေါင်းလုဒ်လုပ်ပါ", "TaskDownloadMissingSubtitles": "ပျောက်ဆုံးနေသော စာတန်းထိုးများကို ဒေါင်းလုဒ်လုပ်ပါ",
"TaskRefreshChannelsDescription": "အင်တာနက်ချန်နယ်အချက်အလက်ကို ပြန်လည်စတင်သည်။", "TaskRefreshChannelsDescription": "အင်တာနက်ချန်နယ်အချက်အလက်ကို ပြန်လည်စတင်သည်။",
"TaskRefreshChannels": "ချန်နယ်များကို ပြန်လည်စတင်ပါ", "TaskRefreshChannels": "ချန်နယ်များကို ပြန်လည်စတင်ပါ",
"TaskCleanTranscodeDescription": "သက်တမ်း တစ်ရက်ထက်ပိုသော အသွင်ပြောင်းကုဒ်ဖိုင်များကို ဖျက်ပါ။", "TaskCleanTranscodeDescription": "သက်တမ်း တစ်ရက်ထက်ပိုသော အသွင်ပြောင်းကုဒ်ဖိုင်များကို ဖျက်ပါ။",
"TaskCleanTranscode": "Transcode လမ်းညွှန်ကို သန့်ရှင်းပါ", "TaskCleanTranscode": "Transcode လမ်းညွှန်ကို သန့်ရှင်းပါ",
"TaskUpdatePluginsDescription": "အလိုအလျောက် အပ်ဒိတ်လုပ်ရန် စီစဉ်ထားသော ပလပ်အင်များအတွက် အပ်ဒိတ်များကို ဒေါင်းလုဒ်လုပ်ပြီး ထည့်သွင်းပါ။", "TaskUpdatePluginsDescription": "အလိုအလျောက် အပ်ဒိတ်လုပ်ရန် စီစဉ်ထားသော ပလပ်အင်များအတွက် အပ်ဒိတ်များကို ဒေါင်းလုဒ်လုပ်ပြီး ထည့်သွင်းပါ။",
"TaskUpdatePlugins": "ပလပ်အင်များကို အပ်ဒိတ်လုပ်ပါ", "TaskUpdatePlugins": "ပလပ်အင်များကို အပ်ဒိတ်လုပ်ပါ",
"TaskRefreshPeopleDescription": "သင့်မီဒီယာစာကြည့်တိုက်ရှိ သရုပ်ဆောင်များနှင့် ဒါရိုက်တာများအတွက် မက်တာဒေတာကို အပ်ဒိတ်လုပ်ပါ။", "TaskRefreshPeopleDescription": "သင့်မီဒီယာစာကြည့်တိုက်ရှိ သရုပ်ဆောင်များနှင့် ဒါရိုက်တာများအတွက် မက်တာဒေတာကို အပ်ဒိတ်လုပ်ပါ။",
"TaskRefreshPeople": "လူများကို ပြန်လည်ဆန်းသစ်ပါ", "TaskRefreshPeople": "လူများကို ပြန်လည်ဆန်းသစ်ပါ",
"TaskCleanLogsDescription": "{0} ရက်ထက်ပိုသော မှတ်တမ်းဖိုင်များကို ဖျက်သည်။", "TaskCleanLogsDescription": "{0} ရက်ထက်ပိုသော မှတ်တမ်းဖိုင်များကို ဖျက်သည်။",
"TaskCleanLogs": "မှတ်တမ်းလမ်းညွှန်ကို သန့်ရှင်းပါ", "TaskCleanLogs": "မှတ်တမ်းလမ်းညွှန်ကို သန့်ရှင်းပါ",
"TaskRefreshLibraryDescription": "သင့်မီဒီယာဒစ်ဂျစ်တိုက်ကို ဖိုင်အသစ်များရှိမရှိ စကင်န်ဖတ်ပြီး ဖိုင်ရဲ့အကြောင်းအရာများ ကို ပြန်ပြုပြင်မွမ်းမံပါ။", "TaskRefreshLibraryDescription": "သင့်မီဒီယာဒစ်ဂျစ်တိုက်ကို ဖိုင်အသစ်များရှိမရှိ စကင်န်ဖတ်ပြီး ဖိုင်ရဲ့အကြောင်းအရာများ ကို ပြန်ပြုပြင်မွမ်းမံပါ။",
"TaskRefreshLibrary": "မီဒီယာစာကြည့်တိုက်ကို စကင်န်ဖတ်ပါ", "TaskRefreshLibrary": "မီဒီယာစာကြည့်တိုက်ကို စကင်န်ဖတ်ပါ",
"TaskRefreshChapterImagesDescription": "အခန်းများပါရှိသော ဗီဒီယိုများအတွက် ပုံသေးများကို ဖန်တီးပါ။", "TaskRefreshChapterImagesDescription": "အခန်းများပါရှိသော ဗီဒီယိုများအတွက် ပုံသေးများကို ဖန်တီးပါ။",
"TaskRefreshChapterImages": "အခန်းတစ်ခုစီ ပုံများကို ထုတ်ယူပါ", "TaskRefreshChapterImages": "အခန်းတစ်ခုစီ ပုံများကို ထုတ်ယူပါ",
"TaskCleanCacheDescription": "စနစ်မှ မလိုအပ်တော့သော ကက်ရှ်ဖိုင်များကို ဖျက်ပါ။.", "TaskCleanCacheDescription": "စနစ်မှ မလိုအပ်တော့သော ကက်ရှ်ဖိုင်များကို ဖျက်ပါ။.",
"TaskCleanCache": "Cache Directory ကို ရှင်းပါ", "TaskCleanCache": "Cache Directory ကို ရှင်းပါ",
"TaskCleanActivityLogDescription": "စီစဉ်သတ်မှတ်ထားသော အသက်ထက် ပိုကြီးသော လုပ်ဆောင်ချက်မှတ်တမ်းများကို ဖျက်ပါ။", "TaskCleanActivityLogDescription": "စီစဉ်သတ်မှတ်ထားသော အသက်ထက် ပိုကြီးသော လုပ်ဆောင်ချက်မှတ်တမ်းများကို ဖျက်ပါ။",
"TaskCleanActivityLog": "လုပ်ဆောင်ချက်မှတ်တမ်းကို ရှင်းလင်းပါ", "TaskCleanActivityLog": "လုပ်ဆောင်ချက်မှတ်တမ်းကို ရှင်းလင်းပါ",
"TasksChannelsCategory": "အင်တာနက် ချန်နယ်လိုင်းများ", "TasksChannelsCategory": "အင်တာနက် ချန်နယ်လိုင်းများ",
"TasksApplicationCategory": "အပလီကေးရှင်း", "TasksApplicationCategory": "အပလီကေးရှင်း",
"TasksLibraryCategory": "မီဒီယာတိုက်", "TasksLibraryCategory": "မီဒီယာတိုက်",
"TasksMaintenanceCategory": "ပြုပြင် ထိန်းသိမ်းခြင်း", "TasksMaintenanceCategory": "ပြုပြင် ထိန်းသိမ်းခြင်း",
"VersionNumber": "ဗားရှင်း {0}", "VersionNumber": "ဗားရှင်း {0}",
"ValueSpecialEpisodeName": "အထူး- {0}", "ValueSpecialEpisodeName": "အထူး- {0}",
"ValueHasBeenAddedToLibrary": "{0} ကို သင့်မီဒီယာဒစ်ဂျစ်တိုက်သို့ ပေါင်းထည့်လိုက်ပါပြီ", "ValueHasBeenAddedToLibrary": "{0} ကို သင့်မီဒီယာဒစ်ဂျစ်တိုက်သို့ ပေါင်းထည့်လိုက်ပါပြီ",
"UserStoppedPlayingItemWithValues": "{0} သည် {1} ကို {2} တွင် ဖွင့်ပြီးပါပြီ", "UserStoppedPlayingItemWithValues": "{0} သည် {1} ကို {2} တွင် ဖွင့်ပြီးပါပြီ",
"UserStartedPlayingItemWithValues": "{0} သည် {1} ကို {2} တွင် ပြသနေသည်", "UserStartedPlayingItemWithValues": "{0} သည် {1} ကို {2} တွင် ပြသနေသည်",
"UserPolicyUpdatedWithName": "{0} အတွက် အသုံးပြုသူမူဝါဒကို အပ်ဒိတ်လုပ်ပြီးပါပြီ", "UserPolicyUpdatedWithName": "{0} အတွက် အသုံးပြုသူမူဝါဒကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
"UserPasswordChangedWithName": "အသုံးပြုသူ {0} အတွက် စကားဝှက်ကို ပြောင်းထားသည်", "UserPasswordChangedWithName": "အသုံးပြုသူ {0} အတွက် စကားဝှက်ကို ပြောင်းထားသည်",
"UserOnlineFromDevice": "{0} သည် {1} မှ အွန်လိုင်းဖြစ်သည်", "UserOnlineFromDevice": "{0} သည် {1} မှ အွန်လိုင်းဖြစ်သည်",
"UserOfflineFromDevice": "{0} သည် {1} မှ ချိတ်ဆက်မှုပြတ်တောက်သွားသည်", "UserOfflineFromDevice": "{0} သည် {1} မှ ချိတ်ဆက်မှုပြတ်တောက်သွားသည်",
"UserLockedOutWithName": "အသုံးပြုသူ {0} အား လော့ခ်ချထားသည်", "UserLockedOutWithName": "အသုံးပြုသူ {0} အား လော့ခ်ချထားသည်",
"UserDownloadingItemWithValues": "{0} သည် {1} ကို ဒေါင်းလုဒ်လုပ်နေသည်", "UserDownloadingItemWithValues": "{0} သည် {1} ကို ဒေါင်းလုဒ်လုပ်နေသည်",
"UserDeletedWithName": "အသုံးပြုသူ {0} ကို ဖျက်လိုက်ပါပြီ", "UserDeletedWithName": "အသုံးပြုသူ {0} ကို ဖျက်လိုက်ပါပြီ",
"UserCreatedWithName": "အသုံးပြုသူ {0} ကို ဖန်တီးပြီးပါပြီ", "UserCreatedWithName": "အသုံးပြုသူ {0} ကို ဖန်တီးပြီးပါပြီ",
"User": "အသုံးပြုသူ", "User": "အသုံးပြုသူ",
"Undefined": "သတ်မှတ်မထားသော", "Undefined": "သတ်မှတ်မထားသော",
"TvShows": "တီဗီ ဇာတ်လမ်းတွဲများ", "TvShows": "တီဗီ ဇာတ်လမ်းတွဲများ",
"System": "စနစ်", "System": "စနစ်",
"Sync": "ထပ်တူကျသည်။", "Sync": "ထပ်တူကျသည်။",
"SubtitleDownloadFailureFromForItem": "{1} အတွက် {0} မှ စာတန်းထိုးများ ဒေါင်းလုဒ်လုပ်ခြင်း မအောင်မြင်ပါ", "SubtitleDownloadFailureFromForItem": "{1} အတွက် {0} မှ စာတန်းထိုးများ ဒေါင်းလုဒ်လုပ်ခြင်း မအောင်မြင်ပါ",
"StartupEmbyServerIsLoading": "Jellyfin ဆာဗာကို အသင့်ပြင်နေပါသည်။ ခဏနေ ထပ်စမ်းကြည့်ပါ။", "StartupEmbyServerIsLoading": "Jellyfin ဆာဗာကို အသင့်ပြင်နေပါသည်။ ခဏနေ ထပ်စမ်းကြည့်ပါ။",
"Songs": "သီချင်းများ", "Songs": "သီချင်းများ",
"Shows": "ဇာတ်လမ်းတွဲများ", "Shows": "ဇာတ်လမ်းတွဲများ",
"ServerNameNeedsToBeRestarted": "{0} ကို ပြန်လည်စတင်ရန် လိုအပ်သည်", "ServerNameNeedsToBeRestarted": "{0} ကို ပြန်လည်စတင်ရန် လိုအပ်သည်",
"ScheduledTaskStartedWithName": "{0} စတင်ခဲ့သည်", "ScheduledTaskStartedWithName": "{0} စတင်ခဲ့သည်",
"ScheduledTaskFailedWithName": "{0} မအောင်မြင်ပါ", "ScheduledTaskFailedWithName": "{0} မအောင်မြင်ပါ",
"ProviderValue": "ဝန်ဆောင်မှုပေးသူ- {0}", "ProviderValue": "ဝန်ဆောင်မှုပေးသူ- {0}",
"PluginUpdatedWithName": "ပလပ်ခ်အင် {0} ကို အပ်ဒိတ်လုပ်ထားသည်", "PluginUpdatedWithName": "ပလပ်ခ်အင် {0} ကို အပ်ဒိတ်လုပ်ထားသည်",
"PluginUninstalledWithName": "ပလပ်ခ်အင် {0} ကို ဖြုတ်လိုက်ပါပြီ", "PluginUninstalledWithName": "ပလပ်ခ်အင် {0} ကို ဖြုတ်လိုက်ပါပြီ",
"PluginInstalledWithName": "ပလပ်ခ်အင် {0} ကို ထည့်သွင်းခဲ့သည်", "PluginInstalledWithName": "ပလပ်ခ်အင် {0} ကို ထည့်သွင်းခဲ့သည်",
"Plugin": "ပလပ်အင်", "Plugin": "ပလပ်အင်",
"Playlists": "အစီအစဉ်များ", "Playlists": "အစီအစဉ်များ",
"Photos": "ဓာတ်ပုံများ", "Photos": "ဓာတ်ပုံများ",
"NotificationOptionVideoPlaybackStopped": "ဗီဒီယိုဖွင့်ခြင်း ရပ်သွားသည်", "NotificationOptionVideoPlaybackStopped": "ဗီဒီယိုဖွင့်ခြင်း ရပ်သွားသည်",
"NotificationOptionVideoPlayback": "ဗီဒီယိုဖွင့်ခြင်း စတင်ပါပြီ", "NotificationOptionVideoPlayback": "ဗီဒီယိုဖွင့်ခြင်း စတင်ပါပြီ",
"NotificationOptionUserLockedOut": "အသုံးပြုသူ ဝင်ရန် တားမြစ်ခံရသည်", "NotificationOptionUserLockedOut": "အသုံးပြုသူ ဝင်ရန် တားမြစ်ခံရသည်",
"NotificationOptionTaskFailed": "စီစဉ်ထားသော အလုပ်ပျက်ကွက်", "NotificationOptionTaskFailed": "စီစဉ်ထားသော အလုပ်ပျက်ကွက်",
"NotificationOptionServerRestartRequired": "ဆာဗာ ပြန်လည်စတင်ရန် လိုအပ်သည်", "NotificationOptionServerRestartRequired": "ဆာဗာ ပြန်လည်စတင်ရန် လိုအပ်သည်",
"NotificationOptionPluginUpdateInstalled": "ပလပ်အင် အပ်ဒိတ် ထည့်သွင်းပြီးပါပြီ", "NotificationOptionPluginUpdateInstalled": "ပလပ်အင် အပ်ဒိတ် ထည့်သွင်းပြီးပါပြီ",
"NotificationOptionPluginUninstalled": "ပလပ်အင်ကို ဖြုတ်လိုက်ပါပြီ", "NotificationOptionPluginUninstalled": "ပလပ်အင်ကို ဖြုတ်လိုက်ပါပြီ",
"NotificationOptionPluginInstalled": "ပလပ်အင် ထည့်သွင်းထားသည်", "NotificationOptionPluginInstalled": "ပလပ်အင် ထည့်သွင်းထားသည်",
"NotificationOptionPluginError": "ပလပ်အင် ချို့ယွင်းခြင်း", "NotificationOptionPluginError": "ပလပ်အင် ချို့ယွင်းခြင်း",
"NotificationOptionNewLibraryContent": "အသစ်များ ထပ်ထည့်ထားပါတယ်", "NotificationOptionNewLibraryContent": "အသစ်များ ထပ်ထည့်ထားပါတယ်",
"NotificationOptionInstallationFailed": "ထည့်သွင်းမှု မအောင်မြင်ပါ", "NotificationOptionInstallationFailed": "ထည့်သွင်းမှု မအောင်မြင်ပါ",
"NotificationOptionCameraImageUploaded": "ကင်မရာမှ ဓာတ်ပုံ အပ်လုဒ် ပြီးပါပြီ", "NotificationOptionCameraImageUploaded": "ကင်မရာမှ ဓာတ်ပုံ အပ်လုဒ် ပြီးပါပြီ",
"NotificationOptionAudioPlaybackStopped": "အသံဖိုင်ဖွင့်ခြင်း ရပ်သွားသည်", "NotificationOptionAudioPlaybackStopped": "အသံဖိုင်ဖွင့်ခြင်း ရပ်သွားသည်",
"NotificationOptionAudioPlayback": "အသံဖွင့်ခြင်း စတင်ပါပြီ", "NotificationOptionAudioPlayback": "အသံဖွင့်ခြင်း စတင်ပါပြီ",
"NotificationOptionApplicationUpdateInstalled": "အပလီကေးရှင်း အပ်ဒိတ်ကို ထည့်သွင်းထားသည်", "NotificationOptionApplicationUpdateInstalled": "အပလီကေးရှင်း အပ်ဒိတ်ကို ထည့်သွင်းထားသည်",
"NotificationOptionApplicationUpdateAvailable": "အပလီကေးရှင်း အပ်ဒိတ် ရနိုင်ပါပြီ", "NotificationOptionApplicationUpdateAvailable": "အပလီကေးရှင်း အပ်ဒိတ် ရနိုင်ပါပြီ",
"NewVersionIsAvailable": "Jellyfin Server ၏ ဗားရှင်းအသစ်ကို ဒေါင်းလုဒ်လုပ်နိုင်ပါပြီ။", "NewVersionIsAvailable": "Jellyfin Server ၏ ဗားရှင်းအသစ်ကို ဒေါင်းလုဒ်လုပ်နိုင်ပါပြီ။",
"NameSeasonUnknown": "ဇာတ်လမ်းတွဲ အပိုင်းမသိ", "NameSeasonUnknown": "ဇာတ်လမ်းတွဲ အပိုင်းမသိ",
"NameSeasonNumber": "ဇာတ်လမ်းတွဲ အပိုင်း {0}", "NameSeasonNumber": "ဇာတ်လမ်းတွဲ အပိုင်း {0}",
"NameInstallFailed": "{0} ထည့်သွင်းမှု မအောင်မြင်ပါ", "NameInstallFailed": "{0} ထည့်သွင်းမှု မအောင်မြင်ပါ",
"MusicVideos": "ဂီတဗီဒီယိုများ", "MusicVideos": "ဂီတဗီဒီယိုများ",
"Music": "တေးဂီတ", "Music": "တေးဂီတ",
"Movies": "ရုပ်ရှင်များ", "Movies": "ရုပ်ရှင်များ",
"MixedContent": "ရောနှောပါဝင်မှု", "MixedContent": "ရောနှောပါဝင်မှု",
"MessageServerConfigurationUpdated": "ဆာဗာဖွဲ့စည်းပုံကို အပ်ဒိတ်လုပ်ပြီးပါပြီ", "MessageServerConfigurationUpdated": "ဆာဗာဖွဲ့စည်းပုံကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
"MessageNamedServerConfigurationUpdatedWithValue": "ဆာဗာဖွဲ့စည်းပုံကဏ္ဍ {0} ကို အပ်ဒိတ်လုပ်ပြီးပါပြီ", "MessageNamedServerConfigurationUpdatedWithValue": "ဆာဗာဖွဲ့စည်းပုံကဏ္ဍ {0} ကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
"MessageApplicationUpdatedTo": "Jellyfin ဆာဗာကို {0} သို့ အပ်ဒိတ်လုပ်ထားသည်", "MessageApplicationUpdatedTo": "Jellyfin ဆာဗာကို {0} သို့ အပ်ဒိတ်လုပ်ထားသည်",
"MessageApplicationUpdated": "Jellyfin ဆာဗာကို အပ်ဒိတ်လုပ်ပြီးပါပြီ", "MessageApplicationUpdated": "Jellyfin ဆာဗာကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
"Latest": "နောက်ဆုံး", "Latest": "နောက်ဆုံး",
"LabelRunningTimeValue": "ကြာချိန် - {0}", "LabelRunningTimeValue": "ကြာချိန် - {0}",
"LabelIpAddressValue": "IP လိပ်စာ- {0}", "LabelIpAddressValue": "IP လိပ်စာ- {0}",
"ItemRemovedWithName": "{0} ကို ဒစ်ဂျစ်တိုက်မှ ဖယ်ရှားခဲ့သည်", "ItemRemovedWithName": "{0} ကို ဒစ်ဂျစ်တိုက်မှ ဖယ်ရှားခဲ့သည်",
"ItemAddedWithName": "{0} ကို စာကြည့်တိုက်သို့ ထည့်ထားသည်", "ItemAddedWithName": "{0} ကို စာကြည့်တိုက်သို့ ထည့်ထားသည်",
"Inherit": "ဆက်ခံ၍ လုပ်ဆောင်သည်", "Inherit": "ဆက်ခံ၍ လုပ်ဆောင်သည်",
"HomeVideos": "ကိုယ်တိုင်ရိုက် ဗီဒီယိုများ", "HomeVideos": "ကိုယ်တိုင်ရိုက် ဗီဒီယိုများ",
"HeaderRecordingGroups": "အသံဖမ်းအဖွဲ့များ", "HeaderRecordingGroups": "အသံဖမ်းအဖွဲ့များ",
"HeaderNextUp": "နောက်ထပ်", "HeaderNextUp": "နောက်ထပ်",
@ -106,18 +106,18 @@
"HeaderFavoriteEpisodes": "အကြိုက်ဆုံး ဇာတ်လမ်းအပိုင်းများ", "HeaderFavoriteEpisodes": "အကြိုက်ဆုံး ဇာတ်လမ်းအပိုင်းများ",
"HeaderFavoriteArtists": "အကြိုက်ဆုံးအနုပညာရှင်များ", "HeaderFavoriteArtists": "အကြိုက်ဆုံးအနုပညာရှင်များ",
"HeaderFavoriteAlbums": "အကြိုက်ဆုံး အယ်လ်ဘမ်များ", "HeaderFavoriteAlbums": "အကြိုက်ဆုံး အယ်လ်ဘမ်များ",
"HeaderContinueWatching": "ဆက်လက်ကြည့်ရှုပါ", "HeaderContinueWatching": "ဆက်လက်ကြည့်ရှုပါ",
"HeaderAlbumArtists": "အယ်လ်ဘမ်အနုပညာရှင်များ", "HeaderAlbumArtists": "အယ်လ်ဘမ်အနုပညာရှင်များ",
"Genres": "အမျိုးအစားများ", "Genres": "အမျိုးအစားများ",
"Forced": "အတင်းအကြပ်", "Forced": "အတင်းအကြပ်",
"Folders": "ဖိုလ်ဒါများ", "Folders": "ဖိုလ်ဒါများ",
"Favorites": "အကြိုက်ဆုံးများ", "Favorites": "အကြိုက်ဆုံးများ",
"FailedLoginAttemptWithUserName": "{0} မှ အကောင့်ဝင်ရန် မအောင်မြင်ပါ", "FailedLoginAttemptWithUserName": "{0} မှ အကောင့်ဝင်ရန် မအောင်မြင်ပါ",
"DeviceOnlineWithName": "{0} ကို ချိတ်ဆက်ထားသည်", "DeviceOnlineWithName": "{0} ကို ချိတ်ဆက်ထားသည်",
"DeviceOfflineWithName": "{0} နှင့် အဆက်ပြတ်သွားပါပြီ", "DeviceOfflineWithName": "{0} နှင့် အဆက်ပြတ်သွားပါပြီ",
"ChapterNameValue": "အခန်း {0}", "ChapterNameValue": "အခန်း {0}",
"CameraImageUploadedFrom": "ကင်မရာပုံအသစ်ကို {0} မှ ထည့်သွင်းလိုက်သည်", "CameraImageUploadedFrom": "ကင်မရာပုံအသစ်ကို {0} မှ ထည့်သွင်းလိုက်သည်",
"AuthenticationSucceededWithUserName": "{0} စစ်မှန်ကြောင်း အောင်မြင်စွာ အတည်ပြုပြီးပါပြီ", "AuthenticationSucceededWithUserName": "{0} အောင်မြင်စွာ စစ်မှန်ကြောင်း အတည်ပြုပြီးပါပြီ",
"Application": "အပလီကေးရှင်း", "Application": "အပလီကေးရှင်း",
"AppDeviceValues": "အက်ပ်- {0}၊ စက်- {1}", "AppDeviceValues": "အက်ပ်- {0}၊ စက်- {1}",
"External": "ပြင်ပ" "External": "ပြင်ပ"

View File

@ -8,15 +8,15 @@
"CameraImageUploadedFrom": "Uma nova imagem de câmara foi enviada a partir de {0}", "CameraImageUploadedFrom": "Uma nova imagem de câmara foi enviada a partir de {0}",
"Channels": "Canais", "Channels": "Canais",
"ChapterNameValue": "Capítulo {0}", "ChapterNameValue": "Capítulo {0}",
"Collections": "Coleções", "Collections": "Colecções",
"DeviceOfflineWithName": "{0} desligou-se", "DeviceOfflineWithName": "{0} desligou-se",
"DeviceOnlineWithName": "{0} ligou-se", "DeviceOnlineWithName": "{0} ligou-se",
"FailedLoginAttemptWithUserName": "Tentativa de login falhada a partir de {0}", "FailedLoginAttemptWithUserName": "Tentativa de login falhada a partir de {0}",
"Favorites": "Favoritos", "Favorites": "Favoritos",
"Folders": "Pastas", "Folders": "Pastas",
"Genres": "Géneros", "Genres": "Géneros",
"HeaderAlbumArtists": "Artistas do Álbum", "HeaderAlbumArtists": "Artistas do álbum",
"HeaderContinueWatching": "Continuar a Ver", "HeaderContinueWatching": "Continuar a ver",
"HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos", "HeaderFavoriteArtists": "Artistas Favoritos",
"HeaderFavoriteEpisodes": "Episódios Favoritos", "HeaderFavoriteEpisodes": "Episódios Favoritos",

View File

@ -1,5 +1,5 @@
{ {
"HeaderLiveTV": "TV Ao Vivo", "HeaderLiveTV": "TV Em Direto",
"Collections": "Coleções", "Collections": "Coleções",
"Books": "Livros", "Books": "Livros",
"Artists": "Artistas", "Artists": "Artistas",
@ -10,9 +10,9 @@
"HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteEpisodes": "Episódios Favoritos", "HeaderFavoriteEpisodes": "Episódios Favoritos",
"HeaderFavoriteShows": "Séries Favoritas", "HeaderFavoriteShows": "Séries Favoritas",
"HeaderContinueWatching": "Continuar assistindo", "HeaderContinueWatching": "Continuar a ver",
"HeaderAlbumArtists": "Artistas do Álbum", "HeaderAlbumArtists": "Artistas do Álbum",
"Genres": "Gêneros", "Genres": "Géneros",
"Folders": "Diretórios", "Folders": "Diretórios",
"Favorites": "Favoritos", "Favorites": "Favoritos",
"Channels": "Canais", "Channels": "Canais",
@ -74,7 +74,7 @@
"ItemRemovedWithName": "{0} foi removido da biblioteca", "ItemRemovedWithName": "{0} foi removido da biblioteca",
"ItemAddedWithName": "{0} foi adicionado à biblioteca", "ItemAddedWithName": "{0} foi adicionado à biblioteca",
"Inherit": "Herdar", "Inherit": "Herdar",
"HomeVideos": "Vídeos principais", "HomeVideos": "Vídeos Caseiros",
"HeaderRecordingGroups": "Grupos de Gravação", "HeaderRecordingGroups": "Grupos de Gravação",
"ValueSpecialEpisodeName": "Episódio Especial - {0}", "ValueSpecialEpisodeName": "Episódio Especial - {0}",
"Sync": "Sincronização", "Sync": "Sincronização",
@ -83,14 +83,14 @@
"Playlists": "Listas de Reprodução", "Playlists": "Listas de Reprodução",
"Photos": "Fotografias", "Photos": "Fotografias",
"Movies": "Filmes", "Movies": "Filmes",
"FailedLoginAttemptWithUserName": "Tentativa falha de login a partir de {0}", "FailedLoginAttemptWithUserName": "Tentativa de início de sessão falhada a partir de {0}",
"DeviceOnlineWithName": "{0} está conectado", "DeviceOnlineWithName": "{0} está ligado",
"DeviceOfflineWithName": "{0} desconectou-se", "DeviceOfflineWithName": "{0} desligou-se",
"ChapterNameValue": "Capítulo {0}", "ChapterNameValue": "Capítulo {0}",
"CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}", "CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}",
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
"Application": "Aplicativo", "Application": "Aplicação",
"AppDeviceValues": "Aplicativo {0}, Dispositivo: {1}", "AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
"TaskCleanCache": "Limpar Diretório de Cache", "TaskCleanCache": "Limpar Diretório de Cache",
"TasksApplicationCategory": "Aplicativo", "TasksApplicationCategory": "Aplicativo",
"TasksLibraryCategory": "Biblioteca", "TasksLibraryCategory": "Biblioteca",

View File

@ -90,7 +90,7 @@
"UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}", "UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}", "UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici", "ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
"ValueSpecialEpisodeName": "Bonus - {0}", "ValueSpecialEpisodeName": "Posebna epizoda - {0}",
"VersionNumber": "Različica {0}", "VersionNumber": "Različica {0}",
"TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise", "TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise",
"TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.", "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.", "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", "TaskOptimizeDatabase": "Optimiziraj bazo podatkov",
"TaskKeyframeExtractor": "Ekstraktor ključnih sličic", "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."
} }

View File

@ -86,7 +86,7 @@
"Channels": "Канали", "Channels": "Канали",
"CameraImageUploadedFrom": "Нова фотографија је учитана са {0}", "CameraImageUploadedFrom": "Нова фотографија је учитана са {0}",
"Books": "Књиге", "Books": "Књиге",
"AuthenticationSucceededWithUserName": "{0} Успешна аутентикација", "AuthenticationSucceededWithUserName": "{0} Успешна аутентификација",
"Artists": "Извођачи", "Artists": "Извођачи",
"Application": "Апликација", "Application": "Апликација",
"AppDeviceValues": "Апликација: {0}, Уређај: {1}", "AppDeviceValues": "Апликација: {0}, Уређај: {1}",
@ -118,7 +118,9 @@
"Undefined": "Недефинисано", "Undefined": "Недефинисано",
"Forced": "Принудно", "Forced": "Принудно",
"Default": "Подразумевано", "Default": "Подразумевано",
"TaskOptimizeDatabase": "Оптимизуј датабазу", "TaskOptimizeDatabase": "Оптимизуј банку података",
"TaskOptimizeDatabaseDescription": "Сажима базу података и скраћује слободан простор. Покретање овог задатка након скенирања библиотеке или других промена које подразумевају измене базе података које могу побољшати перформансе.", "TaskOptimizeDatabaseDescription": "Сажима базу података и скраћује слободан простор. Покретање овог задатка након скенирања библиотеке или других промена које подразумевају измене базе података које могу побољшати перформансе.",
"External": "Спољно" "External": "Спољно",
"TaskKeyframeExtractorDescription": "Екстрактује кљулне сличице из видео датотека да би креирао више преицзну HLS плеј-листу. Овај задатак може да потраје дуже време.",
"TaskKeyframeExtractor": "Екстрактор кључних сличица"
} }

View File

@ -3,7 +3,16 @@
"Channels": "قانال", "Channels": "قانال",
"CameraImageUploadedFrom": "{0} ئورۇندىن يېڭى سۈرەت چىقىرىلدى", "CameraImageUploadedFrom": "{0} ئورۇندىن يېڭى سۈرەت چىقىرىلدى",
"Books": "كىتاب", "Books": "كىتاب",
"AuthenticationSucceededWithUserName": "تىزىملىتىش مۇۋەپپەقىيەتلىك بول", "AuthenticationSucceededWithUserName": "{0} تەستىقلاش مۇۋاپىقىيەتلىك بولدى",
"Artists": "سەنئەتكار", "Artists": "سەنئەتكار",
"Albums": "پىلاستىنكا" "Albums": "پىلاستىنكا",
"DeviceOnlineWithName": "{0} ئۇلاندى",
"DeviceOfflineWithName": "{0} ئۈزۈلدى",
"Collections": "توپلام",
"Application": "ئەپ",
"AppDeviceValues": "ئەپ: {0}، ئۈسكۈنە: {1}",
"HeaderLiveTV": "تور تېلېۋىزىيەسى",
"Default": "سۈكۈتتىكى",
"Folders": "ھۆججەت خالتىسى",
"Favorites": "ساقلىغۇچ"
} }

View File

@ -120,5 +120,8 @@
"Default": "預設", "Default": "預設",
"TaskOptimizeDatabaseDescription": "壓縮數據庫並截斷可用空間。在掃描媒體庫或執行其他數據庫的修改後運行此任務可能會提高性能。", "TaskOptimizeDatabaseDescription": "壓縮數據庫並截斷可用空間。在掃描媒體庫或執行其他數據庫的修改後運行此任務可能會提高性能。",
"TaskOptimizeDatabase": "最佳化數據庫", "TaskOptimizeDatabase": "最佳化數據庫",
"TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。" "TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。",
"TaskKeyframeExtractorDescription": "提取關鍵格以創建更準確的HLS播放列表。次指示可能用時很長。",
"TaskKeyframeExtractor": "關鍵幀提取器",
"External": "外部"
} }

View File

@ -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
1 FI-S 1
2 FI-T 1
3 FI-7 4
4 FI-12 5
5 FI-16 8
6 FI-18 9
7 FI-K7 4
8 FI-K12 5
9 FI-K16 8
10 FI-K18 9

View File

@ -0,0 +1,6 @@
NO-A,1
NO-6,3
NO-9,4
NO-12,5
NO-15,8
NO-18,9
1 NO-A 1
2 NO-6 3
3 NO-9 4
4 NO-12 5
5 NO-15 8
6 NO-18 9

View File

@ -0,0 +1,5 @@
SE-Btl,1
SE-Barntillåten,1
SE-7,3
SE-11,5
SE-15,8
1 SE-Btl 1
2 SE-Barntillåten 1
3 SE-7 3
4 SE-11 5
5 SE-15 8

View File

@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.Net
} }
else 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 else
{ {
tcs.TrySetException(new Exception("SocketError: " + e.SocketError)); tcs.TrySetException(new SocketException((int)e.SocketError));
} }
} }
} }

View File

@ -1242,7 +1242,7 @@ namespace Emby.Server.Implementations.Session
if (item == null) 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<BaseItem>(); return Array.Empty<BaseItem>();
} }

View File

@ -6,7 +6,6 @@ using System.Linq;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
@ -37,7 +36,7 @@ namespace Emby.Server.Implementations.Session
private const float ForceKeepAliveFactor = 0.75f; private const float ForceKeepAliveFactor = 0.75f;
/// <summary> /// <summary>
/// Lock used for accesing the KeepAlive cancellation token. /// Lock used for accessing the KeepAlive cancellation token.
/// </summary> /// </summary>
private readonly object _keepAliveLock = new object(); private readonly object _keepAliveLock = new object();

View File

@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session namespace Emby.Server.Implementations.Session
{ {
public sealed class WebSocketController : ISessionController, IDisposable public sealed class WebSocketController : ISessionController, IAsyncDisposable, IDisposable
{ {
private readonly ILogger<WebSocketController> _logger; private readonly ILogger<WebSocketController> _logger;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
@ -99,6 +99,23 @@ namespace Emby.Server.Implementations.Session
foreach (var socket in _sockets) foreach (var socket in _sockets)
{ {
socket.Closed -= OnConnectionClosed; 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; _disposed = true;

View File

@ -3,7 +3,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Linq;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Sorting;

View File

@ -43,9 +43,9 @@ namespace Emby.Server.Implementations.TV
} }
string presentationUniqueKey = null; 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); presentationUniqueKey = GetUniqueSeriesKey(series);
} }
@ -93,9 +93,9 @@ namespace Emby.Server.Implementations.TV
string presentationUniqueKey = null; string presentationUniqueKey = null;
int? limit = 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); presentationUniqueKey = GetUniqueSeriesKey(series);
limit = 1; limit = 1;
@ -135,25 +135,20 @@ namespace Emby.Server.Implementations.TV
return GetResult(episodes, request); return GetResult(episodes, request);
} }
public IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions) private IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
{ {
// Avoid implicitly captured closure var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false));
var currentUser = user;
var allNextUp = seriesKeys
.Select(i => GetNextUp(i, currentUser, dtoOptions, false));
if (request.EnableRewatching) if (request.EnableRewatching)
{ {
allNextUp = allNextUp.Concat( allNextUp = allNextUp.Concat(
seriesKeys.Select(i => GetNextUp(i, currentUser, dtoOptions, true)) seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, true)))
) .OrderByDescending(i => i.LastWatchedDate);
.OrderByDescending(i => i.Item1);
} }
// If viewing all next up for all series, remove first episodes // If viewing all next up for all series, remove first episodes
// But if that returns empty, keep those first episodes (avoid completely empty view) // 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; var anyFound = false;
return allNextUp return allNextUp
@ -161,23 +156,18 @@ namespace Emby.Server.Implementations.TV
{ {
if (request.DisableFirstEpisode) 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; anyFound = true;
return true; return true;
} }
if (!anyFound && i.Item1 == DateTime.MinValue) return !anyFound && i.LastWatchedDate == DateTime.MinValue;
{
return true;
}
return false;
}) })
.Select(i => i.Item2()) .Select(i => i.GetEpisodeFunction())
.Where(i => i != null); .Where(i => i != null);
} }
@ -195,14 +185,14 @@ namespace Emby.Server.Implementations.TV
/// Gets the next up. /// Gets the next up.
/// </summary> /// </summary>
/// <returns>Task{Episode}.</returns> /// <returns>Task{Episode}.</returns>
private Tuple<DateTime, Func<Episode>> GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching) private (DateTime LastWatchedDate, Func<Episode> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
{ {
var lastQuery = new InternalItemsQuery(user) var lastQuery = new InternalItemsQuery(user)
{ {
AncestorWithPresentationUniqueKey = null, AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey, SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Episode }, IncludeItemTypes = new[] { BaseItemKind.Episode },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Descending) }, OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) },
IsPlayed = true, IsPlayed = true,
Limit = 1, Limit = 1,
ParentIndexNumberNotEquals = 0, ParentIndexNumberNotEquals = 0,
@ -216,24 +206,23 @@ namespace Emby.Server.Implementations.TV
if (rewatching) if (rewatching)
{ {
// find last watched by date played, not by newest episode watched // 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<Episode>().FirstOrDefault(); var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
Func<Episode> getEpisode = () => Episode GetEpisode()
{ {
var nextQuery = new InternalItemsQuery(user) var nextQuery = new InternalItemsQuery(user)
{ {
AncestorWithPresentationUniqueKey = null, AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey, SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Episode }, IncludeItemTypes = new[] { BaseItemKind.Episode },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
Limit = 1, Limit = 1,
IsPlayed = rewatching, IsPlayed = rewatching,
IsVirtualItem = false, IsVirtualItem = false,
ParentIndexNumberNotEquals = 0, ParentIndexNumberNotEquals = 0,
MinSortName = lastWatchedEpisode?.SortName,
DtoOptions = dtoOptions DtoOptions = dtoOptions
}; };
@ -297,7 +286,7 @@ namespace Emby.Server.Implementations.TV
} }
return nextEpisode; return nextEpisode;
}; }
if (lastWatchedEpisode != null) if (lastWatchedEpisode != null)
{ {
@ -305,11 +294,11 @@ namespace Emby.Server.Implementations.TV
var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1); var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
return new Tuple<DateTime, Func<Episode>>(lastWatchedDate, getEpisode); return (lastWatchedDate, GetEpisode);
} }
// Return the first episode // Return the first episode
return new Tuple<DateTime, Func<Episode>>(DateTime.MinValue, getEpisode); return (DateTime.MinValue, GetEpisode);
} }
private static QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, NextUpQuery query) private static QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, NextUpQuery query)

View File

@ -25,11 +25,6 @@ namespace Jellyfin.Api.Attributes
/// <param name="template">The route template. May not be null.</param> /// <param name="template">The route template. May not be null.</param>
public HttpSubscribeAttribute(string template) public HttpSubscribeAttribute(string template)
: base(_supportedMethods, template) : base(_supportedMethods, template)
{ => ArgumentNullException.ThrowIfNull(template, nameof(template));
if (template == null)
{
throw new ArgumentNullException(nameof(template));
}
}
} }
} }

View File

@ -25,11 +25,6 @@ namespace Jellyfin.Api.Attributes
/// <param name="template">The route template. May not be null.</param> /// <param name="template">The route template. May not be null.</param>
public HttpUnsubscribeAttribute(string template) public HttpUnsubscribeAttribute(string template)
: base(_supportedMethods, template) : base(_supportedMethods, template)
{ => ArgumentNullException.ThrowIfNull(template, nameof(template));
if (template == null)
{
throw new ArgumentNullException(nameof(template));
}
}
} }
} }

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.Net.Mime; using System.Net.Mime;
using Jellyfin.Api.Results;
using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -15,5 +17,40 @@ namespace Jellyfin.Api
JsonDefaults.PascalCaseMediaType)] JsonDefaults.PascalCaseMediaType)]
public class BaseJellyfinApiController : ControllerBase public class BaseJellyfinApiController : ControllerBase
{ {
/// <summary>
/// Create a new <see cref="OkResult{T}"/>.
/// </summary>
/// <param name="value">The value to return.</param>
/// <typeparam name="T">The type to return.</typeparam>
/// <returns>The <see cref="ActionResult{T}"/>.</returns>
protected ActionResult<IEnumerable<T>> Ok<T>(List<T> value)
=> new OkResult<IEnumerable<T>>(value);
/// <summary>
/// Create a new <see cref="OkResult{T}"/>.
/// </summary>
/// <param name="value">The value to return.</param>
/// <typeparam name="T">The type to return.</typeparam>
/// <returns>The <see cref="ActionResult{T}"/>.</returns>
protected ActionResult<IEnumerable<T>> Ok<T>(IReadOnlyList<T> value)
=> new OkResult<IEnumerable<T>>(value);
/// <summary>
/// Create a new <see cref="OkResult{T}"/>.
/// </summary>
/// <param name="value">The value to return.</param>
/// <typeparam name="T">The type to return.</typeparam>
/// <returns>The <see cref="ActionResult{T}"/>.</returns>
protected ActionResult<IEnumerable<T>> Ok<T>(IEnumerable<T>? value)
=> new OkResult<IEnumerable<T>?>(value);
/// <summary>
/// Create a new <see cref="OkResult{T}"/>.
/// </summary>
/// <param name="value">The value to return.</param>
/// <typeparam name="T">The type to return.</typeparam>
/// <returns>The <see cref="ActionResult{T}"/>.</returns>
protected ActionResult<T> Ok<T>(T value)
=> new OkResult<T>(value);
} }
} }

View File

@ -207,7 +207,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param> /// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param> /// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param> /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param> /// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param> /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param> /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>

View File

@ -2,7 +2,6 @@ using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Net.Mime; using System.Net.Mime;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.ConfigurationDtos; using Jellyfin.Api.Models.ConfigurationDtos;

View File

@ -89,12 +89,9 @@ namespace Jellyfin.Api.Controllers
// Load all custom display preferences // Load all custom display preferences
var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client); 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. // This will essentially be a noop if no changes have been made, but new prefs must be saved at least.

View File

@ -54,7 +54,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult GetDescriptionXml([FromRoute, Required] string serverId) public ActionResult<string> GetDescriptionXml([FromRoute, Required] string serverId)
{ {
var url = GetAbsoluteUri(); var url = GetAbsoluteUri();
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase)); var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
@ -77,7 +77,7 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetContentDirectory([FromRoute, Required] string serverId) public ActionResult<string> GetContentDirectory([FromRoute, Required] string serverId)
{ {
return Ok(_contentDirectory.GetServiceXml()); return Ok(_contentDirectory.GetServiceXml());
} }
@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId) public ActionResult<string> GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
{ {
return Ok(_mediaReceiverRegistrar.GetServiceXml()); return Ok(_mediaReceiverRegistrar.GetServiceXml());
} }
@ -117,7 +117,7 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Text.Xml)] [Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)] [ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetConnectionManager([FromRoute, Required] string serverId) public ActionResult<string> GetConnectionManager([FromRoute, Required] string serverId)
{ {
return Ok(_connectionManager.GetServiceXml()); return Ok(_connectionManager.GetServiceXml());
} }

View File

@ -121,7 +121,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param> /// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param> /// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param> /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param> /// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param> /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param> /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@ -1790,7 +1790,8 @@ namespace Jellyfin.Api.Controllers
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{ {
if (EncodingHelper.IsCopyCodec(codec) 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, "dvh1", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.VideoStream.CodecTag, "dvhe", 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. // Set the key frame params for video encoding to match the hls segment time.
args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, isEventPlaylist, startNumber); 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)) if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
{ {
args += " -bf 0"; args += " -bf 0";

View File

@ -1,6 +1,7 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
@ -9,6 +10,7 @@ using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -32,6 +34,7 @@ namespace Jellyfin.Api.Controllers
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly IDtoService _dtoService; private readonly IDtoService _dtoService;
private readonly IAuthorizationContext _authContext;
private readonly ILogger<ItemsController> _logger; private readonly ILogger<ItemsController> _logger;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
@ -42,6 +45,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
public ItemsController( public ItemsController(
@ -49,6 +53,7 @@ namespace Jellyfin.Api.Controllers
ILibraryManager libraryManager, ILibraryManager libraryManager,
ILocalizationManager localization, ILocalizationManager localization,
IDtoService dtoService, IDtoService dtoService,
IAuthorizationContext authContext,
ILogger<ItemsController> logger, ILogger<ItemsController> logger,
ISessionManager sessionManager) ISessionManager sessionManager)
{ {
@ -56,6 +61,7 @@ namespace Jellyfin.Api.Controllers
_libraryManager = libraryManager; _libraryManager = libraryManager;
_localization = localization; _localization = localization;
_dtoService = dtoService; _dtoService = dtoService;
_authContext = authContext;
_logger = logger; _logger = logger;
_sessionManager = sessionManager; _sessionManager = sessionManager;
} }
@ -63,7 +69,7 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Gets items based on a query. /// Gets items based on a query.
/// </summary> /// </summary>
/// <param name="userId">The user id supplied as query parameter.</param> /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param>
/// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param> /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
/// <param name="hasThemeSong">Optional filter by items with theme songs.</param> /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
/// <param name="hasThemeVideo">Optional filter by items with theme videos.</param> /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
@ -151,15 +157,15 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
[HttpGet("Items")] [HttpGet("Items")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetItems( public async Task<ActionResult<QueryResult<BaseItemDto>>> GetItems(
[FromQuery] Guid userId, [FromQuery] Guid? userId,
[FromQuery] string? maxOfficialRating, [FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong, [FromQuery] bool? hasThemeSong,
[FromQuery] bool? hasThemeVideo, [FromQuery] bool? hasThemeVideo,
[FromQuery] bool? hasSubtitles, [FromQuery] bool? hasSubtitles,
[FromQuery] bool? hasSpecialFeature, [FromQuery] bool? hasSpecialFeature,
[FromQuery] bool? hasTrailer, [FromQuery] bool? hasTrailer,
[FromQuery] string? adjacentTo, [FromQuery] Guid? adjacentTo,
[FromQuery] int? parentIndexNumber, [FromQuery] int? parentIndexNumber,
[FromQuery] bool? hasParentalRating, [FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd, [FromQuery] bool? isHd,
@ -238,7 +244,19 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = 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 } var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@ -270,30 +288,39 @@ namespace Jellyfin.Api.Controllers
includeItemTypes = new[] { BaseItemKind.Playlist }; includeItemTypes = new[] { BaseItemKind.Playlist };
} }
var enabledChannels = user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels); var enabledChannels = auth.IsApiKey
? Array.Empty<Guid>()
: user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels);
bool isInEnabledFolder = Array.IndexOf(user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1 // api keys are always enabled for all folders
bool isInEnabledFolder = auth.IsApiKey
|| Array.IndexOf(user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1
// Assume all folders inside an EnabledChannel are enabled // Assume all folders inside an EnabledChannel are enabled
|| Array.IndexOf(enabledChannels, item.Id) != -1 || Array.IndexOf(enabledChannels, item.Id) != -1
// Assume all items inside an EnabledChannel are enabled // Assume all items inside an EnabledChannel are enabled
|| Array.IndexOf(enabledChannels, item.ChannelId) != -1; || Array.IndexOf(enabledChannels, item.ChannelId) != -1;
var collectionFolders = _libraryManager.GetCollectionFolders(item); if (!isInEnabledFolder)
foreach (var collectionFolder in collectionFolders)
{ {
if (user.GetPreferenceValues<Guid>(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<Guid>(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 if (item is not UserRootFolder
&& !isInEnabledFolder && !isInEnabledFolder
&& !user.HasPermission(PermissionKind.EnableAllFolders) && !user!.HasPermission(PermissionKind.EnableAllFolders)
&& !user.HasPermission(PermissionKind.EnableAllChannels) && !user.HasPermission(PermissionKind.EnableAllChannels)
&& !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase)) && !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}."); return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
} }
@ -606,7 +633,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
[HttpGet("Users/{userId}/Items")] [HttpGet("Users/{userId}/Items")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserId( public Task<ActionResult<QueryResult<BaseItemDto>>> GetItemsByUserId(
[FromRoute] Guid userId, [FromRoute] Guid userId,
[FromQuery] string? maxOfficialRating, [FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong, [FromQuery] bool? hasThemeSong,
@ -614,7 +641,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasSubtitles, [FromQuery] bool? hasSubtitles,
[FromQuery] bool? hasSpecialFeature, [FromQuery] bool? hasSpecialFeature,
[FromQuery] bool? hasTrailer, [FromQuery] bool? hasTrailer,
[FromQuery] string? adjacentTo, [FromQuery] Guid? adjacentTo,
[FromQuery] int? parentIndexNumber, [FromQuery] int? parentIndexNumber,
[FromQuery] bool? hasParentalRating, [FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd, [FromQuery] bool? isHd,

View File

@ -12,7 +12,6 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;

View File

@ -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<RecommendationDto> GetWithDirector( private IEnumerable<RecommendationDto> GetWithDirector(

View File

@ -98,7 +98,10 @@ namespace Jellyfin.Api.Controllers
Limit = limit ?? 0 Limit = limit ?? 0
}); });
return new QueryResult<BaseItemDto>(peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray()); return new QueryResult<BaseItemDto>(
peopleItems
.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user))
.ToArray());
} }
/// <summary> /// <summary>

View File

@ -39,7 +39,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>Whether Quick Connect is enabled on the server or not.</returns> /// <returns>Whether Quick Connect is enabled on the server or not.</returns>
[HttpGet("Enabled")] [HttpGet("Enabled")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<bool> GetEnabled() public ActionResult<bool> GetQuickConnectEnabled()
{ {
return _quickConnect.IsEnabled; return _quickConnect.IsEnabled;
} }
@ -52,7 +52,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns> /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
[HttpGet("Initiate")] [HttpGet("Initiate")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<QuickConnectResult>> Initiate() public async Task<ActionResult<QuickConnectResult>> InitiateQuickConnect()
{ {
try try
{ {
@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Connect")] [HttpGet("Connect")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QuickConnectResult> Connect([FromQuery, Required] string secret) public ActionResult<QuickConnectResult> GetQuickConnectState([FromQuery, Required] string secret)
{ {
try try
{ {
@ -102,7 +102,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<bool>> Authorize([FromQuery, Required] string code) public async Task<ActionResult<bool>> AuthorizeQuickConnect([FromQuery, Required] string code)
{ {
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User); var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
if (!userId.HasValue) if (!userId.HasValue)

View File

@ -60,9 +60,9 @@ namespace Jellyfin.Api.Controllers
/// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param> /// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param>
/// <param name="searchTerm">The search term to filter on.</param> /// <param name="searchTerm">The search term to filter on.</param>
/// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimeted.</param> /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimited.</param>
/// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimeted.</param> /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimited.</param>
/// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimeted.</param> /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimited.</param>
/// <param name="parentId">If specified, only children of the parent are returned.</param> /// <param name="parentId">If specified, only children of the parent are returned.</param>
/// <param name="isMovie">Optional filter for movies.</param> /// <param name="isMovie">Optional filter for movies.</param>
/// <param name="isSeries">Optional filter for series.</param> /// <param name="isSeries">Optional filter for series.</param>
@ -79,7 +79,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet] [HttpGet]
[Description("Gets search hints based on a search term")] [Description("Gets search hints based on a search term")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<SearchHintResult> Get( public ActionResult<SearchHintResult> GetSearchHints(
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
@ -140,7 +140,7 @@ namespace Jellyfin.Api.Controllers
IndexNumber = item.IndexNumber, IndexNumber = item.IndexNumber,
ParentIndexNumber = item.ParentIndexNumber, ParentIndexNumber = item.ParentIndexNumber,
Id = item.Id, Id = item.Id,
Type = item.GetClientTypeName(), Type = item.GetBaseItemKind(),
MediaType = item.MediaType, MediaType = item.MediaType,
MatchedTerm = hintInfo.MatchedTerm, MatchedTerm = hintInfo.MatchedTerm,
RunTimeTicks = item.RunTimeTicks, RunTimeTicks = item.RunTimeTicks,
@ -149,8 +149,10 @@ namespace Jellyfin.Api.Controllers
EndDate = item.EndDate EndDate = item.EndDate
}; };
// legacy #pragma warning disable CS0618
// Kept for compatibility with older clients
result.ItemId = result.Id; result.ItemId = result.Id;
#pragma warning restore CS0618
if (item.IsFolder) if (item.IsFolder)
{ {

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
@ -31,7 +32,7 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Finds movies and trailers similar to a given trailer. /// Finds movies and trailers similar to a given trailer.
/// </summary> /// </summary>
/// <param name="userId">The user id.</param> /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param>
/// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param> /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
/// <param name="hasThemeSong">Optional filter by items with theme songs.</param> /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
/// <param name="hasThemeVideo">Optional filter by items with theme videos.</param> /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
@ -118,15 +119,15 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
[HttpGet] [HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetTrailers( public Task<ActionResult<QueryResult<BaseItemDto>>> GetTrailers(
[FromQuery] Guid userId, [FromQuery] Guid? userId,
[FromQuery] string? maxOfficialRating, [FromQuery] string? maxOfficialRating,
[FromQuery] bool? hasThemeSong, [FromQuery] bool? hasThemeSong,
[FromQuery] bool? hasThemeVideo, [FromQuery] bool? hasThemeVideo,
[FromQuery] bool? hasSubtitles, [FromQuery] bool? hasSubtitles,
[FromQuery] bool? hasSpecialFeature, [FromQuery] bool? hasSpecialFeature,
[FromQuery] bool? hasTrailer, [FromQuery] bool? hasTrailer,
[FromQuery] string? adjacentTo, [FromQuery] Guid? adjacentTo,
[FromQuery] int? parentIndexNumber, [FromQuery] int? parentIndexNumber,
[FromQuery] bool? hasParentalRating, [FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd, [FromQuery] bool? isHd,

View File

@ -77,7 +77,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? seriesId, [FromQuery] Guid? seriesId,
[FromQuery] Guid? parentId, [FromQuery] Guid? parentId,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? season, [FromQuery] int? season,
[FromQuery] Guid? seasonId, [FromQuery] Guid? seasonId,
[FromQuery] bool? isMissing, [FromQuery] bool? isMissing,
[FromQuery] string? adjacentTo, [FromQuery] Guid? adjacentTo,
[FromQuery] Guid? startItemId, [FromQuery] Guid? startItemId,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
@ -278,9 +278,9 @@ namespace Jellyfin.Api.Controllers
} }
// This must be the last filter // 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)) if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
@ -326,7 +326,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? isSpecialSeason, [FromQuery] bool? isSpecialSeason,
[FromQuery] bool? isMissing, [FromQuery] bool? isMissing,
[FromQuery] string? adjacentTo, [FromQuery] Guid? adjacentTo,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,

View File

@ -10,13 +10,11 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -31,7 +29,6 @@ namespace Jellyfin.Api.Controllers
public class UniversalAudioController : BaseJellyfinApiController public class UniversalAudioController : BaseJellyfinApiController
{ {
private readonly IAuthorizationContext _authorizationContext; private readonly IAuthorizationContext _authorizationContext;
private readonly IDeviceManager _deviceManager;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly ILogger<UniversalAudioController> _logger; private readonly ILogger<UniversalAudioController> _logger;
private readonly MediaInfoHelper _mediaInfoHelper; private readonly MediaInfoHelper _mediaInfoHelper;
@ -42,7 +39,6 @@ namespace Jellyfin.Api.Controllers
/// Initializes a new instance of the <see cref="UniversalAudioController"/> class. /// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
/// </summary> /// </summary>
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param> /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param>
/// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param> /// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param>
@ -50,7 +46,6 @@ namespace Jellyfin.Api.Controllers
/// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param> /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
public UniversalAudioController( public UniversalAudioController(
IAuthorizationContext authorizationContext, IAuthorizationContext authorizationContext,
IDeviceManager deviceManager,
ILibraryManager libraryManager, ILibraryManager libraryManager,
ILogger<UniversalAudioController> logger, ILogger<UniversalAudioController> logger,
MediaInfoHelper mediaInfoHelper, MediaInfoHelper mediaInfoHelper,
@ -58,7 +53,6 @@ namespace Jellyfin.Api.Controllers
DynamicHlsHelper dynamicHlsHelper) DynamicHlsHelper dynamicHlsHelper)
{ {
_authorizationContext = authorizationContext; _authorizationContext = authorizationContext;
_deviceManager = deviceManager;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_logger = logger; _logger = logger;
_mediaInfoHelper = mediaInfoHelper; _mediaInfoHelper = mediaInfoHelper;
@ -117,76 +111,61 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool enableRedirection = true) [FromQuery] bool enableRedirection = true)
{ {
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); 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); var authInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile); _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( var info = await _mediaInfoHelper.GetPlaybackInfo(
itemId, itemId,
userId, userId,
mediaSourceId) mediaSourceId)
.ConfigureAwait(false); .ConfigureAwait(false);
if (deviceProfile != null) // set device specific data
var item = _libraryManager.GetItemById(itemId);
foreach (var sourceInfo in info.MediaSources)
{ {
// set device specific data _mediaInfoHelper.SetDeviceSpecificData(
var item = _libraryManager.GetItemById(itemId); item,
sourceInfo,
foreach (var sourceInfo in info.MediaSources) deviceProfile,
{ authInfo,
_mediaInfoHelper.SetDeviceSpecificData( maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
item, startTimeTicks ?? 0,
sourceInfo, mediaSourceId ?? string.Empty,
deviceProfile, null,
authInfo, null,
maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate, maxAudioChannels,
startTimeTicks ?? 0, info.PlaySessionId!,
mediaSourceId ?? string.Empty, userId ?? Guid.Empty,
null, true,
null, true,
maxAudioChannels, true,
info.PlaySessionId!, true,
userId ?? Guid.Empty, true,
true, Request.HttpContext.GetNormalizedRemoteIp());
true,
true,
true,
true,
Request.HttpContext.GetNormalizedRemoteIp());
}
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
} }
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]; var mediaSource = info.MediaSources[0];
if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http) if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http && enableRedirection && mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value)
{ {
if (enableRedirection) return Redirect(mediaSource.Path);
{
if (mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value)
{
return Redirect(mediaSource.Path);
}
}
} }
var isStatic = mediaSource.SupportsDirectStream; var isStatic = mediaSource.SupportsDirectStream;
@ -249,7 +228,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames, BreakOnNonKeyFrames = breakOnNonKeyFrames,
AudioSampleRate = maxAudioSampleRate, AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = isStatic ? (int?)null : (audioBitRate ?? maxStreamingBitrate), AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
MaxAudioBitDepth = maxAudioBitDepth, MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = maxAudioChannels, AudioChannels = maxAudioChannels,
CopyTimestamps = true, CopyTimestamps = true,

View File

@ -282,16 +282,19 @@ namespace Jellyfin.Api.Controllers
} }
else else
{ {
var success = await _userManager.AuthenticateUser( if (!HttpContext.User.IsInRole(UserRoles.Administrator))
user.Username,
request.CurrentPw,
request.CurrentPw,
HttpContext.GetNormalizedRemoteIp().ToString(),
false).ConfigureAwait(false);
if (success == null)
{ {
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); await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
@ -499,7 +502,7 @@ namespace Jellyfin.Api.Controllers
if (isLocal) 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); var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false);

View File

@ -8,7 +8,6 @@ using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
@ -234,7 +233,8 @@ namespace Jellyfin.Api.Controllers
var dtoOptions = new DtoOptions().AddClientFields(Request); var dtoOptions = new DtoOptions().AddClientFields(Request);
return Ok(item 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))); .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
} }

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
@ -11,9 +10,7 @@ using Jellyfin.Api.Models.UserViewDtos;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Library; using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -32,7 +29,6 @@ namespace Jellyfin.Api.Controllers
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IUserViewManager _userViewManager; private readonly IUserViewManager _userViewManager;
private readonly IDtoService _dtoService; private readonly IDtoService _dtoService;
private readonly IAuthorizationContext _authContext;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
/// <summary> /// <summary>
@ -41,19 +37,16 @@ namespace Jellyfin.Api.Controllers
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param> /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
public UserViewsController( public UserViewsController(
IUserManager userManager, IUserManager userManager,
IUserViewManager userViewManager, IUserViewManager userViewManager,
IDtoService dtoService, IDtoService dtoService,
IAuthorizationContext authContext,
ILibraryManager libraryManager) ILibraryManager libraryManager)
{ {
_userManager = userManager; _userManager = userManager;
_userViewManager = userViewManager; _userViewManager = userViewManager;
_dtoService = dtoService; _dtoService = dtoService;
_authContext = authContext;
_libraryManager = libraryManager; _libraryManager = libraryManager;
} }
@ -138,7 +131,8 @@ namespace Jellyfin.Api.Controllers
Name = i.Name, Name = i.Name,
Id = i.Id.ToString("N", CultureInfo.InvariantCulture) Id = i.Id.ToString("N", CultureInfo.InvariantCulture)
}) })
.OrderBy(i => i.Name)); .OrderBy(i => i.Name)
.AsEnumerable());
} }
} }
} }

View File

@ -654,7 +654,7 @@ namespace Jellyfin.Api.Helpers
{ {
if (EnableThrottling(state)) if (EnableThrottling(state))
{ {
transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem); transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem, _mediaEncoder);
transcodingJob.TranscodingThrottler.Start(); transcodingJob.TranscodingThrottler.Start();
} }
} }

View File

@ -17,10 +17,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.1" /> <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,6 +2,7 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -17,6 +18,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
private readonly ILogger<TranscodingThrottler> _logger; private readonly ILogger<TranscodingThrottler> _logger;
private readonly IConfigurationManager _config; private readonly IConfigurationManager _config;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IMediaEncoder _mediaEncoder;
private Timer? _timer; private Timer? _timer;
private bool _isPaused; private bool _isPaused;
@ -27,12 +29,14 @@ namespace Jellyfin.Api.Models.PlaybackDtos
/// <param name="logger">Instance of the <see cref="ILogger{TranscodingThrottler}"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{TranscodingThrottler}"/> interface.</param>
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param> /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem) /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder)
{ {
_job = job; _job = job;
_logger = logger; _logger = logger;
_config = config; _config = config;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_mediaEncoder = mediaEncoder;
} }
/// <summary> /// <summary>
@ -55,7 +59,8 @@ namespace Jellyfin.Api.Models.PlaybackDtos
try 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; _isPaused = false;
} }
catch (Exception ex) catch (Exception ex)
@ -125,11 +130,13 @@ namespace Jellyfin.Api.Models.PlaybackDtos
{ {
if (!_isPaused) 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 try
{ {
await _job.Process!.StandardInput.WriteAsync("c").ConfigureAwait(false); await _job.Process!.StandardInput.WriteAsync(pauseKey).ConfigureAwait(false);
_isPaused = true; _isPaused = true;
} }
catch (Exception ex) catch (Exception ex)

View File

@ -169,7 +169,7 @@ namespace Jellyfin.Api.Models.StreamingDtos
/// <summary> /// <summary>
/// Disposes the stream state. /// Disposes the stream state.
/// </summary> /// </summary>
/// <param name="disposing">Whether the object is currently beeing disposed.</param> /// <param name="disposing">Whether the object is currently being disposed.</param>
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (_disposed) if (_disposed)

View File

@ -17,9 +17,9 @@ namespace Jellyfin.Api.Models.SyncPlayDtos
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <value>The playlist identifiers ot the items.</value> /// <value>The playlist identifiers of the items.</value>
public IReadOnlyList<Guid> PlaylistItemIds { get; set; } public IReadOnlyList<Guid> PlaylistItemIds { get; set; }
/// <summary> /// <summary>

View File

@ -0,0 +1,21 @@
#pragma warning disable SA1649 // File name should match type name.
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Results;
/// <summary>
/// Ok result with type specified.
/// </summary>
/// <typeparam name="T">The type to return.</typeparam>
public class OkResult<T> : OkObjectResult
{
/// <summary>
/// Initializes a new instance of the <see cref="OkResult{T}"/> class.
/// </summary>
/// <param name="value">The value to return.</param>
public OkResult(T value)
: base(value)
{
}
}

View File

@ -18,8 +18,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BlurHashSharp" Version="1.2.0" /> <PackageReference Include="BlurHashSharp" Version="1.2.0" />
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" /> <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.71" /> <PackageReference Include="SkiaSharp" Version="2.88.2" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1-preview.71" /> <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.2" />
<PackageReference Include="SkiaSharp.Svg" Version="1.60.0" /> <PackageReference Include="SkiaSharp.Svg" Version="1.60.0" />
</ItemGroup> </ItemGroup>

View File

@ -145,9 +145,11 @@ namespace Jellyfin.Drawing.Skia
/// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception> /// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception>
public string GetImageBlurHash(int xComp, int yComp, string path) 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('.'); var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');

View File

@ -13,7 +13,7 @@ namespace Jellyfin.Drawing.Skia
/// </summary> /// </summary>
/// <param name="skiaEncoder">The current skia encoder.</param> /// <param name="skiaEncoder">The current skia encoder.</param>
/// <param name="paths">The list of image paths.</param> /// <param name="paths">The list of image paths.</param>
/// <param name="currentIndex">The current checked indes.</param> /// <param name="currentIndex">The current checked index.</param>
/// <param name="newIndex">The new index.</param> /// <param name="newIndex">The new index.</param>
/// <returns>A valid bitmap, or null if no bitmap exists after <c>currentIndex</c>.</returns> /// <returns>A valid bitmap, or null if no bitmap exists after <c>currentIndex</c>.</returns>
public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList<string> paths, int currentIndex, out int newIndex) public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList<string> paths, int currentIndex, out int newIndex)

View File

@ -27,13 +27,13 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Linq.Async" Version="6.0.1" /> <PackageReference Include="System.Linq.Async" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.6"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.6"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.9">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@ -67,7 +67,7 @@ namespace Jellyfin.Server.Implementations.Users
else if (string.Equals( else if (string.Equals(
spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal), spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal),
pin.Replace("-", string.Empty, StringComparison.Ordinal), pin.Replace("-", string.Empty, StringComparison.Ordinal),
StringComparison.OrdinalIgnoreCase)) StringComparison.Ordinal))
{ {
var resetUser = userManager.GetUserByName(spr.UserName) var resetUser = userManager.GetUserByName(spr.UserName)
?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); ?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");

View File

@ -79,7 +79,7 @@ namespace Jellyfin.Server.Implementations.Users
} }
/// <inheritdoc /> /// <inheritdoc />
public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string> customPreferences) public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string?> customPreferences)
{ {
var existingPrefs = _dbContext.CustomItemDisplayPreferences var existingPrefs = _dbContext.CustomItemDisplayPreferences
.AsQueryable() .AsQueryable()

View File

@ -326,10 +326,10 @@ namespace Jellyfin.Server.Implementations.Users
EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay, EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay,
RememberSubtitleSelections = user.RememberSubtitleSelections, RememberSubtitleSelections = user.RememberSubtitleSelections,
SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty, SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty,
OrderedViews = user.GetPreference(PreferenceKind.OrderedViews), OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
GroupedFolders = user.GetPreference(PreferenceKind.GroupedFolders), GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
MyMediaExcludes = user.GetPreference(PreferenceKind.MyMediaExcludes), MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes),
LatestItemsExcludes = user.GetPreference(PreferenceKind.LatestItemExcludes) LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes)
}, },
Policy = new UserPolicy Policy = new UserPolicy
{ {

View File

@ -69,15 +69,8 @@ namespace Jellyfin.Server.Infrastructure
/// <inheritdoc /> /// <inheritdoc />
protected override Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength) protected override Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
{ {
if (context == null) ArgumentNullException.ThrowIfNull(context, nameof(context));
{ ArgumentNullException.ThrowIfNull(result, nameof(result));
throw new ArgumentNullException(nameof(context));
}
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (range != null && rangeLength == 0) if (range != null && rangeLength == 0)
{ {

View File

@ -37,18 +37,18 @@
<PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.6" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.9" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.6" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.9" />
<PackageReference Include="prometheus-net" Version="6.0.0" /> <PackageReference Include="prometheus-net" Version="6.0.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" /> <PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" /> <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Graylog" Version="2.3.0" /> <PackageReference Include="Serilog.Sinks.Graylog" Version="2.3.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.0" /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -243,7 +243,7 @@ namespace Jellyfin.Server
} }
} }
appHost.Dispose(); await appHost.DisposeAsync().ConfigureAwait(false);
} }
if (_restartOnShutdown) if (_restartOnShutdown)

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Globalization;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
@ -103,6 +104,22 @@ namespace Jellyfin.Server
}) })
.ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); .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() services.AddHealthChecks()
.AddDbContextCheck<JellyfinDb>(); .AddDbContextCheck<JellyfinDb>();

View File

@ -1,22 +1,33 @@
#nullable disable
#pragma warning disable CS1591
using System; using System;
namespace MediaBrowser.Common.Configuration namespace MediaBrowser.Common.Configuration
{ {
/// <summary>
/// <see cref="EventArgs" /> for the ConfigurationUpdated event.
/// </summary>
public class ConfigurationUpdateEventArgs : EventArgs public class ConfigurationUpdateEventArgs : EventArgs
{ {
/// <summary> /// <summary>
/// Gets or sets the key. /// Initializes a new instance of the <see cref="ConfigurationUpdateEventArgs"/> class.
/// </summary> /// </summary>
/// <value>The key.</value> /// <param name="key">The configuration key.</param>
public string Key { get; set; } /// <param name="newConfiguration">The new configuration.</param>
public ConfigurationUpdateEventArgs(string key, object newConfiguration)
{
Key = key;
NewConfiguration = newConfiguration;
}
/// <summary> /// <summary>
/// Gets or sets the new configuration. /// Gets the key.
/// </summary>
/// <value>The key.</value>
public string Key { get; }
/// <summary>
/// Gets the new configuration.
/// </summary> /// </summary>
/// <value>The new configuration.</value> /// <value>The new configuration.</value>
public object NewConfiguration { get; set; } public object NewConfiguration { get; }
} }
} }

View File

@ -1,5 +1,3 @@
#nullable disable
namespace MediaBrowser.Common.Configuration namespace MediaBrowser.Common.Configuration
{ {
/// <summary> /// <summary>

View File

@ -14,5 +14,10 @@
/// Gets the value for the MusicBrainz named http client. /// Gets the value for the MusicBrainz named http client.
/// </summary> /// </summary>
public const string MusicBrainz = nameof(MusicBrainz); public const string MusicBrainz = nameof(MusicBrainz);
/// <summary>
/// Gets the value for the DLNA named http client.
/// </summary>
public const string Dlna = nameof(Dlna);
} }
} }

View File

@ -169,8 +169,8 @@ namespace MediaBrowser.Controller.Entities.Audio
var childUpdateType = ItemUpdateType.None; var childUpdateType = ItemUpdateType.None;
// Refresh songs // Refresh songs only and not m3u files in album folder
foreach (var item in items) foreach (var item in items.OfType<Audio>())
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();

View File

@ -2616,7 +2616,8 @@ namespace MediaBrowser.Controller.Entities
return ExtraIds return ExtraIds
.Select(LibraryManager.GetItemById) .Select(LibraryManager.GetItemById)
.Where(i => i != null) .Where(i => i != null)
.Where(i => i.ExtraType.HasValue && extraTypes.Contains(i.ExtraType.Value)); .Where(i => i.ExtraType.HasValue && extraTypes.Contains(i.ExtraType.Value))
.OrderBy(i => i.SortName);
} }
public virtual long GetRunTimeTicksForPlayState() public virtual long GetRunTimeTicksForPlayState()

View File

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@ -13,7 +11,7 @@ namespace MediaBrowser.Controller.Entities
public abstract class BasePluginFolder : Folder, ICollectionFolder public abstract class BasePluginFolder : Folder, ICollectionFolder
{ {
[JsonIgnore] [JsonIgnore]
public virtual string CollectionType => null; public virtual string? CollectionType => null;
[JsonIgnore] [JsonIgnore]
public override bool SupportsInheritedParentImages => false; public override bool SupportsInheritedParentImages => false;

View File

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using System.Linq; using System.Linq;
using Jellyfin.Extensions; using Jellyfin.Extensions;
@ -19,9 +17,11 @@ namespace MediaBrowser.Controller.Entities
/// <param name="url">Trailer URL.</param> /// <param name="url">Trailer URL.</param>
public static void AddTrailerUrl(this BaseItem item, string url) public static void AddTrailerUrl(this BaseItem item, string url)
{ {
if (string.IsNullOrEmpty(url)) ArgumentNullException.ThrowIfNull(url, nameof(url));
if (url.Length == 0)
{ {
throw new ArgumentNullException(nameof(url)); throw new ArgumentException("String can't be empty", nameof(url));
} }
var current = item.RemoteTrailers.FirstOrDefault(i => string.Equals(i.Url, url, StringComparison.OrdinalIgnoreCase)); var current = item.RemoteTrailers.FirstOrDefault(i => string.Equals(i.Url, url, StringComparison.OrdinalIgnoreCase));

View File

@ -860,7 +860,7 @@ namespace MediaBrowser.Controller.Entities
return true; return true;
} }
if (!string.IsNullOrEmpty(query.AdjacentTo)) if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default))
{ {
Logger.LogDebug("Query requires post-filtering due to AdjacentTo"); Logger.LogDebug("Query requires post-filtering due to AdjacentTo");
return true; return true;
@ -892,29 +892,7 @@ namespace MediaBrowser.Controller.Entities
private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items) private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items)
{ {
var ids = query.ItemIds; return items.OrderBy(i => Array.IndexOf(query.ItemIds, i.Id)).ToArray();
int size = items.Count;
// ids can potentially contain non-unique guids, but query result cannot,
// so we include only first occurrence of each guid
var positions = new Dictionary<Guid, int>(size);
int index = 0;
for (int i = 0; i < ids.Length; i++)
{
if (positions.TryAdd(ids[i], index))
{
index++;
}
}
var newItems = new BaseItem[size];
for (int i = 0; i < size; i++)
{
var item = items[i];
newItems[positions[item.Id]] = item;
}
return newItems;
} }
public QueryResult<BaseItem> GetItems(InternalItemsQuery query) public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
@ -1029,9 +1007,9 @@ namespace MediaBrowser.Controller.Entities
#pragma warning restore CA1309 #pragma warning restore CA1309
// This must be the last filter // This must be the last filter
if (!string.IsNullOrEmpty(query.AdjacentTo)) if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default))
{ {
items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo); items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
} }
return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting); return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting);

View File

@ -129,7 +129,7 @@ namespace MediaBrowser.Controller.Entities
public Guid[] ExcludeItemIds { get; set; } public Guid[] ExcludeItemIds { get; set; }
public string? AdjacentTo { get; set; } public Guid? AdjacentTo { get; set; }
public string[] PersonTypes { get; set; } public string[] PersonTypes { get; set; }

View File

@ -244,7 +244,7 @@ namespace MediaBrowser.Controller.Entities.TV
/// <summary> /// <summary>
/// This is called before any metadata refresh and returns true or false indicating if changes were made. /// This is called before any metadata refresh and returns true or false indicating if changes were made.
/// </summary> /// </summary>
/// <param name="replaceAllMetadata"><c>true</c> to replace metdata, <c>false</c> to not.</param> /// <param name="replaceAllMetadata"><c>true</c> to replace metadata, <c>false</c> to not.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
public override bool BeforeMetadataRefresh(bool replaceAllMetadata) public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
{ {

View File

@ -266,7 +266,7 @@ namespace MediaBrowser.Controller.Entities.TV
DtoOptions = options DtoOptions = options
}; };
if (!user.DisplayMissingEpisodes) if (user == null || !user.DisplayMissingEpisodes)
{ {
query.IsMissing = false; query.IsMissing = false;
} }

Some files were not shown because too many files have changed in this diff Show More