aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci-compat.yml4
-rw-r--r--.github/workflows/openapi-generate.yml2
-rw-r--r--.github/workflows/openapi-pull-request.yml2
-rw-r--r--Directory.Packages.props2
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitor.cs1
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json2
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs2
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs9
-rw-r--r--Jellyfin.Server.Implementations/Item/PeopleRepository.cs15
-rw-r--r--Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs57
-rw-r--r--Jellyfin.Server/Program.cs3
-rw-r--r--Jellyfin.Server/ServerSetupApp/SetupServer.cs1
-rw-r--r--MediaBrowser.Controller/Entities/InternalPeopleQuery.cs6
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs3
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs6
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs2
-rw-r--r--MediaBrowser.Model/Entities/MediaStream.cs1
-rw-r--r--MediaBrowser.Model/System/FolderStorageInfo.cs11
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs2
-rw-r--r--tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs55
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs21
22 files changed, 186 insertions, 25 deletions
diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml
index f9e2fbc3a6..dd48209a1f 100644
--- a/.github/workflows/ci-compat.yml
+++ b/.github/workflows/ci-compat.yml
@@ -26,7 +26,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: abi-head
retention-days: 14
@@ -65,7 +65,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: abi-base
retention-days: 14
diff --git a/.github/workflows/openapi-generate.yml b/.github/workflows/openapi-generate.yml
index 255cc49e82..dbfaf9d30b 100644
--- a/.github/workflows/openapi-generate.yml
+++ b/.github/workflows/openapi-generate.yml
@@ -36,7 +36,7 @@ jobs:
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter Jellyfin.Server.Integration.Tests.OpenApiSpecTests
- name: Upload Artifact
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.artifact }}
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
diff --git a/.github/workflows/openapi-pull-request.yml b/.github/workflows/openapi-pull-request.yml
index 563a0a406f..4acd0f4d4f 100644
--- a/.github/workflows/openapi-pull-request.yml
+++ b/.github/workflows/openapi-pull-request.yml
@@ -74,7 +74,7 @@ jobs:
docker run -v /tmp/openapi-report:/data openapitools/openapi-diff:2.1.6 /data/base.json /data/head.json --state -l ERROR --markdown /data/openapi-report.md
- name: Upload Artifact
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: openapi-report
path: /tmp/openapi-report/openapi-report.md
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f15f7c7a75..bf79ff0e0e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -47,7 +47,7 @@
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.5" />
- <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
+ <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
<PackageVersion Include="Moq" Version="4.18.4" />
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index 7cff2a25b6..23bd5cf200 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -60,6 +60,7 @@ namespace Emby.Server.Implementations.IO
_fileSystem = fileSystem;
appLifetime.ApplicationStarted.Register(Start);
+ appLifetime.ApplicationStopping.Register(Stop);
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index fce3a614c2..ec5cbf0d43 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -63,8 +63,8 @@
"Photos": "Fotos",
"Playlists": "Llistes de reproducció",
"Plugin": "Complement",
- "PluginInstalledWithName": "{0} s'ha instal·lat",
- "PluginUninstalledWithName": "{0} s'ha desinstal·lat",
+ "PluginInstalledWithName": "S'ha instal·lat {0}",
+ "PluginUninstalledWithName": "S'ha desinstal·lat {0}",
"PluginUpdatedWithName": "S'ha actualitzat {0}",
"ProviderValue": "Proveïdor: {0}",
"ScheduledTaskFailedWithName": "{0} ha fallat",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index 8ae899a73c..0a454b2938 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -29,7 +29,7 @@
"Inherit": "繼承",
"ItemAddedWithName": "{0} 經已加咗入媒體櫃",
"ItemRemovedWithName": "{0} 經已由媒體櫃移除咗",
- "LabelIpAddressValue": "IP 地址:{0}",
+ "LabelIpAddressValue": "IP 位址:{0}",
"LabelRunningTimeValue": "運行時間:{0}",
"Latest": "最新",
"MessageApplicationUpdated": "Jellyfin 經已更新咗",
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index bc80c2b405..6fca5bc1ba 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -138,7 +138,7 @@ namespace Emby.Server.Implementations.Localization
string twoCharName = parts[2];
if (string.IsNullOrWhiteSpace(twoCharName))
{
- continue;
+ twoCharName = string.Empty;
}
else if (twoCharName.Contains('-', StringComparison.OrdinalIgnoreCase))
{
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index 2b2afb0fe6..1811a219ac 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -50,6 +50,9 @@ public class PersonsController : BaseJellyfinApiController
/// <param name="startIndex">Optional. All items with a lower index will be dropped from the response.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</param>
+ /// <param name="nameStartsWith">Optional. Filter by items whose name starts with the given input string.</param>
+ /// <param name="nameLessThan">Optional. Filter by items whose name will appear before this value when sorted alphabetically.</param>
+ /// <param name="nameStartsWithOrGreater">Optional. Filter by items whose name will appear after this value when sorted alphabetically.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="filters">Optional. Specify additional filters to apply.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</param>
@@ -70,6 +73,9 @@ public class PersonsController : BaseJellyfinApiController
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
+ [FromQuery] string? nameStartsWith,
+ [FromQuery] string? nameLessThan,
+ [FromQuery] string? nameStartsWithOrGreater,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
@@ -97,6 +103,9 @@ public class PersonsController : BaseJellyfinApiController
excludePersonTypes)
{
NameContains = searchTerm,
+ NameStartsWith = nameStartsWith,
+ NameLessThan = nameLessThan,
+ NameStartsWithOrGreater = nameStartsWithOrGreater,
User = user,
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
AppearsInItemId = appearsInItemId ?? Guid.Empty,
diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
index ad9953d1b6..7147fbfe7d 100644
--- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
@@ -235,6 +235,21 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
query = query.Where(e => e.Name.ToUpper().Contains(nameContainsUpper));
}
+ if (!string.IsNullOrWhiteSpace(filter.NameStartsWith))
+ {
+ query = query.Where(e => e.Name.StartsWith(filter.NameStartsWith.ToLowerInvariant()));
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.NameLessThan))
+ {
+ query = query.Where(e => e.Name.CompareTo(filter.NameLessThan.ToLowerInvariant()) < 0);
+ }
+
+ if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater))
+ {
+ query = query.Where(e => e.Name.CompareTo(filter.NameStartsWithOrGreater.ToLowerInvariant()) >= 0);
+ }
+
return query;
}
diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
index ce628a04d0..13c7895f83 100644
--- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
+++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs
@@ -28,22 +28,44 @@ public static class StorageHelper
}
/// <summary>
- /// Gets the free space of a specific directory.
+ /// Gets the free space of the parent filesystem of a specific directory.
/// </summary>
/// <param name="path">Path to a folder.</param>
- /// <returns>The number of bytes available space.</returns>
+ /// <returns>Various details about the parent filesystem containing the directory.</returns>
public static FolderStorageInfo GetFreeSpaceOf(string path)
{
try
{
- var driveInfo = new DriveInfo(path);
+ // Fully resolve the given path to an actual filesystem target, in case it's a symlink or similar.
+ var resolvedPath = ResolvePath(path);
+ // We iterate all filesystems reported by GetDrives() here, and attempt to find the best
+ // match that contains, as deep as possible, the given path.
+ // This is required because simply calling `DriveInfo` on a path returns that path as
+ // the Name and RootDevice, which is not at all how this should work.
+ var allDrives = DriveInfo.GetDrives();
+ DriveInfo? bestMatch = null;
+ foreach (DriveInfo d in allDrives)
+ {
+ if (resolvedPath.StartsWith(d.RootDirectory.FullName, StringComparison.InvariantCultureIgnoreCase) &&
+ (bestMatch is null || d.RootDirectory.FullName.Length > bestMatch.RootDirectory.FullName.Length))
+ {
+ bestMatch = d;
+ }
+ }
+
+ if (bestMatch is null)
+ {
+ throw new InvalidOperationException($"The path `{path}` has no matching parent device. Space check invalid.");
+ }
+
return new FolderStorageInfo()
{
Path = path,
- FreeSpace = driveInfo.AvailableFreeSpace,
- UsedSpace = driveInfo.TotalSize - driveInfo.AvailableFreeSpace,
- StorageType = driveInfo.DriveType.ToString(),
- DeviceId = driveInfo.Name,
+ ResolvedPath = resolvedPath,
+ FreeSpace = bestMatch.AvailableFreeSpace,
+ UsedSpace = bestMatch.TotalSize - bestMatch.AvailableFreeSpace,
+ StorageType = bestMatch.DriveType.ToString(),
+ DeviceId = bestMatch.Name,
};
}
catch
@@ -51,6 +73,7 @@ public static class StorageHelper
return new FolderStorageInfo()
{
Path = path,
+ ResolvedPath = path,
FreeSpace = -1,
UsedSpace = -1,
StorageType = null,
@@ -60,6 +83,26 @@ public static class StorageHelper
}
/// <summary>
+ /// Walk a path and fully resolve any symlinks within it.
+ /// </summary>
+ private static string ResolvePath(string path)
+ {
+ var parts = path.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
+ var current = Path.DirectorySeparatorChar.ToString();
+ foreach (var part in parts)
+ {
+ current = Path.Combine(current, part);
+ var resolved = new DirectoryInfo(current).ResolveLinkTarget(returnFinalTarget: true);
+ if (resolved is not null)
+ {
+ current = resolved.FullName;
+ }
+ }
+
+ return current;
+ }
+
+ /// <summary>
/// Gets the underlying drive data from a given path and checks if the available storage capacity matches the threshold.
/// </summary>
/// <param name="path">The path to a folder to evaluate.</param>
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 93f71fdc69..93ba672535 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -161,7 +161,6 @@ namespace Jellyfin.Server
_loggerFactory,
options,
startupConfig);
- _appHost = appHost;
var configurationCompleted = false;
try
{
@@ -207,6 +206,7 @@ namespace Jellyfin.Server
await jellyfinMigrationService.MigrateStepAsync(JellyfinMigrationStageTypes.CoreInitialisation, appHost.ServiceProvider).ConfigureAwait(false);
await appHost.InitializeServices(startupConfig).ConfigureAwait(false);
+ _appHost = appHost;
await jellyfinMigrationService.MigrateStepAsync(JellyfinMigrationStageTypes.AppInitialisation, appHost.ServiceProvider).ConfigureAwait(false);
await jellyfinMigrationService.CleanupSystemAfterMigration(_logger).ConfigureAwait(false);
@@ -263,6 +263,7 @@ namespace Jellyfin.Server
_appHost = null;
_jellyfinHost?.Dispose();
+ _jellyfinHost = null;
}
}
diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
index 1aa39f97b6..05975929db 100644
--- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs
+++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
@@ -142,6 +142,7 @@ public sealed class SetupServer : IDisposable
ThrowIfDisposed();
var retryAfterValue = TimeSpan.FromSeconds(5);
var config = _configurationManager.GetNetworkConfiguration()!;
+ _startupServer?.Dispose();
_startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"])
.UseConsoleLifetime()
.UseSerilog()
diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
index f4b3910b0e..e12ba22343 100644
--- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs
@@ -42,6 +42,12 @@ namespace MediaBrowser.Controller.Entities
public string NameContains { get; set; }
+ public string NameStartsWith { get; set; }
+
+ public string NameLessThan { get; set; }
+
+ public string NameStartsWithOrGreater { get; set; }
+
public User User { get; set; }
public bool? IsFavorite { get; set; }
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 73c5b88c8b..770965cab3 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -1331,8 +1331,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
public bool CanExtractSubtitles(string codec)
{
- // TODO is there ever a case when a subtitle can't be extracted??
- return true;
+ return _configurationManager.GetEncodingOptions().EnableSubtitleExtraction;
}
private sealed class ProcessWrapper : IDisposable
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index d3e7b52315..3c6a03713f 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -729,6 +729,9 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.Type = MediaStreamType.Audio;
stream.LocalizedDefault = _localization.GetLocalizedString("Default");
stream.LocalizedExternal = _localization.GetLocalizedString("External");
+ stream.LocalizedLanguage = string.IsNullOrEmpty(stream.Language)
+ ? null
+ : _localization.FindLanguageInfo(stream.Language)?.DisplayName;
stream.Channels = streamInfo.Channels;
@@ -767,6 +770,9 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.LocalizedForced = _localization.GetLocalizedString("Forced");
stream.LocalizedExternal = _localization.GetLocalizedString("External");
stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
+ stream.LocalizedLanguage = string.IsNullOrEmpty(stream.Language)
+ ? null
+ : _localization.FindLanguageInfo(stream.Language)?.DisplayName;
if (string.IsNullOrEmpty(stream.Title))
{
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index 75b8c137f7..c9697c685c 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -1555,7 +1555,7 @@ namespace MediaBrowser.Model.Dlna
continue;
}
- if (!subtitleStream.IsExternal && !transcoderSupport.CanExtractSubtitles(subtitleStream.Codec))
+ if (!subtitleStream.IsExternal && playMethod == PlayMethod.Transcode && !transcoderSupport.CanExtractSubtitles(subtitleStream.Codec))
{
continue;
}
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index 11f81ff7d8..4491fb5ace 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
-using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
diff --git a/MediaBrowser.Model/System/FolderStorageInfo.cs b/MediaBrowser.Model/System/FolderStorageInfo.cs
index 7b10e4ea58..ebca39228b 100644
--- a/MediaBrowser.Model/System/FolderStorageInfo.cs
+++ b/MediaBrowser.Model/System/FolderStorageInfo.cs
@@ -11,17 +11,22 @@ public record FolderStorageInfo
public required string Path { get; init; }
/// <summary>
- /// Gets the free space of the underlying storage device of the <see cref="Path"/>.
+ /// Gets the fully resolved path of the folder in question (interpolating any symlinks if present).
+ /// </summary>
+ public required string ResolvedPath { get; init; }
+
+ /// <summary>
+ /// Gets the free space of the underlying storage device of the <see cref="ResolvedPath"/>.
/// </summary>
public long FreeSpace { get; init; }
/// <summary>
- /// Gets the used space of the underlying storage device of the <see cref="Path"/>.
+ /// Gets the used space of the underlying storage device of the <see cref="ResolvedPath"/>.
/// </summary>
public long UsedSpace { get; init; }
/// <summary>
- /// Gets the kind of storage device of the <see cref="Path"/>.
+ /// Gets the kind of storage device of the <see cref="ResolvedPath"/>.
/// </summary>
public string? StorageType { get; init; }
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index bde23e842f..a89f059060 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -366,6 +366,8 @@ namespace MediaBrowser.Providers.MediaInfo
blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace;
blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer;
blurayVideoStream.ColorPrimaries = ffmpegVideoStream.ColorPrimaries;
+ blurayVideoStream.BitDepth = ffmpegVideoStream.BitDepth;
+ blurayVideoStream.PixelFormat = ffmpegVideoStream.PixelFormat;
}
}
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
index 2c1080ffe3..8269ae58cd 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
@@ -617,5 +617,60 @@ namespace Jellyfin.Model.Tests
return (path, query, filename, extension);
}
+
+ [Theory]
+ // EnableSubtitleExtraction = false, internal subtitles
+ [InlineData("srt", "srt", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
+ [InlineData("srt", "srt", false, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)]
+ [InlineData("pgssub", "pgssub", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
+ [InlineData("pgssub", "pgssub", false, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)]
+ [InlineData("pgssub", "srt", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
+ // EnableSubtitleExtraction = false, external subtitles
+ [InlineData("srt", "srt", false, true, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
+ // EnableSubtitleExtraction = true, internal subtitles
+ [InlineData("srt", "srt", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
+ [InlineData("pgssub", "pgssub", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
+ [InlineData("pgssub", "pgssub", true, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)]
+ [InlineData("pgssub", "srt", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
+ // EnableSubtitleExtraction = true, external subtitles
+ [InlineData("srt", "srt", true, true, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
+ public void GetSubtitleProfile_RespectsExtractionSetting(
+ string codec,
+ string profileFormat,
+ bool enableSubtitleExtraction,
+ bool isExternal,
+ PlayMethod playMethod,
+ SubtitleDeliveryMethod expectedMethod)
+ {
+ var mediaSource = new MediaSourceInfo();
+ var subtitleStream = new MediaStream
+ {
+ Type = MediaStreamType.Subtitle,
+ Index = 0,
+ IsExternal = isExternal,
+ Path = isExternal ? "/media/sub." + codec : null,
+ Codec = codec,
+ SupportsExternalStream = MediaStream.IsTextFormat(codec)
+ };
+
+ var subtitleProfiles = new[]
+ {
+ new SubtitleProfile { Format = profileFormat, Method = SubtitleDeliveryMethod.External }
+ };
+
+ var transcoderSupport = new Mock<ITranscoderSupport>();
+ transcoderSupport.Setup(t => t.CanExtractSubtitles(It.IsAny<string>())).Returns(enableSubtitleExtraction);
+
+ var result = StreamBuilder.GetSubtitleProfile(
+ mediaSource,
+ subtitleStream,
+ subtitleProfiles,
+ playMethod,
+ transcoderSupport.Object,
+ null,
+ null);
+
+ Assert.Equal(expectedMethod, result.Method);
+ }
}
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index e60522bf78..700ac5dced 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -41,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var cultures = localizationManager.GetCultures().ToList();
- Assert.Equal(194, cultures.Count);
+ Assert.Equal(496, cultures.Count);
var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal));
Assert.NotNull(germany);
@@ -99,6 +99,25 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
Assert.Contains("ger", germany.ThreeLetterISOLanguageNames);
}
+ [Theory]
+ [InlineData("mul", "Multiple languages")]
+ [InlineData("und", "Undetermined")]
+ [InlineData("mis", "Uncoded languages")]
+ [InlineData("zxx", "No linguistic content; Not applicable")]
+ public async Task FindLanguageInfo_ISO6392Only_Success(string code, string expectedDisplayName)
+ {
+ var localizationManager = Setup(new ServerConfiguration
+ {
+ UICulture = "en-US"
+ });
+ await localizationManager.LoadAll();
+
+ var culture = localizationManager.FindLanguageInfo(code);
+ Assert.NotNull(culture);
+ Assert.Equal(expectedDisplayName, culture.DisplayName);
+ Assert.Equal(code, culture.ThreeLetterISOLanguageName);
+ }
+
[Fact]
public async Task GetParentalRatings_Default_Success()
{