Compare commits

..

16 Commits

Author SHA1 Message Date
theguymadmax
fb32709259 Backport pull request #16046 from jellyfin/release-10.11.z
Some checks are pending
CodeQL / Analyze (csharp) (push) Waiting to run
OpenAPI / OpenAPI - HEAD (push) Waiting to run
OpenAPI / OpenAPI - BASE (push) Waiting to run
OpenAPI / OpenAPI - Difference (push) Blocked by required conditions
OpenAPI / OpenAPI - Publish Unstable Spec (push) Blocked by required conditions
OpenAPI / OpenAPI - Publish Stable Spec (push) Blocked by required conditions
Tests / run-tests (macos-latest) (push) Waiting to run
Tests / run-tests (ubuntu-latest) (push) Waiting to run
Tests / run-tests (windows-latest) (push) Waiting to run
Project Automation / Project board (push) Waiting to run
Merge Conflict Labeler / Labeling (push) Waiting to run
Restore weekly refresh for library folder images

Original-merge: 338b480217

Merged-by: cvium <cvium@users.noreply.github.com>

Backported-by: Bond_009 <bond.009@outlook.com>
2026-01-18 11:30:44 -05:00
theguymadmax
c9b7c5bb56 Backport pull request #16029 from jellyfin/release-10.11.z
Skip hidden directories and .ignore paths in library monitoring

Original-merge: 2cb7fb52d2

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2026-01-18 11:30:43 -05:00
theguymadmax
42ff253339 Backport pull request #16020 from jellyfin/release-10.11.z
Fix birthplace not saving correctly

Original-merge: 49775b1f6a

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2026-01-18 11:30:42 -05:00
Shadowghost
c4ffc357a3 Backport pull request #15983 from jellyfin/release-10.11.z
Prioritize better matches on search

Original-merge: a518160a6f

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2026-01-18 11:30:41 -05:00
Collin-Swish
afcaec0a89 Backport pull request #15965 from jellyfin/release-10.11.z
Add mblink creation logic to library update endpoint.

Original-merge: 22d593b8e9

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2026-01-18 11:30:39 -05:00
MarcoCoreDuo
09edca8b7a Backport pull request #15899 from jellyfin/release-10.11.z
Fix watched state not kept on Media replace/rename

Original-merge: 8433b6d8a4

Merged-by: joshuaboniface <joshua@boniface.me>

Backported-by: Bond_009 <bond.009@outlook.com>
2026-01-18 11:30:38 -05:00
Shadowghost
f9fd34b11e Backport pull request #15872 from jellyfin/release-10.11.z
Be more strict about PersonType assignment

Original-merge: b56de6493f

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2026-01-18 11:30:37 -05:00
theguymadmax
aa666565d1 Backport pull request #15808 from jellyfin/release-10.11.z
Trim music artist names

Original-merge: 093cfc3f3b

Merged-by: crobibero <cody@robibe.ro>

Backported-by: Bond_009 <bond.009@outlook.com>
2026-01-18 11:30:36 -05:00
Bond-009
f83e8ee806 Merge pull request #15928 from jellyfin/renovate/asynckeyedlock-8.x
Update dependency AsyncKeyedLock to v8
2026-01-18 16:33:39 +01:00
renovate[bot]
84ebed1eb7 Update Microsoft to v5 (#15486) 2026-01-18 07:40:09 -07:00
Bond-009
1d7c6af520 Merge pull request #15481 from jellyfin/renovate/major-dotnet-monorepo
Update dependency dotnet-ef to v10
2026-01-18 12:20:25 +01:00
Bond-009
226da3b371 Fix zh-CN subtitle language display (#15947)
The DisplayTitle property was using .NET's CultureInfo.GetCultures(NeutralCultures)
to resolve language display names. Since zh-CN is a specific culture (not neutral),
it would fall back to the base 'zh' code, resulting in generic 'Chinese' instead
of 'Chinese (Simplified)'.

This change adds a LocalizedLanguage property to MediaStream that gets populated
via LocalizationManager.FindLanguageInfo() when streams are retrieved from the
database. This leverages Jellyfin's existing iso6392.txt mappings which correctly
map zh-CN to 'Chinese (Simplified)'.

The same pattern is already used for other localized strings like LocalizedDefault
and LocalizedExternal.
2026-01-18 12:19:31 +01:00
Abitofevrything
8d052a6cb1 Merge pull request #15926 from abitofevrything/feat/accurate_hls_seeking
Refactor HLS transcode seeking
2026-01-18 12:17:06 +01:00
renovate[bot]
c464ba83f2 Update dependency dotnet-ef to v10 2026-01-13 18:53:28 +00:00
ZeusCraft10
9039077286 Fix zh-CN subtitle language display
The DisplayTitle property was using .NET's CultureInfo.GetCultures(NeutralCultures)
to resolve language display names. Since zh-CN is a specific culture (not neutral),
it would fall back to the base 'zh' code, resulting in generic 'Chinese' instead
of 'Chinese (Simplified)'.

This change adds a LocalizedLanguage property to MediaStream that gets populated
via LocalizationManager.FindLanguageInfo() when streams are retrieved from the
database. This leverages Jellyfin's existing iso6392.txt mappings which correctly
map zh-CN to 'Chinese (Simplified)'.

The same pattern is already used for other localized strings like LocalizedDefault
and LocalizedExternal.
2026-01-05 06:22:44 -05:00
renovate[bot]
e02a2ae48f Update dependency AsyncKeyedLock to v8 2026-01-02 10:49:18 +00:00
55 changed files with 288 additions and 301 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "9.0.11",
"version": "10.0.2",
"commands": [
"dotnet-ef"
]

View File

@@ -210,6 +210,7 @@
- [bjorntp](https://github.com/bjorntp)
- [martenumberto](https://github.com/martenumberto)
- [ZeusCraft10](https://github.com/ZeusCraft10)
- [MarcoCoreDuo](https://github.com/MarcoCoreDuo)
# Emby Contributors

View File

@@ -4,7 +4,7 @@
</PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies">
<PackageVersion Include="AsyncKeyedLock" Version="7.1.8" />
<PackageVersion Include="AsyncKeyedLock" Version="8.0.0" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
@@ -14,7 +14,7 @@
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Diacritics" Version="4.1.4" />
<PackageVersion Include="Diacritics" Version="4.0.17" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="FsCheck.Xunit" Version="3.3.2" />
@@ -29,8 +29,8 @@
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" />

View File

@@ -352,6 +352,12 @@ namespace Emby.Server.Implementations.IO
return;
}
var fileInfo = _fileSystem.GetFileSystemInfo(path);
if (DotIgnoreIgnoreRule.IsIgnored(fileInfo, null))
{
return;
}
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
foreach (var i in _tempIgnoredPaths.Keys)
{

View File

@@ -98,5 +98,11 @@ namespace Emby.Server.Implementations.Images
return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex);
}
protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
{
var age = DateTime.UtcNow - image.DateModified;
return age.TotalDays > 7;
}
}
}

View File

@@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Library
// Unix hidden files
"**/.*",
"**/.*/**",
// Mac - if you ever remove the above.
// "**/._*",

View File

@@ -2202,6 +2202,12 @@ namespace Emby.Server.Implementations.Library
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken);
/// <inheritdoc />
public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
{
await _itemRepository.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
}
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
{
if (item.IsFileProtocol)
@@ -3195,19 +3201,7 @@ namespace Emby.Server.Implementations.Library
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
var shortcutFilename = Path.GetFileNameWithoutExtension(path);
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
while (File.Exists(lnk))
{
shortcutFilename += "1";
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
}
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
RemoveContentTypeOverrides(path);
CreateShortcut(virtualFolderPath, pathInfo);
if (saveLibraryOptions)
{
@@ -3372,5 +3366,24 @@ namespace Emby.Server.Implementations.Library
return item is UserRootFolder || item.IsVisibleStandalone(user);
}
public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo)
{
var path = pathInfo.Path;
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var shortcutFilename = Path.GetFileNameWithoutExtension(path);
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
while (File.Exists(lnk))
{
shortcutFilename += "1";
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
}
_fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
RemoveContentTypeOverrides(path);
}
}
}

View File

@@ -50,7 +50,6 @@ public class AudioController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -107,7 +106,6 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate,
@@ -159,7 +157,6 @@ public class AudioController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate,
@@ -217,7 +214,6 @@ public class AudioController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -274,7 +270,6 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate,
@@ -326,7 +321,6 @@ public class AudioController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate,

View File

@@ -122,7 +122,6 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -182,7 +181,6 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate,
@@ -238,7 +236,6 @@ public class DynamicHlsController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate,
@@ -364,7 +361,6 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -425,7 +421,6 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate,
@@ -481,7 +476,6 @@ public class DynamicHlsController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate,
@@ -543,7 +537,6 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
@@ -601,7 +594,6 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? maxStreamingBitrate,
@@ -654,7 +646,6 @@ public class DynamicHlsController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
@@ -713,7 +704,6 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -771,7 +761,6 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate,
@@ -826,7 +815,6 @@ public class DynamicHlsController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate,
@@ -887,7 +875,6 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
@@ -943,7 +930,6 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? maxStreamingBitrate,
@@ -996,7 +982,6 @@ public class DynamicHlsController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
@@ -1060,7 +1045,6 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -1124,7 +1108,6 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate,
@@ -1181,7 +1164,6 @@ public class DynamicHlsController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate,
@@ -1247,7 +1229,6 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
@@ -1309,7 +1290,6 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? maxStreamingBitrate,
@@ -1364,7 +1344,6 @@ public class DynamicHlsController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
@@ -1586,16 +1565,6 @@ public class DynamicHlsController : BaseJellyfinApiController
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
if (state.BaseRequest.BreakOnNonKeyFrames)
{
// FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe
// breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable
// to produce a missing part of video stream before first keyframe is encountered, which may lead to
// awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js
_logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request");
state.BaseRequest.BreakOnNonKeyFrames = false;
}
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
@@ -1746,11 +1715,6 @@ public class DynamicHlsController : BaseJellyfinApiController
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs;
if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
{
return copyArgs + " -copypriorss:a:0 0";
}
return copyArgs;
}

View File

@@ -421,7 +421,7 @@ public class ItemUpdateController : BaseJellyfinApiController
{
if (item is IHasAlbumArtist hasAlbumArtists)
{
hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name);
hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name.Trim());
}
}
@@ -429,7 +429,7 @@ public class ItemUpdateController : BaseJellyfinApiController
{
if (item is IHasArtist hasArtists)
{
hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name);
hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name.Trim());
}
}

View File

@@ -342,6 +342,17 @@ public class LibraryStructureController : BaseJellyfinApiController
return NotFound();
}
LibraryOptions options = item.GetLibraryOptions();
foreach (var mediaPath in request.LibraryOptions!.PathInfos)
{
if (options.PathInfos.Any(i => i.Path == mediaPath.Path))
{
continue;
}
_libraryManager.CreateShortcut(item.Path, mediaPath);
}
item.UpdateLibraryOptions(request.LibraryOptions);
return NoContent();
}

View File

@@ -83,7 +83,6 @@ public class UniversalAudioController : BaseJellyfinApiController
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param>
/// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
/// <response code="200">Audio stream returned.</response>
/// <response code="302">Redirected to remote audio stream.</response>
@@ -114,7 +113,6 @@ public class UniversalAudioController : BaseJellyfinApiController
[FromQuery] int? maxAudioBitDepth,
[FromQuery] bool? enableRemoteMedia,
[FromQuery] bool enableAudioVbrEncoding = true,
[FromQuery] bool breakOnNonKeyFrames = false,
[FromQuery] bool enableRedirection = true)
{
userId = RequestHelpers.GetUserId(User, userId);
@@ -127,7 +125,7 @@ public class UniversalAudioController : BaseJellyfinApiController
return NotFound();
}
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
@@ -208,7 +206,6 @@ public class UniversalAudioController : BaseJellyfinApiController
EnableAutoStreamCopy = true,
AllowAudioStreamCopy = true,
AllowVideoStreamCopy = true,
BreakOnNonKeyFrames = breakOnNonKeyFrames,
AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels,
MaxAudioBitDepth = maxAudioBitDepth,
@@ -242,7 +239,6 @@ public class UniversalAudioController : BaseJellyfinApiController
EnableAutoStreamCopy = true,
AllowAudioStreamCopy = true,
AllowVideoStreamCopy = true,
BreakOnNonKeyFrames = breakOnNonKeyFrames,
AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
@@ -263,7 +259,6 @@ public class UniversalAudioController : BaseJellyfinApiController
string? transcodingContainer,
string? audioCodec,
MediaStreamProtocol? transcodingProtocol,
bool? breakOnNonKeyFrames,
int? transcodingAudioChannels,
int? maxAudioSampleRate,
int? maxAudioBitDepth,
@@ -298,7 +293,6 @@ public class UniversalAudioController : BaseJellyfinApiController
Container = transcodingContainer ?? "mp3",
AudioCodec = audioCodec ?? "mp3",
Protocol = transcodingProtocol ?? MediaStreamProtocol.http,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
}
};

View File

@@ -270,7 +270,6 @@ public class VideosController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -329,7 +328,6 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate,
@@ -386,7 +384,6 @@ public class VideosController : BaseJellyfinApiController
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate,
@@ -511,7 +508,6 @@ public class VideosController : BaseJellyfinApiController
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
@@ -570,7 +566,6 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? audioBitRate,
@@ -624,7 +619,6 @@ public class VideosController : BaseJellyfinApiController
enableAutoStreamCopy,
allowVideoStreamCopy,
allowAudioStreamCopy,
breakOnNonKeyFrames,
audioSampleRate,
maxAudioBitDepth,
audioBitRate,

View File

@@ -201,7 +201,7 @@ public static class StreamingHelpers
state.OutputVideoCodec = state.Request.VideoCodec;
state.OutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
encodingHelper.TryStreamCopy(state);
encodingHelper.TryStreamCopy(state, encodingOptions);
if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
{

View File

@@ -624,7 +624,6 @@ public sealed class BaseItemRepository
var ids = tuples.Select(f => f.Item.Id).ToArray();
var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
foreach (var item in tuples)
{
@@ -658,19 +657,6 @@ public sealed class BaseItemRepository
context.SaveChanges();
foreach (var item in newItems)
{
// reattach old userData entries
var userKeys = item.UserDataKey.ToArray();
var retentionDate = (DateTime?)null;
context.UserData
.Where(e => e.ItemId == PlaceholderId)
.Where(e => userKeys.Contains(e.CustomDataKey))
.ExecuteUpdate(e => e
.SetProperty(f => f.ItemId, item.Item.Id)
.SetProperty(f => f.RetentionDate, retentionDate));
}
var itemValueMaps = tuples
.Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
.ToArray();
@@ -766,6 +752,29 @@ public sealed class BaseItemRepository
transaction.Commit();
}
/// <inheritdoc />
public async Task ReattachUserDataAsync(BaseItemDto item, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(item);
cancellationToken.ThrowIfCancellationRequested();
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var userKeys = item.GetUserDataKeys().ToArray();
var retentionDate = (DateTime?)null;
await dbContext.UserData
.Where(e => e.ItemId == PlaceholderId)
.Where(e => userKeys.Contains(e.CustomDataKey))
.ExecuteUpdateAsync(
e => e
.SetProperty(f => f.ItemId, item.Id)
.SetProperty(f => f.RetentionDate, retentionDate),
cancellationToken).ConfigureAwait(false);
}
}
/// <inheritdoc />
public BaseItemDto? RetrieveItem(Guid id)
{
@@ -873,7 +882,7 @@ public sealed class BaseItemRepository
}
dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
dto.ProductionLocations = entity.ProductionLocations?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
dto.Studios = entity.Studios?.Split('|') ?? [];
dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
@@ -1035,7 +1044,7 @@ public sealed class BaseItemRepository
}
entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : null;
entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p))) : null;
entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
@@ -1606,29 +1615,36 @@ public sealed class BaseItemRepository
IOrderedQueryable<BaseItemEntity>? orderedQuery = null;
// When searching, prioritize by match quality: exact match > prefix match > contains
if (hasSearch)
{
orderedQuery = query.OrderBy(OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!));
}
var firstOrdering = orderBy.FirstOrDefault();
if (firstOrdering != default)
{
var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
if (firstOrdering.SortOrder == SortOrder.Ascending)
if (orderedQuery is null)
{
orderedQuery = query.OrderBy(expression);
// No search relevance ordering, start fresh
orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
? query.OrderBy(expression)
: query.OrderByDescending(expression);
}
else
{
orderedQuery = query.OrderByDescending(expression);
// Search relevance ordering already applied, chain with ThenBy
orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
? orderedQuery.ThenBy(expression)
: orderedQuery.ThenByDescending(expression);
}
if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
{
if (firstOrdering.SortOrder is SortOrder.Ascending)
{
orderedQuery = orderedQuery.ThenBy(e => e.Name);
}
else
{
orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
}
orderedQuery = firstOrdering.SortOrder is SortOrder.Ascending
? orderedQuery.ThenBy(e => e.Name)
: orderedQuery.ThenByDescending(e => e.Name);
}
}

View File

@@ -158,6 +158,12 @@ public class MediaStreamRepository : IMediaStreamRepository
dto.LocalizedDefault = _localization.GetLocalizedString("Default");
dto.LocalizedExternal = _localization.GetLocalizedString("External");
if (!string.IsNullOrEmpty(dto.Language))
{
var culture = _localization.FindLanguageInfo(dto.Language);
dto.LocalizedLanguage = culture?.DisplayName;
}
if (dto.Type is MediaStreamType.Subtitle)
{
dto.LocalizedUndefined = _localization.GetLocalizedString("Undefined");

View File

@@ -6,6 +6,7 @@ using System.Linq.Expressions;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using Microsoft.EntityFrameworkCore;
@@ -68,4 +69,30 @@ public static class OrderMapper
_ => e => e.SortName
};
}
/// <summary>
/// Creates an expression to order search results by match quality.
/// Prioritizes: exact match (0) > prefix match with word boundary (1) > prefix match (2) > contains (3).
/// </summary>
/// <param name="searchTerm">The search term to match against.</param>
/// <returns>An expression that returns an integer representing match quality (lower is better).</returns>
public static Expression<Func<BaseItemEntity, int>> MapSearchRelevanceOrder(string searchTerm)
{
var cleanSearchTerm = GetCleanValue(searchTerm);
var searchPrefix = cleanSearchTerm + " ";
return e =>
e.CleanName == cleanSearchTerm ? 0 :
e.CleanName!.StartsWith(searchPrefix) ? 1 :
e.CleanName!.StartsWith(cleanSearchTerm) ? 2 : 3;
}
private static string GetCleanValue(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return value;
}
return value.RemoveDiacritics().ToLowerInvariant();
}
}

View File

@@ -2053,6 +2053,9 @@ namespace MediaBrowser.Controller.Entities
public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
=> await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false);
public async Task ReattachUserDataAsync(CancellationToken cancellationToken) =>
await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false);
/// <summary>
/// Validates that images within the item are still on the filesystem.
/// </summary>

View File

@@ -281,6 +281,14 @@ namespace MediaBrowser.Controller.Library
/// <returns>Returns a Task that can be awaited.</returns>
Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
/// <summary>
/// Reattaches the user data to the item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous reattachment operation.</returns>
Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the item.
/// </summary>
@@ -652,5 +660,12 @@ namespace MediaBrowser.Controller.Library
/// This exists so plugins can trigger a library scan.
/// </remarks>
void QueueLibraryScan();
/// <summary>
/// Add mblink file for a media path.
/// </summary>
/// <param name="virtualFolderPath">The path to the virtualfolder.</param>
/// <param name="pathInfo">The new virtualfolder.</param>
public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo);
}
}

View File

@@ -43,8 +43,6 @@ namespace MediaBrowser.Controller.MediaEncoding
public bool AllowAudioStreamCopy { get; set; }
public bool BreakOnNonKeyFrames { get; set; }
/// <summary>
/// Gets or sets the audio sample rate.
/// </summary>

View File

@@ -2914,8 +2914,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (time > 0)
{
// For direct streaming/remuxing, we seek at the exact position of the keyframe
// However, ffmpeg will seek to previous keyframe when the exact time is the input
// For direct streaming/remuxing, HLS segments start at keyframes.
// However, ffmpeg will seek to previous keyframe when the exact frame time is the input
// Workaround this by adding 0.5s offset to the seeking time to get the exact keyframe on most videos.
// This will help subtitle syncing.
var isHlsRemuxing = state.IsVideoRequest && state.TranscodingType is TranscodingJobType.Hls && IsCopyCodec(state.OutputVideoCodec);
@@ -2932,17 +2932,16 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.IsVideoRequest)
{
var outputVideoCodec = GetVideoEncoder(state, options);
var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.');
// Important: If this is ever re-enabled, make sure not to use it with wtv because it breaks seeking
// Disable -noaccurate_seek on mpegts container due to the timestamps issue on some clients,
// but it's still required for fMP4 container otherwise the audio can't be synced to the video.
if (!string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)
&& state.TranscodingType != TranscodingJobType.Progressive
&& !state.EnableBreakOnNonKeyFrames(outputVideoCodec)
&& (state.BaseRequest.StartTimeTicks ?? 0) > 0)
// If we are remuxing, then the copied stream cannot be seeked accurately (it will seek to the nearest
// keyframe). If we are using fMP4, then force all other streams to use the same inaccurate seeking to
// avoid A/V sync issues which cause playback issues on some devices.
// When remuxing video, the segment start times correspond to key frames in the source stream, so this
// option shouldn't change the seeked point that much.
// Important: make sure not to use it with wtv because it breaks seeking
if (state.TranscodingType is TranscodingJobType.Hls
&& string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase)
&& (IsCopyCodec(state.OutputVideoCodec) || IsCopyCodec(state.OutputAudioCodec))
&& !string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase))
{
seekParam += " -noaccurate_seek";
}
@@ -7084,7 +7083,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
#nullable disable
public void TryStreamCopy(EncodingJobInfo state)
public void TryStreamCopy(EncodingJobInfo state, EncodingOptions options)
{
if (state.VideoStream is not null && CanStreamCopyVideo(state, state.VideoStream))
{
@@ -7101,8 +7100,14 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
var preventHlsAudioCopy = state.TranscodingType is TranscodingJobType.Hls
&& state.VideoStream is not null
&& !IsCopyCodec(state.OutputVideoCodec)
&& options.HlsAudioSeekStrategy is HlsAudioSeekStrategy.TranscodeAudio;
if (state.AudioStream is not null
&& CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs))
&& CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs)
&& !preventHlsAudioCopy)
{
state.OutputAudioCodec = "copy";
}

View File

@@ -515,21 +515,6 @@ namespace MediaBrowser.Controller.MediaEncoding
public int HlsListSize => 0;
public bool EnableBreakOnNonKeyFrames(string videoCodec)
{
if (TranscodingType != TranscodingJobType.Progressive)
{
if (IsSegmentedLiveStream)
{
return false;
}
return BaseRequest.BreakOnNonKeyFrames && EncodingHelper.IsCopyCodec(videoCodec);
}
return false;
}
private int? GetMediaStreamCount(MediaStreamType type, int limit)
{
var count = MediaSource.GetStreamCount(type);

View File

@@ -35,6 +35,14 @@ public interface IItemRepository
Task SaveImagesAsync(BaseItem item, CancellationToken cancellationToken = default);
/// <summary>
/// Reattaches the user data to the item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous reattachment operation.</returns>
Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the item.
/// </summary>

View File

@@ -673,7 +673,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
if (state.VideoRequest is not null)
{
_encodingHelper.TryStreamCopy(state);
_encodingHelper.TryStreamCopy(state, encodingOptions);
}
}

View File

@@ -1,6 +1,7 @@
#pragma warning disable CA1819 // XML serialization handles collections improperly, so we need to use arrays
#nullable disable
using System.ComponentModel;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Model.Configuration;
@@ -60,6 +61,7 @@ public class EncodingOptions
SubtitleExtractionTimeoutMinutes = 30;
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = ["mkv"];
HardwareDecodingCodecs = ["h264", "vc1"];
HlsAudioSeekStrategy = HlsAudioSeekStrategy.DisableAccurateSeek;
}
/// <summary>
@@ -301,4 +303,10 @@ public class EncodingOptions
/// Gets or sets the file extensions on-demand metadata based keyframe extraction is enabled for.
/// </summary>
public string[] AllowOnDemandMetadataBasedKeyframeExtractionForExtensions { get; set; }
/// <summary>
/// Gets or sets the method used for audio seeking in HLS.
/// </summary>
[DefaultValue(HlsAudioSeekStrategy.DisableAccurateSeek)]
public HlsAudioSeekStrategy HlsAudioSeekStrategy { get; set; }
}

View File

@@ -0,0 +1,23 @@
namespace MediaBrowser.Model.Configuration
{
/// <summary>
/// An enum representing the options to seek the input audio stream when
/// transcoding HLS segments.
/// </summary>
public enum HlsAudioSeekStrategy
{
/// <summary>
/// If the video stream is transcoded and the audio stream is copied,
/// seek the video stream to the same keyframe as the audio stream. The
/// resulting timestamps in the output streams may be inaccurate.
/// </summary>
DisableAccurateSeek = 0,
/// <summary>
/// Prevent audio streams from being copied if the video stream is transcoded.
/// The resulting timestamps will be accurate, but additional audio transcoding
/// overhead will be incurred.
/// </summary>
TranscodeAudio = 1,
}
}

View File

@@ -610,7 +610,6 @@ namespace MediaBrowser.Model.Dlna
playlistItem.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
playlistItem.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
playlistItem.BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames;
playlistItem.EnableAudioVbrEncoding = transcodingProfile.EnableAudioVbrEncoding;
if (transcodingProfile.MinSegments > 0)

View File

@@ -86,11 +86,6 @@ public class StreamInfo
/// <value>The minimum segments count.</value>
public int? MinSegments { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the stream can be broken on non-keyframes.
/// </summary>
public bool BreakOnNonKeyFrames { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the stream requires AVC.
/// </summary>
@@ -1018,9 +1013,6 @@ public class StreamInfo
sb.Append("&MinSegments=");
sb.Append(MinSegments.Value.ToString(CultureInfo.InvariantCulture));
}
sb.Append("&BreakOnNonKeyFrames=");
sb.Append(BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture));
}
else
{

View File

@@ -41,7 +41,6 @@ public class TranscodingProfile
MaxAudioChannels = other.MaxAudioChannels;
MinSegments = other.MinSegments;
SegmentLength = other.SegmentLength;
BreakOnNonKeyFrames = other.BreakOnNonKeyFrames;
Conditions = other.Conditions;
EnableAudioVbrEncoding = other.EnableAudioVbrEncoding;
}
@@ -143,7 +142,8 @@ public class TranscodingProfile
/// </summary>
[DefaultValue(false)]
[XmlAttribute("breakOnNonKeyFrames")]
public bool BreakOnNonKeyFrames { get; set; }
[Obsolete("This is always false")]
public bool? BreakOnNonKeyFrames { get; set; }
/// <summary>
/// Gets or sets the profile conditions.

View File

@@ -260,6 +260,8 @@ namespace MediaBrowser.Model.Entities
public string LocalizedHearingImpaired { get; set; }
public string LocalizedLanguage { get; set; }
public string DisplayTitle
{
get
@@ -273,29 +275,8 @@ namespace MediaBrowser.Model.Entities
// Do not display the language code in display titles if unset or set to a special code. Show it in all other cases (possibly expanded).
if (!string.IsNullOrEmpty(Language) && !_specialCodes.Contains(Language, StringComparison.OrdinalIgnoreCase))
{
// Get full language string i.e. eng -> English, zh-Hans -> Chinese (Simplified).
var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
CultureInfo match = null;
if (Language.Contains('-', StringComparison.OrdinalIgnoreCase))
{
match = cultures.FirstOrDefault(r =>
r.Name.Equals(Language, StringComparison.OrdinalIgnoreCase));
if (match is null)
{
string baseLang = Language.AsSpan().LeftPart('-').ToString();
match = cultures.FirstOrDefault(r =>
r.TwoLetterISOLanguageName.Equals(baseLang, StringComparison.OrdinalIgnoreCase));
}
}
else
{
match = cultures.FirstOrDefault(r =>
r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase));
}
string fullLanguage = match?.DisplayName;
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
// Use pre-resolved localized language name, falling back to raw language code.
attributes.Add(StringHelper.FirstToUpper(LocalizedLanguage ?? Language));
}
if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
@@ -393,29 +374,8 @@ namespace MediaBrowser.Model.Entities
if (!string.IsNullOrEmpty(Language))
{
// Get full language string i.e. eng -> English, zh-Hans -> Chinese (Simplified).
var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
CultureInfo match = null;
if (Language.Contains('-', StringComparison.OrdinalIgnoreCase))
{
match = cultures.FirstOrDefault(r =>
r.Name.Equals(Language, StringComparison.OrdinalIgnoreCase));
if (match is null)
{
string baseLang = Language.AsSpan().LeftPart('-').ToString();
match = cultures.FirstOrDefault(r =>
r.TwoLetterISOLanguageName.Equals(baseLang, StringComparison.OrdinalIgnoreCase));
}
}
else
{
match = cultures.FirstOrDefault(r =>
r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase));
}
string fullLanguage = match?.DisplayName;
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
// Use pre-resolved localized language name, falling back to raw language code.
attributes.Add(StringHelper.FirstToUpper(LocalizedLanguage ?? Language));
}
else
{

View File

@@ -153,7 +153,7 @@ namespace MediaBrowser.Providers.Manager
if (isFirstRefresh)
{
await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, false, cancellationToken).ConfigureAwait(false);
}
// Next run metadata providers
@@ -247,7 +247,7 @@ namespace MediaBrowser.Providers.Manager
}
// Save to database
await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false);
await SaveItemAsync(metadataResult, updateType, isFirstRefresh, cancellationToken).ConfigureAwait(false);
}
return updateType;
@@ -275,9 +275,14 @@ namespace MediaBrowser.Providers.Manager
}
}
protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken)
protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, bool reattachUserData, CancellationToken cancellationToken)
{
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
if (reattachUserData)
{
await result.Item.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
}
if (result.Item.SupportsPeople && result.People is not null)
{
var baseItem = result.Item;

View File

@@ -303,9 +303,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
.Where(entry =>
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
.Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
if (config.HideMissingCrewMembers)
{

View File

@@ -275,9 +275,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
.Where(entry =>
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
.Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
if (config.HideMissingCrewMembers)
{

View File

@@ -120,9 +120,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
.Where(entry =>
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
.Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
if (config.HideMissingCrewMembers)
{

View File

@@ -367,9 +367,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
.Where(entry =>
TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
.Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
if (config.HideMissingCrewMembers)
{

View File

@@ -70,18 +70,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
public static PersonKind MapCrewToPersonType(Crew crew)
{
if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
&& crew.Job.Contains("director", StringComparison.OrdinalIgnoreCase))
&& crew.Job.Equals("director", StringComparison.OrdinalIgnoreCase))
{
return PersonKind.Director;
}
if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
&& crew.Job.Contains("producer", StringComparison.OrdinalIgnoreCase))
&& crew.Job.Equals("producer", StringComparison.OrdinalIgnoreCase))
{
return PersonKind.Producer;
}
if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase))
if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase)
&& crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase))
{
return PersonKind.Writer;
}

View File

@@ -134,8 +134,6 @@ public class LegacyStreamInfo : StreamInfo
{
list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
}
list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)));
}
else
{

View File

@@ -108,6 +108,49 @@ namespace Jellyfin.Model.Tests.Entities
IsExternal = true
});
// Test LocalizedLanguage is used when set (fixes zh-CN display issue #15935)
data.Add(
"Chinese (Simplified) - SRT",
new MediaStream
{
Type = MediaStreamType.Subtitle,
Title = null,
Language = "zh-CN",
LocalizedLanguage = "Chinese (Simplified)",
IsForced = false,
IsDefault = false,
Codec = "SRT"
});
// Test LocalizedLanguage for audio streams
data.Add(
"Japanese - AAC - Stereo",
new MediaStream
{
Type = MediaStreamType.Audio,
Title = null,
Language = "jpn",
LocalizedLanguage = "Japanese",
IsForced = false,
IsDefault = false,
Codec = "AAC",
ChannelLayout = "stereo"
});
// Test fallback to Language when LocalizedLanguage is null
data.Add(
"Eng - ASS",
new MediaStream
{
Type = MediaStreamType.Subtitle,
Title = null,
Language = "eng",
LocalizedLanguage = null,
IsForced = false,
IsDefault = false,
Codec = "ASS"
});
return data;
}

View File

@@ -152,7 +152,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -169,7 +168,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -185,7 +183,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],

View File

@@ -130,7 +130,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -146,7 +145,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],

View File

@@ -127,7 +127,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -144,7 +143,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -161,7 +159,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -178,7 +175,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -195,7 +191,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -212,7 +207,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -229,7 +223,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -246,7 +239,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -263,7 +255,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -281,7 +272,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -298,7 +288,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],

View File

@@ -107,7 +107,6 @@
"Protocol": "hls",
"MaxAudioChannels": "2",
"MinSegments": "2",
"BreakOnNonKeyFrames": true,
"EnableAudioVbrEncoding": true
},
{
@@ -182,8 +181,7 @@
"Context": "Streaming",
"Protocol": "hls",
"MaxAudioChannels": "2",
"MinSegments": "2",
"BreakOnNonKeyFrames": true
"MinSegments": "2"
},
{
"Container": "ts",
@@ -193,8 +191,7 @@
"Context": "Streaming",
"Protocol": "hls",
"MaxAudioChannels": "2",
"MinSegments": "2",
"BreakOnNonKeyFrames": true
"MinSegments": "2"
}
],
"ContainerProfiles": [],

View File

@@ -95,7 +95,6 @@
"TranscodingProfiles": [
{
"AudioCodec": "aac",
"BreakOnNonKeyFrames": true,
"Container": "mp4",
"Context": "Streaming",
"EnableAudioVbrEncoding": true,
@@ -170,7 +169,6 @@
},
{
"AudioCodec": "aac,mp2,opus,flac",
"BreakOnNonKeyFrames": true,
"Container": "mp4",
"Context": "Streaming",
"MaxAudioChannels": "2",
@@ -181,7 +179,6 @@
},
{
"AudioCodec": "aac,mp3,mp2",
"BreakOnNonKeyFrames": true,
"Container": "ts",
"Context": "Streaming",
"MaxAudioChannels": "2",

View File

@@ -30,7 +30,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -48,7 +47,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -62,7 +60,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],

View File

@@ -30,7 +30,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -48,7 +47,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -62,7 +60,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],

View File

@@ -49,7 +49,6 @@
"MaxAudioChannels": " 2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -66,7 +65,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -83,7 +81,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -100,7 +97,6 @@
"MaxAudioChannels": " 2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -118,7 +114,6 @@
"MaxAudioChannels": " 2",
"MinSegments": 1,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -135,7 +130,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],

View File

@@ -49,7 +49,6 @@
"MaxAudioChannels": " 2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -66,7 +65,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -83,7 +81,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -100,7 +97,6 @@
"MaxAudioChannels": " 2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -118,7 +114,6 @@
"MaxAudioChannels": " 2",
"MinSegments": 1,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -135,7 +130,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],

View File

@@ -114,7 +114,6 @@
"Protocol": "hls",
"MaxAudioChannels": "6",
"MinSegments": "2",
"BreakOnNonKeyFrames": true,
"EnableAudioVbrEncoding": true
},
{
@@ -173,8 +172,7 @@
"Context": "Streaming",
"Protocol": "hls",
"MaxAudioChannels": "2",
"MinSegments": "2",
"BreakOnNonKeyFrames": true
"MinSegments": "2"
},
{
"Container": "ts",
@@ -184,8 +182,7 @@
"Context": "Streaming",
"Protocol": "hls",
"MaxAudioChannels": "2",
"MinSegments": "2",
"BreakOnNonKeyFrames": true
"MinSegments": "2"
}
],
"ContainerProfiles": [],

View File

@@ -165,7 +165,6 @@
"MaxAudioChannels": "2",
"MinSegments": 1,
"SegmentLength": 0,
"BreakOnNonKeyFrames": true,
"$type": "TranscodingProfile"
},
{
@@ -182,7 +181,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -199,7 +197,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -216,7 +213,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -233,7 +229,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -250,7 +245,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -267,7 +261,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -284,7 +277,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -301,7 +293,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -319,7 +310,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",
@@ -346,7 +336,6 @@
"MaxAudioChannels": "2",
"MinSegments": 1,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",
@@ -373,7 +362,6 @@
"MaxAudioChannels": "2",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",
@@ -399,7 +387,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",

View File

@@ -165,7 +165,6 @@
"MaxAudioChannels": "6",
"MinSegments": 1,
"SegmentLength": 0,
"BreakOnNonKeyFrames": true,
"$type": "TranscodingProfile"
},
{
@@ -182,7 +181,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -199,7 +197,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -216,7 +213,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -233,7 +229,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -250,7 +245,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -267,7 +261,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -284,7 +277,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -301,7 +293,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -319,7 +310,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",
@@ -346,7 +336,6 @@
"MaxAudioChannels": "6",
"MinSegments": 1,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",
@@ -373,7 +362,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",
@@ -399,7 +387,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"Conditions": [
{
"Condition": "LessThanEqual",

View File

@@ -16,7 +16,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -28,7 +27,6 @@
"Protocol": "hls",
"MaxAudioChannels": "2",
"MinSegments": "2",
"BreakOnNonKeyFrames": true,
"$type": "TranscodingProfile"
},
{
@@ -40,7 +38,6 @@
"Protocol": "hls",
"MaxAudioChannels": "2",
"MinSegments": "2",
"BreakOnNonKeyFrames": true,
"$type": "TranscodingProfile"
},
{
@@ -64,7 +61,6 @@
"EnableSubtitlesInManifest": false,
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],

View File

@@ -135,7 +135,6 @@
"Protocol": "hls",
"MaxAudioChannels": "6",
"MinSegments": "1",
"BreakOnNonKeyFrames": false,
"EnableAudioVbrEncoding": true
},
{
@@ -210,8 +209,7 @@
"Context": "Streaming",
"Protocol": "hls",
"MaxAudioChannels": "6",
"MinSegments": "1",
"BreakOnNonKeyFrames": false
"MinSegments": "1"
}
],
"ContainerProfiles": [],

View File

@@ -52,7 +52,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -70,7 +69,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -88,7 +86,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],

View File

@@ -52,7 +52,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -70,7 +69,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
},
{
@@ -88,7 +86,6 @@
"MaxAudioChannels": "6",
"MinSegments": 0,
"SegmentLength": 0,
"BreakOnNonKeyFrames": false,
"$type": "TranscodingProfile"
}
],

View File

@@ -19,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("/media/movies/#recycle", true)]
[InlineData("thumbs.db", true)]
[InlineData(@"C:\media\movies\movie.avi", false)]
[InlineData("/media/.hiddendir/file.mp4", false)]
[InlineData("/media/.hiddendir/file.mp4", true)]
[InlineData("/media/dir/.hiddenfile.mp4", true)]
[InlineData("/media/dir/._macjunk.mp4", true)]
[InlineData("/volume1/video/Series/@eaDir", true)]
@@ -32,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("/media/music/Foo B.A.R", false)]
[InlineData("/media/music/Foo B.A.R.", false)]
[InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)]
public void PathIgnored(string path, bool expected)
public void PathIgnored(string path, bool expected)
{
Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));
}