Merge pull request #1 from JinYi-Tsinghua/patch-1

Patch 1
This commit is contained in:
JinYi-Tsinghua 2022-08-29 02:32:16 +00:00 committed by GitHub
commit f1bfbff953
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
132 changed files with 982 additions and 655 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

@ -157,6 +157,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 +226,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

@ -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

@ -64,7 +64,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

@ -36,7 +36,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId> <PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.8.0</VersionPrefix> <VersionPrefix>10.9.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

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

@ -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>();
@ -1232,5 +1234,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

@ -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.8" />
<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

@ -19,7 +19,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 +36,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 +246,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

@ -2453,6 +2453,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 +2529,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;

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

@ -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

@ -121,7 +121,7 @@
"Default": "Standard", "Default": "Standard",
"TaskOptimizeDatabaseDescription": "Komprimiert die Datenbank und trimmt den freien Speicherplatz. Die Ausführung dieser Aufgabe nach dem Scannen der Bibliothek oder nach anderen Änderungen, die Datenbankänderungen implizieren, kann die Leistung verbessern.", "TaskOptimizeDatabaseDescription": "Komprimiert die Datenbank und trimmt den freien Speicherplatz. Die Ausführung dieser Aufgabe nach dem Scannen der Bibliothek oder nach anderen Änderungen, die Datenbankänderungen implizieren, kann die Leistung verbessern.",
"TaskOptimizeDatabase": "Datenbank optimieren", "TaskOptimizeDatabase": "Datenbank optimieren",
"TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Diese Aufgabe kann sehr lange dauern.", "TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.",
"TaskKeyframeExtractor": "Keyframe Extraktor", "TaskKeyframeExtractor": "Keyframe Extraktor",
"External": "Extern" "External": "Extern"
} }

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",
@ -92,34 +92,34 @@
"ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque", "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
"ValueSpecialEpisodeName": "Spécial - {0}", "ValueSpecialEpisodeName": "Spécial - {0}",
"VersionNumber": "Version {0}", "VersionNumber": "Version {0}",
"TasksChannelsCategory": "Chaines 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 chaines en ligne.", "TaskRefreshChannelsDescription": "Actualise les informations des chaînes en ligne.",
"TaskRefreshChannels": "Rafraîchir les chaines", "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/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

@ -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

@ -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

@ -31,7 +31,7 @@
"ItemRemovedWithName": "{0} - изъято из медиатеки", "ItemRemovedWithName": "{0} - изъято из медиатеки",
"LabelIpAddressValue": "IP-адрес: {0}", "LabelIpAddressValue": "IP-адрес: {0}",
"LabelRunningTimeValue": "Длительность: {0}", "LabelRunningTimeValue": "Длительность: {0}",
"Latest": "Крайнее", "Latest": "Новое",
"MessageApplicationUpdated": "Jellyfin Server был обновлён", "MessageApplicationUpdated": "Jellyfin Server был обновлён",
"MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}", "MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена", "MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",

View File

@ -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

@ -120,5 +120,7 @@
"Default": "Подразумевано", "Default": "Подразумевано",
"TaskOptimizeDatabase": "Оптимизуј датабазу", "TaskOptimizeDatabase": "Оптимизуј датабазу",
"TaskOptimizeDatabaseDescription": "Сажима базу података и скраћује слободан простор. Покретање овог задатка након скенирања библиотеке или других промена које подразумевају измене базе података које могу побољшати перформансе.", "TaskOptimizeDatabaseDescription": "Сажима базу података и скраћује слободан простор. Покретање овог задатка након скенирања библиотеке или других промена које подразумевају измене базе података које могу побољшати перформансе.",
"External": "Спољно" "External": "Спољно",
"TaskKeyframeExtractorDescription": "Екстрактује кљулне сличице из видео датотека да би креирао више преицзну HLS плеј-листу. Овај задатак може да потраје дуже време.",
"TaskKeyframeExtractor": "Екстрактор кључних сличица"
} }

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

@ -37,7 +37,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

@ -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;
@ -153,7 +153,7 @@ namespace Emby.Server.Implementations.TV
// 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

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

@ -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

@ -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

@ -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

@ -17,7 +17,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.8" />
<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.3.1" />

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

@ -18,7 +18,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Data</PackageId> <PackageId>Jellyfin.Data</PackageId>
<VersionPrefix>10.8.0</VersionPrefix> <VersionPrefix>10.9.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

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.1-preview.79" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1-preview.71" /> <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1-preview.79" />
<PackageReference Include="SkiaSharp.Svg" Version="1.60.0" /> <PackageReference Include="SkiaSharp.Svg" Version="1.60.0" />
</ItemGroup> </ItemGroup>

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

@ -193,7 +193,7 @@ namespace Jellyfin.Networking.Configuration
public bool AutoDiscovery { get; set; } = true; public bool AutoDiscovery { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets the filter for remote IP connectivity. Used in conjuntion with <seealso cref="IsRemoteIPFilterBlacklist"/>. /// Gets or sets the filter for remote IP connectivity. Used in conjunction with <seealso cref="IsRemoteIPFilterBlacklist"/>.
/// </summary> /// </summary>
public string[] RemoteIPFilter { get; set; } = Array.Empty<string>(); public string[] RemoteIPFilter { get; set; } = Array.Empty<string>();

View File

@ -944,7 +944,7 @@ namespace Jellyfin.Networking.Manager
// Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded. // Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded.
if (config.IgnoreVirtualInterfaces) if (config.IgnoreVirtualInterfaces)
{ {
// each virtual interface name must be pre-pended with the exclusion symbol ! // each virtual interface name must be prepended with the exclusion symbol !
var virtualInterfaceNames = config.VirtualInterfaceNames.Split(',').Select(p => "!" + p).ToArray(); var virtualInterfaceNames = config.VirtualInterfaceNames.Split(',').Select(p => "!" + p).ToArray();
if (lanAddresses.Length > 0) if (lanAddresses.Length > 0)
{ {

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.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.6"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
<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.8">
<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

@ -37,8 +37,8 @@
<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.8" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.6" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.8" />
<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" />

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

@ -32,7 +32,7 @@ namespace MediaBrowser.Common.Configuration
var transcodingTempPath = configurationManager.GetEncodingOptions().TranscodingTempPath; var transcodingTempPath = configurationManager.GetEncodingOptions().TranscodingTempPath;
if (string.IsNullOrEmpty(transcodingTempPath)) if (string.IsNullOrEmpty(transcodingTempPath))
{ {
transcodingTempPath = Path.Combine(configurationManager.CommonApplicationPaths.ProgramDataPath, "transcodes"); transcodingTempPath = Path.Combine(configurationManager.CommonApplicationPaths.CachePath, "transcodes");
} }
// Make sure the directory exists // Make sure the directory exists

View File

@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Common</PackageId> <PackageId>Jellyfin.Common</PackageId>
<VersionPrefix>10.8.0</VersionPrefix> <VersionPrefix>10.9.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

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

@ -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;
@ -1029,9 +1029,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;
} }

View File

@ -433,9 +433,9 @@ namespace MediaBrowser.Controller.Entities
var user = query.User; var user = query.User;
// 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 = FilterForAdjacency(items.ToList(), query.AdjacentTo); items = FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
} }
return SortAndPage(items, totalRecordLimit, query, libraryManager, true); return SortAndPage(items, totalRecordLimit, query, libraryManager, true);
@ -985,10 +985,9 @@ namespace MediaBrowser.Controller.Entities
return _userViewManager.GetUserSubView(parent.Id, type, localizationKey, sortName); return _userViewManager.GetUserSubView(parent.Id, type, localizationKey, sortName);
} }
public static IEnumerable<BaseItem> FilterForAdjacency(List<BaseItem> list, string adjacentToId) public static IEnumerable<BaseItem> FilterForAdjacency(List<BaseItem> list, Guid adjacentTo)
{ {
var adjacentToIdGuid = new Guid(adjacentToId); var adjacentToItem = list.FirstOrDefault(i => i.Id.Equals(adjacentTo));
var adjacentToItem = list.FirstOrDefault(i => i.Id.Equals(adjacentToIdGuid));
var index = list.IndexOf(adjacentToItem); var index = list.IndexOf(adjacentToItem);
@ -1005,7 +1004,7 @@ namespace MediaBrowser.Controller.Entities
nextId = list[index + 1].Id; nextId = list[index + 1].Id;
} }
return list.Where(i => i.Id.Equals(previousId) || i.Id.Equals(nextId) || i.Id.Equals(adjacentToIdGuid)); return list.Where(i => i.Id.Equals(previousId) || i.Id.Equals(nextId) || i.Id.Equals(adjacentTo));
} }
} }
} }

View File

@ -570,5 +570,13 @@ namespace MediaBrowser.Controller.Library
Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason); Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason);
BaseItem GetParentItem(Guid? parentId, Guid? userId); BaseItem GetParentItem(Guid? parentId, Guid? userId);
/// <summary>
/// Queue a library scan.
/// </summary>
/// <remarks>
/// This exists so plugins can trigger a library scan.
/// </remarks>
void QueueLibraryScan();
} }
} }

View File

@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId> <PackageId>Jellyfin.Controller</PackageId>
<VersionPrefix>10.8.0</VersionPrefix> <VersionPrefix>10.9.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View File

@ -35,6 +35,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly ISubtitleEncoder _subtitleEncoder; private readonly ISubtitleEncoder _subtitleEncoder;
private readonly IConfiguration _config; private readonly IConfiguration _config;
private readonly Version _minKernelVersioni915Hang = new Version(5, 18);
private static readonly string[] _videoProfilesH264 = new[] private static readonly string[] _videoProfilesH264 = new[]
{ {
@ -193,7 +194,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary> /// <summary>
/// Gets the name of the output video codec. /// Gets the name of the output video codec.
/// </summary> /// </summary>
/// <param name="state">Encording state.</param> /// <param name="state">Encoding state.</param>
/// <param name="encodingOptions">Encoding options.</param> /// <param name="encodingOptions">Encoding options.</param>
/// <returns>Encoder string.</returns> /// <returns>Encoder string.</returns>
public string GetVideoEncoder(EncodingJobInfo state, EncodingOptions encodingOptions) public string GetVideoEncoder(EncodingJobInfo state, EncodingOptions encodingOptions)
@ -930,6 +931,13 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append(" -i \"").Append(state.AudioStream.Path).Append('"'); arg.Append(" -i \"").Append(state.AudioStream.Path).Append('"');
} }
// Disable auto inserted SW scaler for HW decoders in case of changed resolution.
var isSwDecoder = string.IsNullOrEmpty(GetHardwareVideoDecoder(state, options));
if (!isSwDecoder)
{
arg.Append(" -autoscale 0");
}
return arg.ToString(); return arg.ToString();
} }
@ -1144,16 +1152,15 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.SubtitleStream.IsExternal) if (state.SubtitleStream.IsExternal)
{ {
var subtitlePath = state.SubtitleStream.Path;
var charsetParam = string.Empty; var charsetParam = string.Empty;
if (!string.IsNullOrEmpty(state.SubtitleStream.Language)) if (!string.IsNullOrEmpty(state.SubtitleStream.Language))
{ {
var charenc = _subtitleEncoder.GetSubtitleFileCharacterSet( var charenc = _subtitleEncoder.GetSubtitleFileCharacterSet(
subtitlePath, state.SubtitleStream,
state.SubtitleStream.Language, state.SubtitleStream.Language,
state.MediaSource.Protocol, state.MediaSource,
CancellationToken.None).GetAwaiter().GetResult(); CancellationToken.None).GetAwaiter().GetResult();
if (!string.IsNullOrEmpty(charenc)) if (!string.IsNullOrEmpty(charenc))
{ {
@ -1165,7 +1172,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format( return string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"subtitles=f='{0}'{1}{2}{3}{4}{5}", "subtitles=f='{0}'{1}{2}{3}{4}{5}",
_mediaEncoder.EscapeSubtitleFilterPath(subtitlePath), _mediaEncoder.EscapeSubtitleFilterPath(state.SubtitleStream.Path),
charsetParam, charsetParam,
alphaParam, alphaParam,
sub2videoParam, sub2videoParam,
@ -1302,6 +1309,10 @@ namespace MediaBrowser.Controller.MediaEncoding
// which will reduce overhead in performance intensive tasks such as 4k transcoding and tonemapping. // which will reduce overhead in performance intensive tasks such as 4k transcoding and tonemapping.
var intelLowPowerHwEncoding = false; var intelLowPowerHwEncoding = false;
// Workaround for linux 5.18+ i915 hang at cost of performance.
// https://github.com/intel/media-driver/issues/1456
var enableWaFori915Hang = false;
if (string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) if (string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
{ {
var isIntelVaapiDriver = _mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965; var isIntelVaapiDriver = _mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965;
@ -1317,6 +1328,20 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
else if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
{ {
if (OperatingSystem.IsLinux() && Environment.OSVersion.Version >= _minKernelVersioni915Hang)
{
var vidDecoder = GetHardwareVideoDecoder(state, encodingOptions) ?? string.Empty;
var isIntelDecoder = vidDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase)
|| vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
var doOclTonemap = _mediaEncoder.SupportsHwaccel("qsv")
&& IsVaapiSupported(state)
&& IsOpenclFullSupported()
&& !IsVaapiVppTonemapAvailable(state, encodingOptions)
&& IsHwTonemapAvailable(state, encodingOptions);
enableWaFori915Hang = isIntelDecoder && doOclTonemap;
}
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
{ {
intelLowPowerHwEncoding = encodingOptions.EnableIntelLowPowerH264HwEncoder; intelLowPowerHwEncoding = encodingOptions.EnableIntelLowPowerH264HwEncoder;
@ -1325,6 +1350,10 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
intelLowPowerHwEncoding = encodingOptions.EnableIntelLowPowerHevcHwEncoder; intelLowPowerHwEncoding = encodingOptions.EnableIntelLowPowerHevcHwEncoder;
} }
else
{
enableWaFori915Hang = false;
}
} }
if (intelLowPowerHwEncoding) if (intelLowPowerHwEncoding)
@ -1332,6 +1361,11 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -low_power 1"; param += " -low_power 1";
} }
if (enableWaFori915Hang)
{
param += " -async_depth 1";
}
var isVc1 = string.Equals(state.VideoStream?.Codec, "vc1", StringComparison.OrdinalIgnoreCase); var isVc1 = string.Equals(state.VideoStream?.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase); var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase);
@ -1713,6 +1747,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Can't stream copy if we're burning in subtitles // Can't stream copy if we're burning in subtitles
if (request.SubtitleStreamIndex.HasValue if (request.SubtitleStreamIndex.HasValue
&& request.SubtitleStreamIndex.Value >= 0
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
{ {
return false; return false;
@ -1760,7 +1795,7 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
var requestedRangeTypes = state.GetRequestedRangeTypes(videoStream.Codec); var requestedRangeTypes = state.GetRequestedRangeTypes(videoStream.Codec);
if (requestedProfiles.Length > 0) if (requestedRangeTypes.Length > 0)
{ {
if (string.IsNullOrEmpty(videoStream.VideoRangeType)) if (string.IsNullOrEmpty(videoStream.VideoRangeType))
{ {
@ -1945,7 +1980,7 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
} }
// Cap the max target bitrate to intMax/2 to satisify the bufsize=bitrate*2. // Cap the max target bitrate to intMax/2 to satisfy the bufsize=bitrate*2.
return Math.Min(bitrate ?? 0, int.MaxValue / 2); return Math.Min(bitrate ?? 0, int.MaxValue / 2);
} }
@ -2026,6 +2061,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase) if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
{ {
@ -4503,7 +4540,9 @@ namespace MediaBrowser.Controller.MediaEncoding
if (isD3d11Supported && isCodecAvailable) if (isD3d11Supported && isCodecAvailable)
{ {
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + (isAv1 ? " -c:v av1" : string.Empty); // set -threads 3 to intel d3d11va decoder explicitly. Lower threads may result in dead lock.
// on newer devices such as Xe, the larger the init_pool_size, the longer the initialization time for opencl to derive from d3d11.
return " -hwaccel d3d11va" + (outputHwSurface ? " -hwaccel_output_format d3d11" : string.Empty) + " -threads 3" + (isAv1 ? " -c:v av1" : string.Empty);
} }
} }
else else
@ -4967,7 +5006,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.InputProtocol == MediaProtocol.Rtsp) if (state.InputProtocol == MediaProtocol.Rtsp)
{ {
inputModifier += " -rtsp_transport tcp -rtsp_transport udp -rtsp_flags prefer_tcp"; inputModifier += " -rtsp_transport tcp+udp -rtsp_flags prefer_tcp";
} }
if (!string.IsNullOrEmpty(state.InputAudioSync)) if (!string.IsNullOrEmpty(state.InputAudioSync))
@ -5496,7 +5535,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return index; return index;
} }
if (string.Equals(currentMediaStream.Path, streamToFind.Path, StringComparison.Ordinal)) if (string.Equals(currentMediaStream.Path, streamToFind.Path, StringComparison.Ordinal))
{ {
index++; index++;
} }

View File

@ -6,7 +6,8 @@ using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.MediaEncoding namespace MediaBrowser.Controller.MediaEncoding
{ {
@ -37,11 +38,11 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary> /// <summary>
/// Gets the subtitle language encoding parameter. /// Gets the subtitle language encoding parameter.
/// </summary> /// </summary>
/// <param name="path">The path.</param> /// <param name="subtitleStream">The subtitle stream.</param>
/// <param name="language">The language.</param> /// <param name="language">The language.</param>
/// <param name="protocol">The protocol.</param> /// <param name="mediaSource">The media source.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>System.String.</returns> /// <returns>System.String.</returns>
Task<string> GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken); Task<string> GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceInfo mediaSource, CancellationToken cancellationToken);
} }
} }

View File

@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller.Net namespace MediaBrowser.Controller.Net
{ {
public interface IWebSocketConnection public interface IWebSocketConnection : IAsyncDisposable, IDisposable
{ {
/// <summary> /// <summary>
/// Occurs when [closed]. /// Occurs when [closed].

View File

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
@ -17,7 +18,7 @@ namespace MediaBrowser.Controller.Session
/// <summary> /// <summary>
/// Class SessionInfo. /// Class SessionInfo.
/// </summary> /// </summary>
public sealed class SessionInfo : IDisposable public sealed class SessionInfo : IAsyncDisposable, IDisposable
{ {
// 1 second // 1 second
private const long ProgressIncrement = 10000000; private const long ProgressIncrement = 10000000;
@ -380,10 +381,28 @@ namespace MediaBrowser.Controller.Session
{ {
if (controller is IDisposable disposable) if (controller is IDisposable disposable)
{ {
_logger.LogDebug("Disposing session controller {0}", disposable.GetType().Name); _logger.LogDebug("Disposing session controller synchronously {TypeName}", disposable.GetType().Name);
disposable.Dispose(); disposable.Dispose();
} }
} }
} }
public async ValueTask DisposeAsync()
{
_disposed = true;
StopAutomaticProgress();
var controllers = SessionControllers.ToList();
foreach (var controller in controllers)
{
if (controller is IAsyncDisposable disposableAsync)
{
_logger.LogDebug("Disposing session controller asynchronously {TypeName}", disposableAsync.GetType().Name);
await disposableAsync.DisposeAsync().ConfigureAwait(false);
}
}
}
} }
} }

View File

@ -549,7 +549,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
if (InitialState.Equals(GroupStateType.Playing)) if (InitialState.Equals(GroupStateType.Playing))
{ {
// Group went from playing to waiting state and a pause request occured while waiting. // Group went from playing to waiting state and a pause request occurred while waiting.
var pauseRequest = new PauseGroupRequest(); var pauseRequest = new PauseGroupRequest();
pausedState.HandleRequest(pauseRequest, context, Type, session, cancellationToken); pausedState.HandleRequest(pauseRequest, context, Type, session, cancellationToken);
} }

View File

@ -27,9 +27,9 @@ namespace MediaBrowser.Controller.SyncPlay.PlaybackRequests
} }
/// <summary> /// <summary>
/// Gets the playlist identifiers ot the items. /// Gets the playlist identifiers of the items.
/// </summary> /// </summary>
/// <value>The playlist identifiers ot the items.</value> /// <value>The playlist identifiers of the items.</value>
public IReadOnlyList<Guid> PlaylistItemIds { get; } public IReadOnlyList<Guid> PlaylistItemIds { get; }
/// <summary> /// <summary>

View File

@ -102,7 +102,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
} }
/// <summary> /// <summary>
/// Appends new items to the playlist. The specified order is mantained. /// Appends new items to the playlist. The specified order is maintained.
/// </summary> /// </summary>
/// <param name="items">The items to add to the playlist.</param> /// <param name="items">The items to add to the playlist.</param>
public void Queue(IReadOnlyList<Guid> items) public void Queue(IReadOnlyList<Guid> items)
@ -197,7 +197,7 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
} }
/// <summary> /// <summary>
/// Adds new items to the playlist right after the playing item. The specified order is mantained. /// Adds new items to the playlist right after the playing item. The specified order is maintained.
/// </summary> /// </summary>
/// <param name="items">The items to add to the playlist.</param> /// <param name="items">The items to add to the playlist.</param>
public void QueueNext(IReadOnlyList<Guid> items) public void QueueNext(IReadOnlyList<Guid> items)

View File

@ -619,9 +619,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
Video3DFormat.HalfSideBySide => "crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", Video3DFormat.HalfSideBySide => "crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
// fsbs crop width in half,set the display aspect,crop out any black bars we may have made // fsbs crop width in half,set the display aspect,crop out any black bars we may have made
Video3DFormat.FullSideBySide => "crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", Video3DFormat.FullSideBySide => "crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
// htab crop heigh in half,scale to correct size, set the display aspect,crop out any black bars we may have made // htab crop height in half,scale to correct size, set the display aspect,crop out any black bars we may have made
Video3DFormat.HalfTopAndBottom => "crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", Video3DFormat.HalfTopAndBottom => "crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
// ftab crop heigt in half, set the display aspect,crop out any black bars we may have made // ftab crop height in half, set the display aspect,crop out any black bars we may have made
Video3DFormat.FullTopAndBottom => "crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1", Video3DFormat.FullTopAndBottom => "crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1",
_ => "scale=trunc(iw*sar):ih" _ => "scale=trunc(iw*sar):ih"
}; };

View File

@ -30,7 +30,7 @@
<PackageReference Include="libse" Version="3.6.5" /> <PackageReference Include="libse" Version="3.6.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
<PackageReference Include="UTF.Unknown" Version="2.5.0" /> <PackageReference Include="UTF.Unknown" Version="2.5.1" />
</ItemGroup> </ItemGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->

View File

@ -1,19 +0,0 @@
using Microsoft.Extensions.Logging;
using Nikse.SubtitleEdit.Core.SubtitleFormats;
namespace MediaBrowser.MediaEncoding.Subtitles
{
/// <summary>
/// Advanced SubStation Alpha subtitle parser.
/// </summary>
public class AssParser : SubtitleEditParser<AdvancedSubStationAlpha>
{
/// <summary>
/// Initializes a new instance of the <see cref="AssParser"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
public AssParser(ILogger logger) : base(logger)
{
}
}
}

View File

@ -1,7 +1,6 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System.IO; using System.IO;
using System.Threading;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.MediaEncoding.Subtitles namespace MediaBrowser.MediaEncoding.Subtitles
@ -12,8 +11,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// Parses the specified stream. /// Parses the specified stream.
/// </summary> /// </summary>
/// <param name="stream">The stream.</param> /// <param name="stream">The stream.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="fileExtension">The file extension.</param>
/// <returns>SubtitleTrackInfo.</returns> /// <returns>SubtitleTrackInfo.</returns>
SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken); SubtitleTrackInfo Parse(Stream stream, string fileExtension);
/// <summary>
/// Determines whether the file extension is supported by the parser.
/// </summary>
/// <param name="fileExtension">The file extension.</param>
/// <returns>A value indicating whether the file extension is supported.</returns>
bool SupportsFileExtension(string fileExtension);
} }
} }

View File

@ -1,19 +0,0 @@
using Microsoft.Extensions.Logging;
using Nikse.SubtitleEdit.Core.SubtitleFormats;
namespace MediaBrowser.MediaEncoding.Subtitles
{
/// <summary>
/// SubRip subtitle parser.
/// </summary>
public class SrtParser : SubtitleEditParser<SubRip>
{
/// <summary>
/// Initializes a new instance of the <see cref="SrtParser"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
public SrtParser(ILogger logger) : base(logger)
{
}
}
}

View File

@ -1,19 +0,0 @@
using Microsoft.Extensions.Logging;
using Nikse.SubtitleEdit.Core.SubtitleFormats;
namespace MediaBrowser.MediaEncoding.Subtitles
{
/// <summary>
/// SubStation Alpha subtitle parser.
/// </summary>
public class SsaParser : SubtitleEditParser<SubStationAlpha>
{
/// <summary>
/// Initializes a new instance of the <see cref="SsaParser"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
public SsaParser(ILogger logger) : base(logger)
{
}
}
}

View File

@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Reflection;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Nikse.SubtitleEdit.Core.Common; using Nikse.SubtitleEdit.Core.Common;
using ILogger = Microsoft.Extensions.Logging.ILogger; using Nikse.SubtitleEdit.Core.SubtitleFormats;
using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat; using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat;
namespace MediaBrowser.MediaEncoding.Subtitles namespace MediaBrowser.MediaEncoding.Subtitles
@ -14,31 +16,57 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <summary> /// <summary>
/// SubStation Alpha subtitle parser. /// SubStation Alpha subtitle parser.
/// </summary> /// </summary>
/// <typeparam name="T">The <see cref="SubtitleFormat" />.</typeparam> public class SubtitleEditParser : ISubtitleParser
public abstract class SubtitleEditParser<T> : ISubtitleParser
where T : SubtitleFormat, new()
{ {
private readonly ILogger _logger; private readonly ILogger<SubtitleEditParser> _logger;
private readonly Dictionary<string, SubtitleFormat[]> _subtitleFormats;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SubtitleEditParser{T}"/> class. /// Initializes a new instance of the <see cref="SubtitleEditParser"/> class.
/// </summary> /// </summary>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
protected SubtitleEditParser(ILogger logger) public SubtitleEditParser(ILogger<SubtitleEditParser> logger)
{ {
_logger = logger; _logger = logger;
_subtitleFormats = GetSubtitleFormats()
.Where(subtitleFormat => !string.IsNullOrEmpty(subtitleFormat.Extension))
.GroupBy(subtitleFormat => subtitleFormat.Extension.TrimStart('.'), StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase);
} }
/// <inheritdoc /> /// <inheritdoc />
public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) public SubtitleTrackInfo Parse(Stream stream, string fileExtension)
{ {
var subtitle = new Subtitle(); var subtitle = new Subtitle();
var subRip = new T();
var lines = stream.ReadAllLines().ToList(); var lines = stream.ReadAllLines().ToList();
subRip.LoadSubtitle(subtitle, lines, "untitled");
if (subRip.ErrorCount > 0) if (!_subtitleFormats.TryGetValue(fileExtension, out var subtitleFormats))
{ {
_logger.LogError("{ErrorCount} errors encountered while parsing subtitle", subRip.ErrorCount); throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension));
}
foreach (var subtitleFormat in subtitleFormats)
{
_logger.LogDebug(
"Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
fileExtension,
subtitleFormat.Name);
subtitleFormat.LoadSubtitle(subtitle, lines, fileExtension);
if (subtitleFormat.ErrorCount == 0)
{
break;
}
_logger.LogError(
"{ErrorCount} errors encountered while parsing '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
subtitleFormat.ErrorCount,
fileExtension,
subtitleFormat.Name);
}
if (subtitle.Paragraphs.Count == 0)
{
throw new ArgumentException("Unsupported format: " + fileExtension);
} }
var trackInfo = new SubtitleTrackInfo(); var trackInfo = new SubtitleTrackInfo();
@ -57,5 +85,36 @@ namespace MediaBrowser.MediaEncoding.Subtitles
trackInfo.TrackEvents = trackEvents; trackInfo.TrackEvents = trackEvents;
return trackInfo; return trackInfo;
} }
/// <inheritdoc />
public bool SupportsFileExtension(string fileExtension)
=> _subtitleFormats.ContainsKey(fileExtension);
private IEnumerable<SubtitleFormat> GetSubtitleFormats()
{
var subtitleFormats = new List<SubtitleFormat>();
var assembly = typeof(SubtitleFormat).Assembly;
foreach (var type in assembly.GetTypes())
{
if (!type.IsSubclassOf(typeof(SubtitleFormat)) || type.IsAbstract)
{
continue;
}
try
{
// It shouldn't be null, but the exception is caught if it is
var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(type, true)!;
subtitleFormats.Add(subtitleFormat);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to create instance of the subtitle format {SubtitleFormatType}", type.Name);
}
}
return subtitleFormats;
}
} }
} }

View File

@ -35,6 +35,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly ISubtitleParser _subtitleParser;
/// <summary> /// <summary>
/// The _semaphoreLocks. /// The _semaphoreLocks.
@ -48,7 +49,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
IFileSystem fileSystem, IFileSystem fileSystem,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IMediaSourceManager mediaSourceManager) IMediaSourceManager mediaSourceManager,
ISubtitleParser subtitleParser)
{ {
_logger = logger; _logger = logger;
_appPaths = appPaths; _appPaths = appPaths;
@ -56,6 +58,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_subtitleParser = subtitleParser;
} }
private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
@ -73,8 +76,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
try try
{ {
var reader = GetReader(inputFormat); var trackInfo = _subtitleParser.Parse(stream, inputFormat);
var trackInfo = reader.Parse(stream, cancellationToken);
FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps); FilterEvents(trackInfo, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
@ -233,54 +235,21 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec) var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec)
.TrimStart('.'); .TrimStart('.');
if (!TryGetReader(currentFormat, out _)) // Fallback to ffmpeg conversion
if (!_subtitleParser.SupportsFileExtension(currentFormat))
{ {
// Convert // Convert
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt"); var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt");
await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); await ConvertTextSubtitleToSrt(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true); return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true);
} }
// It's possbile that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs) // It's possible that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with local subs)
return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true); return new SubtitleInfo(subtitleStream.Path, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), currentFormat, true);
} }
private bool TryGetReader(string format, [NotNullWhen(true)] out ISubtitleParser? value)
{
if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase))
{
value = new SrtParser(_logger);
return true;
}
if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
{
value = new SsaParser(_logger);
return true;
}
if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
{
value = new AssParser(_logger);
return true;
}
value = null;
return false;
}
private ISubtitleParser GetReader(string format)
{
if (TryGetReader(format, out var reader))
{
return reader;
}
throw new ArgumentException("Unsupported format: " + format);
}
private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value) private bool TryGetWriter(string format, [NotNullWhen(true)] out ISubtitleWriter? value)
{ {
if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)) if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
@ -351,13 +320,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <summary> /// <summary>
/// Converts the text subtitle to SRT. /// Converts the text subtitle to SRT.
/// </summary> /// </summary>
/// <param name="inputPath">The input path.</param> /// <param name="subtitleStream">The subtitle stream.</param>
/// <param name="language">The language.</param>
/// <param name="mediaSource">The input mediaSource.</param> /// <param name="mediaSource">The input mediaSource.</param>
/// <param name="outputPath">The output path.</param> /// <param name="outputPath">The output path.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns> /// <returns>Task.</returns>
private async Task ConvertTextSubtitleToSrt(string inputPath, string language, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken) private async Task ConvertTextSubtitleToSrt(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
{ {
var semaphore = GetLock(outputPath); var semaphore = GetLock(outputPath);
@ -367,7 +335,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{ {
if (!File.Exists(outputPath)) if (!File.Exists(outputPath))
{ {
await ConvertTextSubtitleToSrtInternal(inputPath, language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
} }
} }
finally finally
@ -379,8 +347,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <summary> /// <summary>
/// Converts the text subtitle to SRT internal. /// Converts the text subtitle to SRT internal.
/// </summary> /// </summary>
/// <param name="inputPath">The input path.</param> /// <param name="subtitleStream">The subtitle stream.</param>
/// <param name="language">The language.</param>
/// <param name="mediaSource">The input mediaSource.</param> /// <param name="mediaSource">The input mediaSource.</param>
/// <param name="outputPath">The output path.</param> /// <param name="outputPath">The output path.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
@ -388,8 +355,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <exception cref="ArgumentNullException"> /// <exception cref="ArgumentNullException">
/// The <c>inputPath</c> or <c>outputPath</c> is <c>null</c>. /// The <c>inputPath</c> or <c>outputPath</c> is <c>null</c>.
/// </exception> /// </exception>
private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string language, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken) private async Task ConvertTextSubtitleToSrtInternal(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
{ {
var inputPath = subtitleStream.Path;
if (string.IsNullOrEmpty(inputPath)) if (string.IsNullOrEmpty(inputPath))
{ {
throw new ArgumentNullException(nameof(inputPath)); throw new ArgumentNullException(nameof(inputPath));
@ -402,7 +370,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath))); Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)));
var encodingParam = await GetSubtitleFileCharacterSet(inputPath, language, mediaSource.Protocol, cancellationToken).ConfigureAwait(false); var encodingParam = await GetSubtitleFileCharacterSet(subtitleStream, subtitleStream.Language, mediaSource, cancellationToken).ConfigureAwait(false);
// FFmpeg automatically convert character encoding when it is UTF-16 // FFmpeg automatically convert character encoding when it is UTF-16
// If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event" // If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event"
@ -420,18 +388,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
int exitCode; int exitCode;
using (var process = new Process using (var process = new Process
{
StartInfo = new ProcessStartInfo
{ {
StartInfo = new ProcessStartInfo CreateNoWindow = true,
{ UseShellExecute = false,
CreateNoWindow = true, FileName = _mediaEncoder.EncoderPath,
UseShellExecute = false, Arguments = string.Format(CultureInfo.InvariantCulture, "{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath),
FileName = _mediaEncoder.EncoderPath, WindowStyle = ProcessWindowStyle.Hidden,
Arguments = string.Format(CultureInfo.InvariantCulture, "{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath), ErrorDialog = false
WindowStyle = ProcessWindowStyle.Hidden, },
ErrorDialog = false EnableRaisingEvents = true
}, })
EnableRaisingEvents = true
})
{ {
_logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); _logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
@ -571,7 +539,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var processArgs = string.Format( var processArgs = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"-i {0} -map 0:{1} -an -vn -c:s {2} \"{3}\"", "-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
inputPath, inputPath,
subtitleStreamIndex, subtitleStreamIndex,
outputCodec, outputCodec,
@ -580,18 +548,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
int exitCode; int exitCode;
using (var process = new Process using (var process = new Process
{
StartInfo = new ProcessStartInfo
{ {
StartInfo = new ProcessStartInfo CreateNoWindow = true,
{ UseShellExecute = false,
CreateNoWindow = true, FileName = _mediaEncoder.EncoderPath,
UseShellExecute = false, Arguments = processArgs,
FileName = _mediaEncoder.EncoderPath, WindowStyle = ProcessWindowStyle.Hidden,
Arguments = processArgs, ErrorDialog = false
WindowStyle = ProcessWindowStyle.Hidden, },
ErrorDialog = false EnableRaisingEvents = true
}, })
EnableRaisingEvents = true
})
{ {
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
@ -606,7 +574,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw; throw;
} }
var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(5)).ConfigureAwait(false); var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
if (!ranToCompletion) if (!ranToCompletion)
{ {
@ -729,9 +697,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<string> GetSubtitleFileCharacterSet(string path, string language, MediaProtocol protocol, CancellationToken cancellationToken) public async Task<string> GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceInfo mediaSource, CancellationToken cancellationToken)
{ {
using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false)) var subtitleCodec = subtitleStream.Codec;
var path = subtitleStream.Path;
if (path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
{
path = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec);
await ExtractTextSubtitle(mediaSource, subtitleStream, subtitleCodec, path, cancellationToken)
.ConfigureAwait(false);
}
using (var stream = await GetStream(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false))
{ {
var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName ?? string.Empty; var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName ?? string.Empty;
@ -754,12 +732,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
switch (protocol) switch (protocol)
{ {
case MediaProtocol.Http: case MediaProtocol.Http:
{ {
using var response = await _httpClientFactory.CreateClient(NamedClient.Default) using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(new Uri(path), cancellationToken) .GetAsync(new Uri(path), cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
} }
case MediaProtocol.File: case MediaProtocol.File:
return AsyncFile.OpenRead(path); return AsyncFile.OpenRead(path);

View File

@ -157,7 +157,7 @@ namespace MediaBrowser.Model.Dlna
flagValue |= DlnaFlags.ByteBasedSeek; flagValue |= DlnaFlags.ByteBasedSeek;
} }
// Time based seek is curently disabled when streaming. On LG CX3 adding DlnaFlags.TimeBasedSeek and orgPn causes the DLNA playback to fail (format not supported). Further investigations are needed before enabling the remaining code paths. // Time based seek is currently disabled when streaming. On LG CX3 adding DlnaFlags.TimeBasedSeek and orgPn causes the DLNA playback to fail (format not supported). Further investigations are needed before enabling the remaining code paths.
// else if (runtimeTicks.HasValue) // else if (runtimeTicks.HasValue)
// { // {
// flagValue = flagValue | DlnaFlags.TimeBasedSeek; // flagValue = flagValue | DlnaFlags.TimeBasedSeek;

View File

@ -294,13 +294,13 @@ namespace MediaBrowser.Model.Dto
public NameGuidPair[] GenreItems { get; set; } public NameGuidPair[] GenreItems { get; set; }
/// <summary> /// <summary>
/// Gets or sets wether the item has a logo, this will hold the Id of the Parent that has one. /// Gets or sets whether the item has a logo, this will hold the Id of the Parent that has one.
/// </summary> /// </summary>
/// <value>The parent logo item id.</value> /// <value>The parent logo item id.</value>
public Guid? ParentLogoItemId { get; set; } public Guid? ParentLogoItemId { get; set; }
/// <summary> /// <summary>
/// Gets or sets wether the item has any backdrops, this will hold the Id of the Parent that has one. /// Gets or sets whether the item has any backdrops, this will hold the Id of the Parent that has one.
/// </summary> /// </summary>
/// <value>The parent backdrop item id.</value> /// <value>The parent backdrop item id.</value>
public Guid? ParentBackdropItemId { get; set; } public Guid? ParentBackdropItemId { get; set; }
@ -506,7 +506,7 @@ namespace MediaBrowser.Model.Dto
public string ParentLogoImageTag { get; set; } public string ParentLogoImageTag { get; set; }
/// <summary> /// <summary>
/// Gets or sets wether the item has fan art, this will hold the Id of the Parent that has one. /// Gets or sets whether the item has fan art, this will hold the Id of the Parent that has one.
/// </summary> /// </summary>
/// <value>The parent art item id.</value> /// <value>The parent art item id.</value>
public Guid? ParentArtItemId { get; set; } public Guid? ParentArtItemId { get; set; }

View File

@ -588,15 +588,26 @@ namespace MediaBrowser.Model.Entities
return Width switch return Width switch
{ {
<= 720 when Height <= 480 => IsInterlaced ? "480i" : "480p", // 256x144 (16:9 square pixel format)
// 720x576 (PAL) (768 when rescaled for square pixels) <= 256 when Height <= 144 => IsInterlaced ? "144i" : "144p",
<= 768 when Height <= 576 => IsInterlaced ? "576i" : "576p", // 426x240 (16:9 square pixel format)
// 960x540 (sometimes 544 which is multiple of 16) <= 426 when Height <= 240 => IsInterlaced ? "240i" : "240p",
// 640x360 (16:9 square pixel format)
<= 640 when Height <= 360 => IsInterlaced ? "360i" : "360p",
// 682x384 (16:9 square pixel format)
<= 682 when Height <= 384 => IsInterlaced ? "384i" : "384p",
// 720x404 (16:9 square pixel format)
<= 720 when Height <= 404 => IsInterlaced ? "404i" : "404p",
// 854x480 (16:9 square pixel format)
<= 854 when Height <= 480 => IsInterlaced ? "480i" : "480p",
// 960x544 (16:9 square pixel format)
<= 960 when Height <= 544 => IsInterlaced ? "540i" : "540p", <= 960 when Height <= 544 => IsInterlaced ? "540i" : "540p",
// 1024x576 (16:9 square pixel format)
<= 1024 when Height <= 576 => IsInterlaced ? "576i" : "576p",
// 1280x720 // 1280x720
<= 1280 when Height <= 962 => IsInterlaced ? "720i" : "720p", <= 1280 when Height <= 962 => IsInterlaced ? "720i" : "720p",
// 1920x1080 // 2560x1080 (FHD ultra wide 21:9) using 1440px width to accommodate WQHD
<= 1920 when Height <= 1440 => IsInterlaced ? "1080i" : "1080p", <= 2560 when Height <= 1440 => IsInterlaced ? "1080i" : "1080p",
// 4K // 4K
<= 4096 when Height <= 3072 => "4K", <= 4096 when Height <= 3072 => "4K",
// 8K // 8K

View File

@ -8,6 +8,7 @@ namespace MediaBrowser.Model.LiveTv
public TunerHostInfo() public TunerHostInfo()
{ {
AllowHWTranscoding = true; AllowHWTranscoding = true;
IgnoreDts = true;
} }
public string Id { get; set; } public string Id { get; set; }
@ -31,5 +32,7 @@ namespace MediaBrowser.Model.LiveTv
public int TunerCount { get; set; } public int TunerCount { get; set; }
public string UserAgent { get; set; } public string UserAgent { get; set; }
public bool IgnoreDts { get; set; }
} }
} }

View File

@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Model</PackageId> <PackageId>Jellyfin.Model</PackageId>
<VersionPrefix>10.8.0</VersionPrefix> <VersionPrefix>10.9.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>

View File

@ -33,7 +33,7 @@ namespace MediaBrowser.Model.Querying
/// Gets or sets the series id. /// Gets or sets the series id.
/// </summary> /// </summary>
/// <value>The series id.</value> /// <value>The series id.</value>
public string SeriesId { get; set; } public Guid? SeriesId { get; set; }
/// <summary> /// <summary>
/// Gets or sets the start index. Use for paging. /// Gets or sets the start index. Use for paging.

View File

@ -1,8 +1,6 @@
#nullable disable
#pragma warning disable CS1591
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Search namespace MediaBrowser.Model.Search
{ {
@ -11,12 +9,28 @@ namespace MediaBrowser.Model.Search
/// </summary> /// </summary>
public class SearchHint public class SearchHint
{ {
/// <summary>
/// Initializes a new instance of the <see cref="SearchHint" /> class.
/// </summary>
public SearchHint()
{
Name = string.Empty;
MatchedTerm = string.Empty;
MediaType = string.Empty;
Artists = Array.Empty<string>();
}
/// <summary> /// <summary>
/// Gets or sets the item id. /// Gets or sets the item id.
/// </summary> /// </summary>
/// <value>The item id.</value> /// <value>The item id.</value>
[Obsolete("Use Id instead")]
public Guid ItemId { get; set; } public Guid ItemId { get; set; }
/// <summary>
/// Gets or sets the item id.
/// </summary>
/// <value>The item id.</value>
public Guid Id { get; set; } public Guid Id { get; set; }
/// <summary> /// <summary>
@ -53,38 +67,42 @@ namespace MediaBrowser.Model.Search
/// Gets or sets the image tag. /// Gets or sets the image tag.
/// </summary> /// </summary>
/// <value>The image tag.</value> /// <value>The image tag.</value>
public string PrimaryImageTag { get; set; } public string? PrimaryImageTag { get; set; }
/// <summary> /// <summary>
/// Gets or sets the thumb image tag. /// Gets or sets the thumb image tag.
/// </summary> /// </summary>
/// <value>The thumb image tag.</value> /// <value>The thumb image tag.</value>
public string ThumbImageTag { get; set; } public string? ThumbImageTag { get; set; }
/// <summary> /// <summary>
/// Gets or sets the thumb image item identifier. /// Gets or sets the thumb image item identifier.
/// </summary> /// </summary>
/// <value>The thumb image item identifier.</value> /// <value>The thumb image item identifier.</value>
public string ThumbImageItemId { get; set; } public string? ThumbImageItemId { get; set; }
/// <summary> /// <summary>
/// Gets or sets the backdrop image tag. /// Gets or sets the backdrop image tag.
/// </summary> /// </summary>
/// <value>The backdrop image tag.</value> /// <value>The backdrop image tag.</value>
public string BackdropImageTag { get; set; } public string? BackdropImageTag { get; set; }
/// <summary> /// <summary>
/// Gets or sets the backdrop image item identifier. /// Gets or sets the backdrop image item identifier.
/// </summary> /// </summary>
/// <value>The backdrop image item identifier.</value> /// <value>The backdrop image item identifier.</value>
public string BackdropImageItemId { get; set; } public string? BackdropImageItemId { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type. /// Gets or sets the type.
/// </summary> /// </summary>
/// <value>The type.</value> /// <value>The type.</value>
public string Type { get; set; } public BaseItemKind Type { get; set; }
/// <summary>
/// Gets a value indicating whether this instance is folder.
/// </summary>
/// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value>
public bool? IsFolder { get; set; } public bool? IsFolder { get; set; }
/// <summary> /// <summary>
@ -99,31 +117,47 @@ namespace MediaBrowser.Model.Search
/// <value>The type of the media.</value> /// <value>The type of the media.</value>
public string MediaType { get; set; } public string MediaType { get; set; }
/// <summary>
/// Gets or sets the start date.
/// </summary>
/// <value>The start date.</value>
public DateTime? StartDate { get; set; } public DateTime? StartDate { get; set; }
/// <summary>
/// Gets or sets the end date.
/// </summary>
/// <value>The end date.</value>
public DateTime? EndDate { get; set; } public DateTime? EndDate { get; set; }
/// <summary> /// <summary>
/// Gets or sets the series. /// Gets or sets the series.
/// </summary> /// </summary>
/// <value>The series.</value> /// <value>The series.</value>
public string Series { get; set; } public string? Series { get; set; }
public string Status { get; set; } /// <summary>
/// Gets or sets the status.
/// </summary>
/// <value>The status.</value>
public string? Status { get; set; }
/// <summary> /// <summary>
/// Gets or sets the album. /// Gets or sets the album.
/// </summary> /// </summary>
/// <value>The album.</value> /// <value>The album.</value>
public string Album { get; set; } public string? Album { get; set; }
public Guid AlbumId { get; set; } /// <summary>
/// Gets or sets the album id.
/// </summary>
/// <value>The album id.</value>
public Guid? AlbumId { get; set; }
/// <summary> /// <summary>
/// Gets or sets the album artist. /// Gets or sets the album artist.
/// </summary> /// </summary>
/// <value>The album artist.</value> /// <value>The album artist.</value>
public string AlbumArtist { get; set; } public string? AlbumArtist { get; set; }
/// <summary> /// <summary>
/// Gets or sets the artists. /// Gets or sets the artists.
@ -147,13 +181,13 @@ namespace MediaBrowser.Model.Search
/// Gets or sets the channel identifier. /// Gets or sets the channel identifier.
/// </summary> /// </summary>
/// <value>The channel identifier.</value> /// <value>The channel identifier.</value>
public Guid ChannelId { get; set; } public Guid? ChannelId { get; set; }
/// <summary> /// <summary>
/// Gets or sets the name of the channel. /// Gets or sets the name of the channel.
/// </summary> /// </summary>
/// <value>The name of the channel.</value> /// <value>The name of the channel.</value>
public string ChannelName { get; set; } public string? ChannelName { get; set; }
/// <summary> /// <summary>
/// Gets or sets the primary image aspect ratio. /// Gets or sets the primary image aspect ratio.

View File

@ -11,7 +11,7 @@ namespace MediaBrowser.Model.SyncPlay
Idle = 0, Idle = 0,
/// <summary> /// <summary>
/// The group is in wating state. Playback is paused. Will start playing when users are ready. /// The group is in waiting state. Playback is paused. Will start playing when users are ready.
/// </summary> /// </summary>
Waiting = 1, Waiting = 1,

View File

@ -22,7 +22,7 @@ namespace MediaBrowser.Model.Tasks
/// <summary> /// <summary>
/// Cancels if running and queue. /// Cancels if running and queue.
/// </summary> /// </summary>
/// <typeparam name="T">An implementatin of <see cref="IScheduledTask" />.</typeparam> /// <typeparam name="T">An implementation of <see cref="IScheduledTask" />.</typeparam>
/// <param name="options">Task options.</param> /// <param name="options">Task options.</param>
void CancelIfRunningAndQueue<T>(TaskOptions options) void CancelIfRunningAndQueue<T>(TaskOptions options)
where T : IScheduledTask; where T : IScheduledTask;
@ -30,21 +30,21 @@ namespace MediaBrowser.Model.Tasks
/// <summary> /// <summary>
/// Cancels if running and queue. /// Cancels if running and queue.
/// </summary> /// </summary>
/// <typeparam name="T">An implementatin of <see cref="IScheduledTask" />.</typeparam> /// <typeparam name="T">An implementation of <see cref="IScheduledTask" />.</typeparam>
void CancelIfRunningAndQueue<T>() void CancelIfRunningAndQueue<T>()
where T : IScheduledTask; where T : IScheduledTask;
/// <summary> /// <summary>
/// Cancels if running. /// Cancels if running.
/// </summary> /// </summary>
/// <typeparam name="T">An implementatin of <see cref="IScheduledTask" />.</typeparam> /// <typeparam name="T">An implementation of <see cref="IScheduledTask" />.</typeparam>
void CancelIfRunning<T>() void CancelIfRunning<T>()
where T : IScheduledTask; where T : IScheduledTask;
/// <summary> /// <summary>
/// Queues the scheduled task. /// Queues the scheduled task.
/// </summary> /// </summary>
/// <typeparam name="T">An implementatin of <see cref="IScheduledTask" />.</typeparam> /// <typeparam name="T">An implementation of <see cref="IScheduledTask" />.</typeparam>
/// <param name="options">Task options.</param> /// <param name="options">Task options.</param>
void QueueScheduledTask<T>(TaskOptions options) void QueueScheduledTask<T>(TaskOptions options)
where T : IScheduledTask; where T : IScheduledTask;
@ -52,7 +52,7 @@ namespace MediaBrowser.Model.Tasks
/// <summary> /// <summary>
/// Queues the scheduled task. /// Queues the scheduled task.
/// </summary> /// </summary>
/// <typeparam name="T">An implementatin of <see cref="IScheduledTask" />.</typeparam> /// <typeparam name="T">An implementation of <see cref="IScheduledTask" />.</typeparam>
void QueueScheduledTask<T>() void QueueScheduledTask<T>()
where T : IScheduledTask; where T : IScheduledTask;

View File

@ -21,10 +21,10 @@ namespace MediaBrowser.Model.Tasks
/// <summary> /// <summary>
/// Stars waiting for the trigger action. /// Stars waiting for the trigger action.
/// </summary> /// </summary>
/// <param name="lastResult">Result of the last run triggerd task.</param> /// <param name="lastResult">Result of the last run triggered task.</param>
/// <param name="logger">The <see cref="ILogger"/>.</param> /// <param name="logger">The <see cref="ILogger"/>.</param>
/// <param name="taskName">The name of the task.</param> /// <param name="taskName">The name of the task.</param>
/// <param name="isApplicationStartup">Wheter or not this is is fired during startup.</param> /// <param name="isApplicationStartup">Whether or not this is is fired during startup.</param>
void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup); void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup);
/// <summary> /// <summary>

View File

@ -926,7 +926,7 @@ namespace MediaBrowser.Providers.Manager
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error in {0}.Suports", i.GetType().Name); _logger.LogError(ex, "Error in {0}.Supports", i.GetType().Name);
return false; return false;
} }
}); });

View File

@ -36,7 +36,7 @@ namespace MediaBrowser.Providers.Music
/// <summary> /// <summary>
/// The Jellyfin user-agent is unrestricted but source IP must not exceed /// The Jellyfin user-agent is unrestricted but source IP must not exceed
/// one request per second, therefore we rate limit to avoid throttling. /// one request per second, therefore we rate limit to avoid throttling.
/// Be prudent, use a value slightly above the minimun required. /// Be prudent, use a value slightly above the minimum required.
/// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting. /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting.
/// </summary> /// </summary>
private readonly long _musicBrainzQueryIntervalMs; private readonly long _musicBrainzQueryIntervalMs;

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
@ -40,6 +41,7 @@ namespace MediaBrowser.Providers.TV
{ {
await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false); await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
RemoveObsoleteEpisodes(item);
RemoveObsoleteSeasons(item); RemoveObsoleteSeasons(item);
await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false); await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
} }
@ -121,6 +123,61 @@ namespace MediaBrowser.Providers.TV
} }
} }
private void RemoveObsoleteEpisodes(Series series)
{
var episodes = series.GetEpisodes(null, new DtoOptions()).OfType<Episode>().ToList();
var numberOfEpisodes = episodes.Count;
// TODO: O(n^2), but can it be done faster without overcomplicating it?
for (var i = 0; i < numberOfEpisodes; i++)
{
var currentEpisode = episodes[i];
// The outer loop only examines virtual episodes
if (!currentEpisode.IsVirtualItem)
{
continue;
}
// Virtual episodes without an episode number are practically orphaned and should be deleted
if (!currentEpisode.IndexNumber.HasValue)
{
DeleteEpisode(currentEpisode);
continue;
}
for (var j = i + 1; j < numberOfEpisodes; j++)
{
var comparisonEpisode = episodes[j];
// The inner loop is only for "physical" episodes
if (comparisonEpisode.IsVirtualItem
|| currentEpisode.ParentIndexNumber != comparisonEpisode.ParentIndexNumber
|| !comparisonEpisode.ContainsEpisodeNumber(currentEpisode.IndexNumber.Value))
{
continue;
}
DeleteEpisode(currentEpisode);
break;
}
}
}
private void DeleteEpisode(Episode episode)
{
Logger.LogInformation(
"Removing virtual episode S{SeasonNumber}E{EpisodeNumber} in series {SeriesName}",
episode.ParentIndexNumber,
episode.IndexNumber,
episode.SeriesName);
LibraryManager.DeleteItem(
episode,
new DeleteOptions
{
DeleteFileLocation = true
},
false);
}
/// <summary> /// <summary>
/// Creates seasons for all episodes that aren't in a season folder. /// Creates seasons for all episodes that aren't in a season folder.
/// If no season number can be determined, a dummy season will be created. /// If no season number can be determined, a dummy season will be created.

View File

@ -1330,7 +1330,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}; };
/// <summary> /// <summary>
/// Used to split names of comma or pipe delimeted genres and people. /// Used to split names of comma or pipe delimited genres and people.
/// </summary> /// </summary>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
/// <returns>IEnumerable{System.String}.</returns> /// <returns>IEnumerable{System.String}.</returns>

View File

@ -1,4 +1,4 @@
using System.Reflection; using System.Reflection;
[assembly: AssemblyVersion("10.8.0")] [assembly: AssemblyVersion("10.9.0")]
[assembly: AssemblyFileVersion("10.8.0")] [assembly: AssemblyFileVersion("10.9.0")]

View File

@ -1,7 +1,7 @@
--- ---
# We just wrap `build` so this is really it # We just wrap `build` so this is really it
name: "jellyfin" name: "jellyfin"
version: "10.8.0" version: "10.9.0"
packages: packages:
- debian.amd64 - debian.amd64
- debian.arm64 - debian.arm64

6
debian/changelog vendored
View File

@ -1,3 +1,9 @@
jellyfin-server (10.9.0-1) unstable; urgency=medium
* New upstream version 10.9.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.9.0
-- Jellyfin Packaging Team <packaging@jellyfin.org> Wed, 13 Jul 2022 20:58:08 -0600
jellyfin-server (10.8.0-1) unstable; urgency=medium jellyfin-server (10.8.0-1) unstable; urgency=medium
* Forthcoming stable release * Forthcoming stable release

View File

@ -5,7 +5,7 @@ Homepage: https://jellyfin.org
Standards-Version: 3.9.2 Standards-Version: 3.9.2
Package: jellyfin Package: jellyfin
Version: 10.8.0 Version: 10.9.0
Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org> Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org>
Depends: jellyfin-server, jellyfin-web Depends: jellyfin-server, jellyfin-web
Description: Provides the Jellyfin Free Software Media System Description: Provides the Jellyfin Free Software Media System

2
debian/rules vendored
View File

@ -40,7 +40,7 @@ override_dh_clistrip:
override_dh_auto_build: override_dh_auto_build:
dotnet publish -maxcpucount:1 --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \ dotnet publish -maxcpucount:1 --configuration $(CONFIG) --output='$(CURDIR)/usr/lib/jellyfin/bin' --self-contained --runtime $(DOTNETRUNTIME) \
"-p:DebugSymbols=false;DebugType=none" Jellyfin.Server -p:DebugSymbols=false -p:DebugType=none Jellyfin.Server
override_dh_auto_clean: override_dh_auto_clean:
dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true dotnet clean -maxcpucount:1 --configuration $(CONFIG) Jellyfin.Server || true

View File

@ -13,7 +13,7 @@ RUN yum update -yq \
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
# Install DotNET SDK # Install DotNET SDK
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/77d472e5-194c-421e-992d-e4ca1d08e6cc/56c61ac303ddf1b12026151f4f000a2b/dotnet-sdk-6.0.301-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ RUN wget -q https://download.visualstudio.microsoft.com/download/pr/cd0d0a4d-2a6a-4d0d-b42e-dfd3b880e222/008a93f83aba6d1acf75ded3d2cfba24/dotnet-sdk-6.0.400-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
&& mkdir -p dotnet-sdk \ && mkdir -p dotnet-sdk \
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet

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