Compare commits

...

83 commits

Author SHA1 Message Date
Jellyfin Release Bot
cf78aefbb7 Bump version to 10.10.4 2025-01-21 21:20:10 -05:00
Bond-009
344cc8b97b
Merge pull request #13345 from gnattu/fix-matroska-as-webm-audio
Never treat matroska as webm for audio playback
2025-01-14 15:00:31 +01:00
gnattu
cc9c000412 Never treat matroska as webm for audio playback
This would break browsers like Firefox where the matroska file cannot be played as audio file.
2025-01-10 15:24:10 +08:00
gnattu
5c6317f68d
Use nv15 as intermediate format for 2-pass rkrga scaling (#13313) 2025-01-02 16:47:51 -07:00
gnattu
80940c0c57
Don't generate trickplay for backdrops (#13183) 2024-12-31 09:15:39 -07:00
gnattu
8aa41d5904
Transcode to audio codec satisfied other conditions when copy check failed. (#13209) 2024-12-31 09:15:05 -07:00
Tim Eisele
cea0c95942
Fix DTS in HLS (#13288) 2024-12-31 09:10:25 -07:00
Tim Eisele
4e28f4fe03
Fix missing episode removal (#13218) 2024-12-31 09:09:42 -07:00
Tim Eisele
f0e9b2fb96
Fix NFO ID parsing (#13167) 2024-12-31 09:06:45 -07:00
Tim Eisele
b9881b8bdf
Fix EPG image caching (#13227) 2024-12-31 09:04:22 -07:00
Bond-009
b31f1696f2
Merge pull request #13151 from nyanmisaka/sw-tonemap-by-default
Always do tone-mapping for HDR transcoding when software pipeline is used
2024-12-29 22:29:46 +01:00
Bond-009
86160cd99c
Merge pull request #13262 from gnattu/don't-use-x265-params-on-ultrafast
Don't use custom params on ultrafast x265 preset
2024-12-27 10:42:43 +01:00
Bond-009
230eacf15e
Merge pull request #13280 from gnattu/backport-atl-update
Backport ATL update 6.11 to 10.10
2024-12-27 10:41:45 +01:00
gnattu
0ecaa98ee7 Backport ATL update 6.11 to 10.10
This fixed long duration (> 1hr) LRC formatting
2024-12-24 18:24:36 +08:00
gnattu
45c4bedbc6 Always apply necessary params 2024-12-21 22:09:56 +08:00
gnattu
2c4c1d054d Don't use custom params on ultrafast x265 preset
Our custom parameters are slower than the ultrafast preset, but users would expect encoding to be as fast as possible when selecting ultrafast. Only apply those parameters to superfast and slower presets.
2024-12-21 21:54:03 +08:00
Bond-009
f97f38585b
Merge pull request #13182 from gnattu/no-multivalue-ffprobe-fallback
Don't fall back to ffprobe results for multi-value audio tags
2024-12-20 22:35:15 +01:00
Bond-009
a2a0cbf7ab
Merge pull request #13180 from gnattu/backport-atl-update
Backport ATL update to 10.10
2024-12-09 22:05:00 +01:00
Bond-009
eb5f8d49dd
Merge pull request #13187 from gnattu/properly-check-lan
Properly check LAN IP in HasRemoteAccess
2024-12-09 19:31:29 +01:00
Bond-009
6f7ce439d3
Merge pull request #13188 from Bond-009/nebml
Fix possible infinite loops in incomplete MKV files
2024-12-09 19:30:13 +01:00
Bond_009
03ea566271 Fix possible infinite loops in incomplete MKV files
https://github.com/OlegZee/NEbml/pull/14
Fixes #13122
2024-12-08 19:39:41 +01:00
gnattu
2a96b8b34b Properly check LAN IP in HasRemoteAccess
We cannot simply use the subnet list to check if the IP is in LAN as it does not handle special cases like IPv4MappedToIPv6 and IPv6 loopback addresses.
2024-12-08 22:06:11 +08:00
Bond-009
ff4f3b0441
Merge pull request #13169 from gnattu/fix-no-audio-transcoding
Check if the video has an audio track before codec fallback
2024-12-08 12:17:02 +01:00
gnattu
d49bb1d86d Don't fall back to ffprobe results for multi-value audio tags 2024-12-08 10:56:05 +08:00
renovate[bot]
cf6aa12627 Update dependency z440.atl.core to 6.9.0 2024-12-08 09:16:13 +08:00
gnattu
cd4519c15f Check if the video has an audio track before fallback
This would break transcoding for videos without an audio track as the codec checking would be null referencing.
2024-12-07 01:40:41 +08:00
nyanmisaka
8e248c7c05 Enable software tone-mapping by default
Transcoding HDR video without tonemapping results
in an unacceptable viewing experience. Many users
are not even aware of the option and therefore we
should always enable the software tonemapx filter.

Signed-off-by: nyanmisaka <nst7999610810@gmail.com>
2024-12-03 22:39:27 +08:00
gnattu
65f722f23c
Fallback to lossy audio codec for bitrate limit (#13127) 2024-12-01 17:08:28 -07:00
gnattu
e7ac3e3929
Fix missing ConfigureAwait (#13139)
Regression from #12940
2024-12-01 10:57:37 -07:00
Bond-009
9464f9e622
Merge pull request #13113 from gnattu/only-remux-dv-when-no-fallback
Only do DoVi remux when the client supports profiles without fallbacks
2024-11-30 12:14:55 +01:00
Joshua M. Boniface
746280af0b
Merge pull request #13106 from RealGreenDragon/patch-1
Enable RemoveOldPlugins by default (10.10.z backport)
2024-11-28 15:58:49 -05:00
gnattu
9bc6e8a306 Only do DoVi remux when the client supports profiles without fallbacks
In 10.10 clients that can only play the fallback layer like the Samsung TVs will report `DOVIWithHDR10` as supported video range, but the server should not do remux in DoVi as the client can only play the fallback layer. This changes the server to only do DoVi remux when the client can play DoVi videos without a fallback layer.
2024-11-26 15:01:59 +08:00
RealGreenDragon
b0105179eb
Enable RemoveOldPlugins by default
Backport of PR #13102 to 10.10.z branch.
2024-11-25 08:40:20 +01:00
Jellyfin Release Bot
b3e563385c Bump version to 10.10.3 2024-11-18 22:38:42 -05:00
Cody Robibero
5e45403cb1
Downgrade minimum sdk version (#13063) 2024-11-18 05:58:57 -07:00
Tim Eisele
23de7e517e
Exclude file system based library playlists from migration (#13059) 2024-11-17 20:18:53 -07:00
Jellyfin Release Bot
be23f4eb0d Bump version to 10.10.2 2024-11-16 14:59:25 -05:00
Joshua M. Boniface
38c08c4fad
Merge pull request #12916 from JPVenson/bugfix/10.10/MediaSegmentsRespectDisabledProviders
Added query filter to disregard disabled Providers
2024-11-16 14:25:56 -05:00
JPVenson
1b4ab5e777 pr review stuff 2024-11-16 18:39:11 +00:00
Akaanksh Raj
293e0f5faf
Respect cancellation token/HTTP request aborts correctly in SymlinkFollowingPhysicalFileResultExecutor (#13033) 2024-11-16 10:16:43 -07:00
Cody Robibero
13ae2266de
Merge pull request #13038 from Bond-009/stable-deps 2024-11-16 10:11:33 -07:00
renovate[bot]
6870e3496c Update dependency z440.atl.core to 6.8.0 2024-11-15 18:54:55 +01:00
renovate[bot]
ea88bdf2f3 Update dependency z440.atl.core to 6.7.0 (#12943)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-15 18:54:52 +01:00
Bond-009
a6f04ffb7c Merge pull request #13021 from jellyfin/renovate/microsoft
Update Microsoft to 8.0.11
2024-11-15 18:52:49 +01:00
Bond-009
db266d75d6 Merge pull request #12986 from jellyfin/renovate/skiasharp-monorepo
Update skiasharp monorepo
2024-11-15 18:49:45 +01:00
Bond-009
f47d2c1f1a Merge pull request #12792 from jellyfin/renovate/dotnet-monorepo
Update dotnet monorepo
2024-11-15 18:49:14 +01:00
Tim Eisele
8bee67f1f8
Fix playlists (#12934) 2024-11-14 17:03:31 -07:00
Nyanmisaka
cf11a2dc1e
Fix missing procamp vaapi filter (#13026) 2024-11-14 17:02:02 -07:00
gnattu
e2434d38c5
Only set first MusicBrainz ID for audio tags (#13003) 2024-11-14 17:01:48 -07:00
gnattu
9e61a6fd72
Always cleanup trickplay temp for ffmpeg failures (#13030) 2024-11-14 17:00:59 -07:00
gnattu
d292fde9e2
Use invariant culture for tonemap options (#12991) 2024-11-09 11:33:27 -07:00
Nyanmisaka
25321d7f80
Fix InvariantCulture in VPP tonemap options (#12989) 2024-11-09 11:31:59 -07:00
Joshua M. Boniface
9c6454ec46
Merge pull request #12955 from gnattu/fix-trickplay-regeneration
Fix trickplay images never being replaced
2024-11-09 10:19:32 -05:00
Joshua M. Boniface
09c377fb6c
Merge pull request #12964 from nyanmisaka/fix-imported-trickplay-height
Fix height of imported trickplay tiles
2024-11-09 10:13:58 -05:00
gnattu
97dc02b163
Always consider null char as delimiter for ID3v2 (#12962) 2024-11-06 06:38:00 -07:00
Nyanmisaka
aa08d3f2bf
Fix pixel format in HEVC RExt SDR transcoding (#12973) 2024-11-06 06:37:47 -07:00
nyanmisaka
2354cd45d4 Fix height of imported trickplay tiles
fixes c56dbc1

Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2024-11-05 10:17:15 +08:00
gnattu
c8ca0c72e1 Fix trickplay images never being replaced
The Refresh API controller did not pass the query parameter from the client to MetadataRefreshOptions and the old trickplay files never got replaced.
2024-11-05 00:07:29 +08:00
gnattu
3089e9e40a
Fix json array string writer in JsonDelimitedArrayConverter (#12949) 2024-11-04 08:04:04 -07:00
gnattu
954950dc14
Add a small tolerance value to remux fps check (#12947) 2024-11-04 07:59:23 -07:00
Jellyfin Release Bot
f6f4cdf9e7 Bump version to 10.10.1 2024-11-03 10:57:46 -05:00
Joshua M. Boniface
3a9b48a2aa
Merge pull request #12940 from gnattu/remove-local-temp-file
Remove DynamicImageResponse local image after saved to metadata folder
2024-11-03 10:54:20 -05:00
gnattu
5769d5ca91 Catch all exceptions for file removal 2024-11-03 23:25:11 +08:00
gnattu
03271c43a7 Throw the exception as is 2024-11-03 16:10:17 +08:00
gnattu
bb30d26ffb Use ExceptionDispatchInfo 2024-11-03 04:28:48 +08:00
gnattu
e9ee0ef1f5 Remove temp file even when saving failed 2024-11-03 04:11:41 +08:00
gnattu
3aefbf8cf6 Don't do double remove in BaseDynamicImageProvider 2024-11-03 03:02:35 +08:00
gnattu
469bf9d514 Move the remove source implementation into ProviderManager 2024-11-03 02:51:11 +08:00
Niels van Velzen
a165883999
Merge pull request #12931 from gnattu/set-audio-codec-when-transcoding
Set AudioCodec when building stream
2024-11-02 19:11:34 +01:00
gnattu
74d2c2addf Remove DynamicImageResponse local image after saved to metadata folder
Previously, local images provided by DynamicImageResponse were never cleaned up until the server was restarted. This issue has become more severe in 10.10, as the default is now set to use the system's native temp folder, which might be a RAM backed tmpfs. This behavior could lead to resource starvation for long-running servers performing multiple library scans.

Metadata plugins prefer the old behavior should do its own backup.
2024-11-02 17:15:00 +08:00
gnattu
096e1b2970 Add comments noting that comma separated codec list is not supported in pure audio transcoding for now 2024-11-01 07:09:16 +08:00
gnattu
b0f44f1d5a Set AudioCodec when building stream
This was not set at least since 10.9 and the transcoding behavior is close to "undefined" and in 10.10 this will not work at all. This will make the returned transcoding url from PlayBackInfo to correctly specify the desired transcoding codec. If the client wants to use the HLS controller directly it should be responsible to provide valid container and codec in the parameters.
2024-11-01 05:49:31 +08:00
JPVenson
584be05e93 reduced providerid build 2024-10-31 17:51:56 +00:00
JPVenson
3592c629e7
Fixed possible NullReferenceException in SessionManager (#12915) 2024-10-31 09:40:48 -06:00
Mikal S.
f99e0407fd
Don't try to prune images for virtual episodes. (#12909) 2024-10-31 09:40:03 -06:00
JPVenson
fe9c6fb8ae Fixed enumerable 2024-10-31 07:40:47 +00:00
JPVenson
54a6a33c01 renamed param 2024-10-30 10:31:10 +00:00
JPVenson
0130580151 Fixed interface definition 2024-10-30 10:25:57 +00:00
JPVenson
aa4dd04b99 Added fast fail for no provider selected segment query 2024-10-30 10:10:55 +00:00
JPVenson
c08d1d5b7f Added parameter to enable or disable library filter 2024-10-30 10:09:39 +00:00
JPVenson
312ff4f3d8 Fixed disabled providers not beeing returned 2024-10-30 10:05:52 +00:00
Benedikt
c6629aebf8
Fix TMDB import failing when no IMDB ID is set for a movie (#12891) 2024-10-28 07:29:15 -06:00
Jellyfin Release Bot
016a7e5542 Bump version to 10.10.0 2024-10-26 13:32:50 -04:00
49 changed files with 614 additions and 296 deletions

View file

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

View file

@ -192,6 +192,8 @@
- [jaina heartles](https://github.com/heartles)
- [oxixes](https://github.com/oxixes)
- [elfalem](https://github.com/elfalem)
- [benedikt257](https://github.com/benedikt257)
- [revam](https://github.com/revam)
# Emby Contributors

View file

@ -17,21 +17,21 @@
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.3" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="4.0.8" />
<PackageVersion Include="LrcParser" Version="2024.0728.2" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.11" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
@ -40,8 +40,8 @@
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.10" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.10" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
@ -51,7 +51,7 @@
<PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="0.11.0" />
<PackageVersion Include="NEbml" Version="0.12.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
@ -66,9 +66,9 @@
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
<PackageVersion Include="SkiaSharp" Version="2.88.8" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.8" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.8" />
<PackageVersion Include="SkiaSharp" Version="2.88.9" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.9" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="2.0.0.1" />
@ -80,7 +80,7 @@
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.1" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="6.6.0" />
<PackageVersion Include="z440.atl.core" Version="6.11.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
@ -88,4 +88,4 @@
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
<PackageVersion Include="xunit" Version="2.9.2" />
</ItemGroup>
</Project>
</Project>

View file

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

View file

@ -17,7 +17,6 @@ namespace Emby.Server.Implementations
{ DefaultRedirectKey, "web/" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString },
{ SqliteCacheSizeKey, "20000" },
{ FfmpegSkipValidationKey, bool.FalseString },

View file

@ -122,7 +122,6 @@ namespace Emby.Server.Implementations.Images
}
await ProviderManager.SaveImage(item, outputPath, mimeType, imageType, null, false, cancellationToken).ConfigureAwait(false);
File.Delete(outputPath);
return ItemUpdateType.ImageUpdate;
}

View file

@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (args.IsDirectory)
{
// It's a boxset if the path is a directory with [playlist] in its name
// It's a playlist if the path is a directory with [playlist] in its name
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
if (string.IsNullOrEmpty(filename))
{

View file

@ -216,14 +216,11 @@ namespace Emby.Server.Implementations.Playlists
var newItems = GetPlaylistItems(newItemIds, user, options)
.Where(i => i.SupportsAddingToPlaylist);
// Filter out duplicate items, if necessary
if (!_appConfig.DoPlaylistsAllowDuplicates())
{
var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet();
newItems = newItems
.Where(i => !existingIds.Contains(i.Id))
.Distinct();
}
// Filter out duplicate items
var existingIds = playlist.LinkedChildren.Select(c => c.ItemId).ToHashSet();
newItems = newItems
.Where(i => !existingIds.Contains(i.Id))
.Distinct();
// Create a list of the new linked children to add to the playlist
var childrenToAdd = newItems
@ -269,7 +266,7 @@ namespace Emby.Server.Implementations.Playlists
var idList = entryIds.ToList();
var removals = children.Where(i => idList.Contains(i.Item1.Id));
var removals = children.Where(i => idList.Contains(i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture)));
playlist.LinkedChildren = children.Except(removals)
.Select(i => i.Item1)
@ -286,26 +283,39 @@ namespace Emby.Server.Implementations.Playlists
RefreshPriority.High);
}
public async Task MoveItemAsync(string playlistId, string entryId, int newIndex)
public async Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId)
{
if (_libraryManager.GetItemById(playlistId) is not Playlist playlist)
{
throw new ArgumentException("No Playlist exists with the supplied Id");
}
var user = _userManager.GetUserById(callingUserId);
var children = playlist.GetManageableItems().ToList();
var accessibleChildren = children.Where(c => c.Item2.IsVisible(user)).ToArray();
var oldIndex = children.FindIndex(i => string.Equals(entryId, i.Item1.Id, StringComparison.OrdinalIgnoreCase));
var oldIndexAll = children.FindIndex(i => string.Equals(entryId, i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
var oldIndexAccessible = accessibleChildren.FindIndex(i => string.Equals(entryId, i.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
if (oldIndex == newIndex)
if (oldIndexAccessible == newIndex)
{
return;
}
var item = playlist.LinkedChildren[oldIndex];
var newPriorItemIndex = newIndex > oldIndexAccessible ? newIndex : newIndex - 1 < 0 ? 0 : newIndex - 1;
var newPriorItemId = accessibleChildren[newPriorItemIndex].Item1.ItemId;
var newPriorItemIndexOnAllChildren = children.FindIndex(c => c.Item1.ItemId.Equals(newPriorItemId));
var adjustedNewIndex = newPriorItemIndexOnAllChildren + 1;
var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase));
if (item is null)
{
_logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", item.ItemId, playlistId);
return;
}
var newList = playlist.LinkedChildren.ToList();
newList.Remove(item);
if (newIndex >= newList.Count)
@ -314,7 +324,7 @@ namespace Emby.Server.Implementations.Playlists
}
else
{
newList.Insert(newIndex, item);
newList.Insert(adjustedNewIndex, item);
}
playlist.LinkedChildren = [.. newList];

View file

@ -1938,7 +1938,11 @@ namespace Emby.Server.Implementations.Session
// Don't report acceleration type for non-admin users.
result = result.Select(r =>
{
r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
if (r.TranscodingInfo is not null)
{
r.TranscodingInfo.HardwareAccelerationType = HardwareAccelerationType.none;
}
return r;
});
}

View file

@ -1819,16 +1819,13 @@ public class DynamicHlsController : BaseJellyfinApiController
if (isActualOutputVideoCodecHevc || isActualOutputVideoCodecAv1)
{
var requestedRange = state.GetRequestedRangeTypes(state.ActualOutputVideoCodec);
var requestHasDOVI = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasDOVIWithHDR10 = requestedRange.Contains(VideoRangeType.DOVIWithHDR10.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasDOVIWithHLG = requestedRange.Contains(VideoRangeType.DOVIWithHLG.ToString(), StringComparison.OrdinalIgnoreCase);
var requestHasDOVIWithSDR = requestedRange.Contains(VideoRangeType.DOVIWithSDR.ToString(), StringComparison.OrdinalIgnoreCase);
// Clients reporting Dolby Vision capabilities with fallbacks may only support the fallback layer.
// Only enable Dolby Vision remuxing if the client explicitly declares support for profiles without fallbacks.
var clientSupportsDoVi = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
var videoIsDoVi = state.VideoStream.VideoRangeType is VideoRangeType.DOVI or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG or VideoRangeType.DOVIWithSDR;
if (EncodingHelper.IsCopyCodec(codec)
&& ((state.VideoStream.VideoRangeType == VideoRangeType.DOVI && requestHasDOVI)
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10 && requestHasDOVIWithHDR10)
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG && requestHasDOVIWithHLG)
|| (state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithSDR && requestHasDOVIWithSDR)))
&& (videoIsDoVi && clientSupportsDoVi))
{
if (isActualOutputVideoCodecHevc)
{

View file

@ -50,6 +50,7 @@ public class ItemRefreshController : BaseJellyfinApiController
/// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
/// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
/// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
/// <param name="regenerateTrickplay">(Optional) Determines if trickplay images should be replaced. Only applicable if mode is FullRefresh.</param>
/// <response code="204">Item metadata refresh queued.</response>
/// <response code="404">Item to refresh not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
@ -62,7 +63,8 @@ public class ItemRefreshController : BaseJellyfinApiController
[FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
[FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
[FromQuery] bool replaceAllMetadata = false,
[FromQuery] bool replaceAllImages = false)
[FromQuery] bool replaceAllImages = false,
[FromQuery] bool regenerateTrickplay = false)
{
var item = _libraryManager.GetItemById<BaseItem>(itemId, User.GetUserId());
if (item is null)
@ -81,7 +83,8 @@ public class ItemRefreshController : BaseJellyfinApiController
|| replaceAllImages
|| replaceAllMetadata,
IsAutomated = false,
RemoveOldMetadata = replaceAllMetadata
RemoveOldMetadata = replaceAllMetadata,
RegenerateTrickplay = regenerateTrickplay
};
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);

View file

@ -55,7 +55,7 @@ public class MediaSegmentsController : BaseJellyfinApiController
return NotFound();
}
var items = await _mediaSegmentManager.GetSegmentsAsync(item.Id, includeSegmentTypes).ConfigureAwait(false);
var items = await _mediaSegmentManager.GetSegmentsAsync(item, includeSegmentTypes).ConfigureAwait(false);
return Ok(new QueryResult<MediaSegmentDto>(items.ToArray()));
}
}

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@ -426,7 +427,7 @@ public class PlaylistsController : BaseJellyfinApiController
return Forbid();
}
await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex).ConfigureAwait(false);
await _playlistManager.MoveItemAsync(playlistId, itemId, newIndex, callingUserId).ConfigureAwait(false);
return NoContent();
}
@ -514,7 +515,8 @@ public class PlaylistsController : BaseJellyfinApiController
return Forbid();
}
var items = playlist.GetManageableItems().ToArray();
var user = _userManager.GetUserById(callingUserId);
var items = playlist.GetManageableItems().Where(i => i.Item2.IsVisible(user)).ToArray();
var count = items.Length;
if (startIndex.HasValue)
{
@ -529,11 +531,11 @@ public class PlaylistsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var user = _userManager.GetUserById(callingUserId);
var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user);
for (int index = 0; index < dtos.Count; index++)
{
dtos[index].PlaylistItemId = items[index].Item1.Id;
dtos[index].PlaylistItemId = items[index].Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture);
}
var result = new QueryResult<BaseItemDto>(

View file

@ -235,6 +235,11 @@ public static class StreamingHelpers
state.VideoRequest.MaxHeight = resolution.MaxHeight;
}
}
if (state.AudioStream is not null && !EncodingHelper.IsCopyCodec(state.OutputAudioCodec) && string.Equals(state.AudioStream.Codec, state.OutputAudioCodec, StringComparison.OrdinalIgnoreCase) && state.OutputAudioBitrate.HasValue)
{
state.OutputAudioCodec = state.SupportedAudioCodecs.Where(c => !EncodingHelper.LosslessAudioCodecs.Contains(c)).FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec);
}
}
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)

View file

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

View file

@ -139,23 +139,53 @@ public class MediaSegmentManager : IMediaSegmentManager
}
/// <inheritdoc />
public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter)
public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true)
{
var baseItem = _libraryManager.GetItemById(itemId);
if (baseItem is null)
{
_logger.LogError("Tried to request segments for an invalid item");
return [];
}
return await GetSegmentsAsync(baseItem, typeFilter, filterByProvider).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true)
{
using var db = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
var query = db.MediaSegments
.Where(e => e.ItemId.Equals(itemId));
.Where(e => e.ItemId.Equals(item.Id));
if (typeFilter is not null)
{
query = query.Where(e => typeFilter.Contains(e.Type));
}
if (filterByProvider)
{
var libraryOptions = _libraryManager.GetLibraryOptions(item);
var providerIds = _segmentProviders
.Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
.Select(f => GetProviderId(f.Name))
.ToArray();
if (providerIds.Length == 0)
{
return [];
}
query = query.Where(e => providerIds.Contains(e.SegmentProviderId));
}
return query
.OrderBy(e => e.StartTicks)
.AsNoTracking()
.ToImmutableList()
.Select(Map);
.AsEnumerable()
.Select(Map)
.ToArray();
}
private static MediaSegmentDto Map(MediaSegment segment)

View file

@ -194,6 +194,14 @@ public class TrickplayManager : ITrickplayManager
return;
}
// We support video backdrops, but we should not generate trickplay images for them
var parentDirectory = Directory.GetParent(mediaPath);
if (parentDirectory is not null && string.Equals(parentDirectory.Name, "backdrops", StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", mediaPath, video.Id);
return;
}
// The width has to be even, otherwise a lot of filters will not be able to sample it
var actualWidth = 2 * (width / 2);
@ -238,7 +246,7 @@ public class TrickplayManager : ITrickplayManager
foreach (var tile in existingFiles)
{
var image = _imageEncoder.GetImageSize(tile);
localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, image.Height);
localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, (int)Math.Ceiling((double)image.Height / localTrickplayInfo.TileHeight));
var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tile).Length * 8 / localTrickplayInfo.TileWidth / localTrickplayInfo.TileHeight / (localTrickplayInfo.Interval / 1000));
localTrickplayInfo.Bandwidth = Math.Max(localTrickplayInfo.Bandwidth, bitrate);
}

View file

@ -101,7 +101,7 @@ namespace Jellyfin.Server.Infrastructure
count: null);
}
private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count)
private async Task SendFileAsync(string filePath, HttpResponse response, long offset, long? count, CancellationToken cancellationToken = default)
{
var fileInfo = GetFileInfo(filePath);
if (offset < 0 || offset > fileInfo.Length)
@ -118,6 +118,9 @@ namespace Jellyfin.Server.Infrastructure
// Copied from SendFileFallback.SendFileAsync
const int BufferSize = 1024 * 16;
var useRequestAborted = !cancellationToken.CanBeCanceled;
var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken;
var fileStream = new FileStream(
filePath,
FileMode.Open,
@ -127,10 +130,17 @@ namespace Jellyfin.Server.Infrastructure
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
await using (fileStream.ConfigureAwait(false))
{
fileStream.Seek(offset, SeekOrigin.Begin);
await StreamCopyOperation
.CopyToAsync(fileStream, response.Body, count, BufferSize, CancellationToken.None)
.ConfigureAwait(true);
try
{
localCancel.ThrowIfCancellationRequested();
fileStream.Seek(offset, SeekOrigin.Begin);
await StreamCopyOperation
.CopyToAsync(fileStream, response.Body, count, BufferSize, localCancel)
.ConfigureAwait(true);
}
catch (OperationCanceledException) when (useRequestAborted)
{
}
}
}

View file

@ -47,7 +47,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.AddDefaultCastReceivers),
typeof(Routines.UpdateDefaultPluginRepository),
typeof(Routines.FixAudioData),
typeof(Routines.MoveTrickplayFiles)
typeof(Routines.MoveTrickplayFiles),
typeof(Routines.RemoveDuplicatePlaylistChildren)
};
/// <summary>

View file

@ -15,12 +15,12 @@ namespace Jellyfin.Server.Migrations.Routines;
/// </summary>
internal class FixPlaylistOwner : IMigrationRoutine
{
private readonly ILogger<RemoveDuplicateExtras> _logger;
private readonly ILogger<FixPlaylistOwner> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IPlaylistManager _playlistManager;
public FixPlaylistOwner(
ILogger<RemoveDuplicateExtras> logger,
ILogger<FixPlaylistOwner> logger,
ILibraryManager libraryManager,
IPlaylistManager playlistManager)
{

View file

@ -0,0 +1,69 @@
using System;
using System.Linq;
using System.Threading;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Remove duplicate playlist entries.
/// </summary>
internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine
{
private readonly ILogger<RemoveDuplicatePlaylistChildren> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IPlaylistManager _playlistManager;
public RemoveDuplicatePlaylistChildren(
ILogger<RemoveDuplicatePlaylistChildren> logger,
ILibraryManager libraryManager,
IPlaylistManager playlistManager)
{
_logger = logger;
_libraryManager = libraryManager;
_playlistManager = playlistManager;
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("{96C156A2-7A13-4B3B-A8B8-FB80C94D20C0}");
/// <inheritdoc/>
public string Name => "RemoveDuplicatePlaylistChildren";
/// <inheritdoc/>
public bool PerformOnNewInstall => false;
/// <inheritdoc/>
public void Perform()
{
var playlists = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Playlist]
})
.Cast<Playlist>()
.Where(p => !p.OpenAccess || !p.OwnerUserId.Equals(Guid.Empty))
.ToArray();
if (playlists.Length > 0)
{
foreach (var playlist in playlists)
{
var linkedChildren = playlist.LinkedChildren;
if (linkedChildren.Length > 0)
{
var nullItemChildren = linkedChildren.Where(c => c.ItemId is null);
var deduplicatedChildren = linkedChildren.DistinctBy(c => c.ItemId);
var newLinkedChildren = nullItemChildren.Concat(deduplicatedChildren);
playlist.LinkedChildren = linkedChildren;
playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
_playlistManager.SavePlaylistFile(playlist);
}
}
}
}
}

View file

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

View file

@ -4,7 +4,6 @@
using System;
using System.Globalization;
using System.Text.Json.Serialization;
namespace MediaBrowser.Controller.Entities
{
@ -12,7 +11,6 @@ namespace MediaBrowser.Controller.Entities
{
public LinkedChild()
{
Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
}
public string Path { get; set; }
@ -21,9 +19,6 @@ namespace MediaBrowser.Controller.Entities
public string LibraryItemId { get; set; }
[JsonIgnore]
public string Id { get; set; }
/// <summary>
/// Gets or sets the linked item id.
/// </summary>
@ -31,6 +26,8 @@ namespace MediaBrowser.Controller.Entities
public static LinkedChild Create(BaseItem item)
{
ArgumentNullException.ThrowIfNull(item);
var child = new LinkedChild
{
Path = item.Path,

View file

@ -49,11 +49,6 @@ namespace MediaBrowser.Controller.Extensions
/// </summary>
public const string FfmpegPathKey = "ffmpeg";
/// <summary>
/// The key for a setting that indicates whether playlists should allow duplicate entries.
/// </summary>
public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates";
/// <summary>
/// The key for a setting that indicates whether kestrel should bind to a unix socket.
/// </summary>
@ -120,14 +115,6 @@ namespace MediaBrowser.Controller.Extensions
public static bool GetFFmpegImgExtractPerfTradeoff(this IConfiguration configuration)
=> configuration.GetValue<bool>(FfmpegImgExtractPerfTradeoffKey);
/// <summary>
/// Gets a value indicating whether playlists should allow duplicate entries from the <see cref="IConfiguration"/>.
/// </summary>
/// <param name="configuration">The configuration to read the setting from.</param>
/// <returns>True if playlists should allow duplicates, otherwise false.</returns>
public static bool DoPlaylistsAllowDuplicates(this IConfiguration configuration)
=> configuration.GetValue<bool>(PlaylistsAllowDuplicatesKey);
/// <summary>
/// Gets a value indicating whether kestrel should bind to a unix socket from the <see cref="IConfiguration" />.
/// </summary>

View file

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

View file

@ -309,7 +309,6 @@ namespace MediaBrowser.Controller.MediaEncoding
private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
{
if (state.VideoStream is null
|| !options.EnableTonemapping
|| GetVideoColorBitDepth(state) < 10
|| !_mediaEncoder.SupportsFilter("tonemapx"))
{
@ -2061,7 +2060,13 @@ namespace MediaBrowser.Controller.MediaEncoding
// libx265 only accept level option in -x265-params.
// level option may cause libx265 to fail.
// libx265 cannot adjust the given level, just throw an error.
param += " -x265-params:0 subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1:no-scenecut=1:no-open-gop=1:no-info=1";
param += " -x265-params:0 no-scenecut=1:no-open-gop=1:no-info=1";
if (encodingOptions.EncoderPreset < EncoderPreset.ultrafast)
{
// The following params are slower than the ultrafast preset, don't use when ultrafast is selected.
param += ":subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1";
}
}
if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)
@ -2196,7 +2201,10 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var videoFrameRate = videoStream.ReferenceFrameRate;
if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value)
// Add a little tolerance to the framerate check because some videos might record a framerate
// that is slightly higher than the intended framerate, but the device can still play it correctly.
// 0.05 fps tolerance should be safe enough.
if (!videoFrameRate.HasValue || videoFrameRate.Value > requestedFramerate.Value + 0.05f)
{
return false;
}
@ -3318,24 +3326,25 @@ namespace MediaBrowser.Controller.MediaEncoding
&& options.VppTonemappingBrightness >= -100
&& options.VppTonemappingBrightness <= 100)
{
procampParams += $"=b={options.VppTonemappingBrightness}";
procampParams += "procamp_vaapi=b={0}";
doVaVppProcamp = true;
}
if (options.VppTonemappingContrast > 1
&& options.VppTonemappingContrast <= 10)
{
procampParams += doVaVppProcamp ? ":" : "=";
procampParams += $"c={options.VppTonemappingContrast}";
procampParams += doVaVppProcamp ? ":c={1}" : "procamp_vaapi=c={1}";
doVaVppProcamp = true;
}
args = "{0}tonemap_vaapi=format={1}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32";
args = procampParams + "{2}tonemap_vaapi=format={3}:p=bt709:t=bt709:m=bt709:extra_hw_frames=32";
return string.Format(
CultureInfo.InvariantCulture,
args,
doVaVppProcamp ? $"procamp_vaapi{procampParams}," : string.Empty,
options.VppTonemappingBrightness,
options.VppTonemappingContrast,
doVaVppProcamp ? "," : string.Empty,
videoFormat ?? "nv12");
}
else
@ -3523,20 +3532,29 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// tonemapx requires yuv420p10 input for dovi reshaping, let ffmpeg convert the frame when necessary
var tonemapFormat = requireDoviReshaping ? "yuv420p" : outFormat;
var tonemapArgs = $"tonemapx=tonemap={options.TonemappingAlgorithm}:desat={options.TonemappingDesat}:peak={options.TonemappingPeak}:t=bt709:m=bt709:p=bt709:format={tonemapFormat}";
var tonemapArgString = "tonemapx=tonemap={0}:desat={1}:peak={2}:t=bt709:m=bt709:p=bt709:format={3}";
if (options.TonemappingParam != 0)
{
tonemapArgs += $":param={options.TonemappingParam}";
tonemapArgString += ":param={4}";
}
var range = options.TonemappingRange;
if (range == TonemappingRange.tv || range == TonemappingRange.pc)
{
tonemapArgs += $":range={options.TonemappingRange}";
tonemapArgString += ":range={5}";
}
var tonemapArgs = string.Format(
CultureInfo.InvariantCulture,
tonemapArgString,
options.TonemappingAlgorithm,
options.TonemappingDesat,
options.TonemappingPeak,
tonemapFormat,
options.TonemappingParam,
options.TonemappingRange);
mainFilters.Add(tonemapArgs);
}
else
@ -4128,31 +4146,46 @@ namespace MediaBrowser.Controller.MediaEncoding
else if (isD3d11vaDecoder || isQsvDecoder)
{
var isRext = IsVideoStreamHevcRext(state);
var twoPassVppTonemap = isRext;
var twoPassVppTonemap = false;
var doVppFullRangeOut = isMjpegEncoder
&& _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppOutRangeOption;
var doVppScaleModeHq = isMjpegEncoder
&& _mediaEncoder.EncoderVersion >= _minFFmpegQsvVppScaleModeOption;
var doVppProcamp = false;
var procampParams = string.Empty;
var procampParamsString = string.Empty;
if (doVppTonemap)
{
if (isRext)
{
// VPP tonemap requires p010 input
twoPassVppTonemap = true;
}
if (options.VppTonemappingBrightness != 0
&& options.VppTonemappingBrightness >= -100
&& options.VppTonemappingBrightness <= 100)
{
procampParams += $":brightness={options.VppTonemappingBrightness}";
procampParamsString += ":brightness={0}";
twoPassVppTonemap = doVppProcamp = true;
}
if (options.VppTonemappingContrast > 1
&& options.VppTonemappingContrast <= 10)
{
procampParams += $":contrast={options.VppTonemappingContrast}";
procampParamsString += ":contrast={1}";
twoPassVppTonemap = doVppProcamp = true;
}
procampParams += doVppProcamp ? ":procamp=1:async_depth=2" : string.Empty;
if (doVppProcamp)
{
procampParamsString += ":procamp=1:async_depth=2";
procampParams = string.Format(
CultureInfo.InvariantCulture,
procampParamsString,
options.VppTonemappingBrightness,
options.VppTonemappingContrast);
}
}
var outFormat = doOclTonemap ? ((doVppTranspose || isRext) ? "p010" : string.Empty) : "nv12";
@ -5662,7 +5695,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(doScaling)
&& !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f))
{
var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={outFormat}:afbc=1";
// Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format.
// Use NV15 instead of P010 to avoid the issue.
// SDR inputs are using BGRA formats already which is not affected.
var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat;
var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_divisible_by=4:afbc=1";
mainFilters.Add(hwScaleFilterFirstPass);
}
@ -7036,7 +7073,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// DTS and TrueHD are not supported by HLS
// Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
shiftAudioCodecs.Add("dca");
shiftAudioCodecs.Add("dts");
shiftAudioCodecs.Add("truehd");
}
else

View file

@ -50,8 +50,18 @@ public interface IMediaSegmentManager
/// </summary>
/// <param name="itemId">The id of the <see cref="BaseItem"/>.</param>
/// <param name="typeFilter">filteres all media segments of the given type to be included. If null all types are included.</param>
/// <param name="filterByProvider">When set filteres the segments to only return those that which providers are currently enabled on their library.</param>
/// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns>
Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter);
Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(Guid itemId, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true);
/// <summary>
/// Obtains all segments accociated with the itemId.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param>
/// <param name="typeFilter">filteres all media segments of the given type to be included. If null all types are included.</param>
/// <param name="filterByProvider">When set filteres the segments to only return those that which providers are currently enabled on their library.</param>
/// <returns>An enumerator of <see cref="MediaSegmentDto"/>'s.</returns>
Task<IEnumerable<MediaSegmentDto>> GetSegmentsAsync(BaseItem item, IEnumerable<MediaSegmentType>? typeFilter, bool filterByProvider = true);
/// <summary>
/// Gets information about any media segments stored for the given itemId.

View file

@ -92,8 +92,9 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="playlistId">The playlist identifier.</param>
/// <param name="entryId">The entry identifier.</param>
/// <param name="newIndex">The new index.</param>
/// <param name="callingUserId">The calling user.</param>
/// <returns>Task.</returns>
Task MoveItemAsync(string playlistId, string entryId, int newIndex);
Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId);
/// <summary>
/// Removed all playlists of a user.

View file

@ -77,7 +77,8 @@ namespace MediaBrowser.Controller.Providers
Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken);
/// <summary>
/// Saves the image.
/// Saves the image by giving the image path on filesystem.
/// This method will remove the image on the source path after saving it to the destination.
/// </summary>
/// <param name="item">Image to save.</param>
/// <param name="source">Source of image.</param>

View file

@ -1035,6 +1035,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (exitCode == -1)
{
_logger.LogError("ffmpeg image extraction failed for {ProcessDescription}", processDescription);
// Cleanup temp folder here, because the targetDirectory is not returned and the cleanup for failed ffmpeg process is not possible for caller.
// Ideally the ffmpeg should not write any files if it fails, but it seems like it is not guaranteed.
try
{
Directory.Delete(targetDirectory, true);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to delete ffmpeg temp directory {TargetDirectory}", targetDirectory);
}
throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "ffmpeg image extraction failed for {0}", processDescription));
}

View file

@ -246,7 +246,7 @@ public class ServerConfiguration : BaseApplicationConfiguration
/// <summary>
/// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
/// </summary>
public bool RemoveOldPlugins { get; set; }
public bool RemoveOldPlugins { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether clients should be allowed to upload logs.

View file

@ -30,7 +30,7 @@ namespace MediaBrowser.Model.Dlna
private readonly ITranscoderSupport _transcoderSupport;
private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"];
private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"];
private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd"];
private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dts", "truehd"];
/// <summary>
/// Initializes a new instance of the <see cref="StreamBuilder"/> class.
@ -208,6 +208,14 @@ namespace MediaBrowser.Model.Dlna
var longBitrate = Math.Min(transcodingBitrate, playlistItem.AudioBitrate ?? transcodingBitrate);
playlistItem.AudioBitrate = longBitrate > int.MaxValue ? int.MaxValue : Convert.ToInt32(longBitrate);
// Pure audio transcoding does not support comma separated list of transcoding codec at the moment.
// So just use the AudioCodec as is would be safe enough as the _transcoderSupport.CanEncodeToAudioCodec
// would fail so this profile will not even be picked up.
if (playlistItem.AudioCodecs.Count == 0 && !string.IsNullOrWhiteSpace(transcodingProfile.AudioCodec))
{
playlistItem.AudioCodecs = [transcodingProfile.AudioCodec];
}
}
playlistItem.TranscodeReasons = transcodeReasons;
@ -854,18 +862,37 @@ namespace MediaBrowser.Model.Dlna
if (options.AllowAudioStreamCopy)
{
if (ContainerHelper.ContainsContainer(transcodingProfile.AudioCodec, audioCodec))
// For Audio stream, we prefer the audio codec that can be directly copied, then the codec that can otherwise satisfies
// the transcoding conditions, then the one does not satisfy the transcoding conditions.
// For example: A client can support both aac and flac, but flac only supports 2 channels while aac supports 6.
// When the source audio is 6 channel flac, we should transcode to 6 channel aac, instead of down-mix to 2 channel flac.
var transcodingAudioCodecs = ContainerHelper.Split(transcodingProfile.AudioCodec);
foreach (var transcodingAudioCodec in transcodingAudioCodecs)
{
var appliedVideoConditions = options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.VideoAudio &&
i.ContainsAnyCodec(audioCodec, container) &&
i.ContainsAnyCodec(transcodingAudioCodec, container) &&
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)))
.Select(i =>
i.Conditions.All(condition => ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)));
// An empty appliedVideoConditions means that the codec has no conditions for the current audio stream
var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
rank.Audio = conditionsSatisfied ? 1 : 2;
var rankAudio = 3;
if (conditionsSatisfied)
{
rankAudio = string.Equals(transcodingAudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ? 1 : 2;
}
rank.Audio = Math.Min(rank.Audio, rankAudio);
if (rank.Audio == 1)
{
break;
}
}
}
@ -955,9 +982,26 @@ namespace MediaBrowser.Model.Dlna
var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(audioCodecs, false, stream.Codec)).FirstOrDefault();
var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null;
var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && audioStreamWithSupportedCodec.Channels > (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue);
var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && directAudioStream is null;
var directAudioStreamSatisfied = audioStreamWithSupportedCodec is not null && !channelsExceedsLimit
&& options.Profile.CodecProfiles
.Where(i => i.Type == CodecType.VideoAudio
&& i.ContainsAnyCodec(audioStreamWithSupportedCodec.Codec, container)
&& i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioStreamWithSupportedCodec.Channels, audioStreamWithSupportedCodec.BitRate, audioStreamWithSupportedCodec.SampleRate, audioStreamWithSupportedCodec.BitDepth, audioStreamWithSupportedCodec.Profile, false)))
.Select(i => i.Conditions.All(condition =>
{
var satisfied = ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioStreamWithSupportedCodec.Channels, audioStreamWithSupportedCodec.BitRate, audioStreamWithSupportedCodec.SampleRate, audioStreamWithSupportedCodec.BitDepth, audioStreamWithSupportedCodec.Profile, false);
if (!satisfied)
{
playlistItem.TranscodeReasons |= GetTranscodeReasonForFailedCondition(condition);
}
return satisfied;
}))
.All(satisfied => satisfied);
var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : null;
if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
{
@ -2205,7 +2249,7 @@ namespace MediaBrowser.Model.Dlna
}
}
private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
private static bool IsAudioContainerSupported(DirectPlayProfile profile, MediaSourceInfo item)
{
// Check container type
if (!profile.SupportsContainer(item.Container))
@ -2213,6 +2257,20 @@ namespace MediaBrowser.Model.Dlna
return false;
}
// Never direct play audio in matroska when the device only declare support for webm.
// The first check is not enough because mkv is assumed can be webm.
// See https://github.com/jellyfin/jellyfin/issues/13344
return !ContainerHelper.ContainsContainer("mkv", item.Container)
|| profile.SupportsContainer("mkv");
}
private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
{
if (!IsAudioContainerSupported(profile, item))
{
return false;
}
// Check audio codec
string? audioCodec = audioStream?.Codec;
if (!profile.SupportsAudioCodec(audioCodec))
@ -2227,19 +2285,16 @@ namespace MediaBrowser.Model.Dlna
{
// Check container type, this should NOT be supported
// If the container is supported, the file should be directly played
if (!profile.SupportsContainer(item.Container))
if (IsAudioContainerSupported(profile, item))
{
// Check audio codec, we cannot use the SupportsAudioCodec here
// Because that one assumes empty container supports all codec, which is just useless
string? audioCodec = audioStream?.Codec;
if (string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ||
string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
return false;
// Check audio codec, we cannot use the SupportsAudioCodec here
// Because that one assumes empty container supports all codec, which is just useless
string? audioCodec = audioStream?.Codec;
return string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase)
|| string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase);
}
private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings)

View file

@ -1,3 +1,4 @@
using System;
using System.ComponentModel;
using System.Xml.Serialization;
using Jellyfin.Data.Enums;
@ -6,6 +7,7 @@ namespace MediaBrowser.Model.Dlna;
/// <summary>
/// A class for transcoding profile information.
/// Note for client developers: Conditions defined in <see cref="CodecProfile"/> has higher priority and can override values defined here.
/// </summary>
public class TranscodingProfile
{
@ -17,6 +19,33 @@ public class TranscodingProfile
Conditions = [];
}
/// <summary>
/// Initializes a new instance of the <see cref="TranscodingProfile" /> class copying the values from another instance.
/// </summary>
/// <param name="other">Another instance of <see cref="TranscodingProfile" /> to be copied.</param>
public TranscodingProfile(TranscodingProfile other)
{
ArgumentNullException.ThrowIfNull(other);
Container = other.Container;
Type = other.Type;
VideoCodec = other.VideoCodec;
AudioCodec = other.AudioCodec;
Protocol = other.Protocol;
EstimateContentLength = other.EstimateContentLength;
EnableMpegtsM2TsMode = other.EnableMpegtsM2TsMode;
TranscodeSeekInfo = other.TranscodeSeekInfo;
CopyTimestamps = other.CopyTimestamps;
Context = other.Context;
EnableSubtitlesInManifest = other.EnableSubtitlesInManifest;
MaxAudioChannels = other.MaxAudioChannels;
MinSegments = other.MinSegments;
SegmentLength = other.SegmentLength;
BreakOnNonKeyFrames = other.BreakOnNonKeyFrames;
Conditions = other.Conditions;
EnableAudioVbrEncoding = other.EnableAudioVbrEncoding;
}
/// <summary>
/// Gets or sets the container.
/// </summary>

View file

@ -18,7 +18,7 @@ public static class LibraryOptionsExtension
{
ArgumentNullException.ThrowIfNull(options);
return options.CustomTagDelimiters.Select<string, char?>(x =>
var delimiterList = options.CustomTagDelimiters.Select<string, char?>(x =>
{
var isChar = char.TryParse(x, out var c);
if (isChar)
@ -27,6 +27,8 @@ public static class LibraryOptionsExtension
}
return null;
}).Where(x => x is not null).Select(x => x!.Value).ToArray();
}).Where(x => x is not null).Select(x => x!.Value).ToList();
delimiterList.Add('\0');
return delimiterList.ToArray();
}
}

View file

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

View file

@ -229,9 +229,7 @@ namespace MediaBrowser.Providers.Manager
{
var mimeType = MimeTypes.GetMimeType(response.Path);
var stream = AsyncFile.OpenRead(response.Path);
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
await _providerManager.SaveImage(item, response.Path, mimeType, imageType, null, null, cancellationToken).ConfigureAwait(false);
}
}
@ -387,8 +385,8 @@ namespace MediaBrowser.Providers.Manager
item.RemoveImages(images);
// Cleanup old metadata directory for episodes if empty
if (item is Episode)
// Cleanup old metadata directory for episodes if empty, as long as it's not a virtual item
if (item is Episode && !item.IsVirtualItem)
{
var oldLocalMetadataDirectory = Path.Combine(item.ContainingFolderPath, "metadata");
if (_fileSystem.DirectoryExists(oldLocalMetadataDirectory) && !_fileSystem.GetFiles(oldLocalMetadataDirectory).Any())

View file

@ -7,6 +7,7 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
@ -251,15 +252,29 @@ namespace MediaBrowser.Providers.Manager
}
/// <inheritdoc/>
public Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken)
public async Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(source))
{
throw new ArgumentNullException(nameof(source));
}
var fileStream = AsyncFile.OpenRead(source);
return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
try
{
var fileStream = AsyncFile.OpenRead(source);
await new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken).ConfigureAwait(false);
}
finally
{
try
{
File.Delete(source);
}
catch (Exception ex)
{
_logger.LogError(ex, "Source file {Source} not found or in use, skip removing", source);
}
}
}
/// <inheritdoc/>

View file

@ -179,7 +179,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
{
var people = new List<PersonInfo>();
var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split(InternalValueSeparator);
var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? [] : track.AlbumArtist.Split(InternalValueSeparator);
if (libraryOptions.UseCustomTagDelimiters)
{
@ -210,7 +210,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (performers is null || performers.Length == 0)
{
performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator);
performers = string.IsNullOrEmpty(track.Artist) ? [] : track.Artist.Split(InternalValueSeparator);
}
if (libraryOptions.UseCustomTagDelimiters)
@ -314,7 +314,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (!audio.LockedFields.Contains(MetadataField.Genres))
{
var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
var genres = string.IsNullOrEmpty(track.Genre) ? [] : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
if (libraryOptions.UseCustomTagDelimiters)
{
@ -347,7 +347,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag))
&& !string.IsNullOrEmpty(musicBrainzArtistTag))
{
audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzArtistTag);
var id = GetFirstMusicBrainzId(musicBrainzArtistTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, id);
}
}
@ -357,7 +358,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag))
&& !string.IsNullOrEmpty(musicBrainzReleaseArtistIdTag))
{
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, musicBrainzReleaseArtistIdTag);
var id = GetFirstMusicBrainzId(musicBrainzReleaseArtistIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, id);
}
}
@ -367,7 +369,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag))
&& !string.IsNullOrEmpty(musicBrainzReleaseIdTag))
{
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, musicBrainzReleaseIdTag);
var id = GetFirstMusicBrainzId(musicBrainzReleaseIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, id);
}
}
@ -377,7 +380,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag))
&& !string.IsNullOrEmpty(musicBrainzReleaseGroupIdTag))
{
audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, musicBrainzReleaseGroupIdTag);
var id = GetFirstMusicBrainzId(musicBrainzReleaseGroupIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, id);
}
}
@ -387,7 +391,8 @@ namespace MediaBrowser.Providers.MediaInfo
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId))
&& !string.IsNullOrEmpty(trackMbId))
{
audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
var id = GetFirstMusicBrainzId(trackMbId, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, id);
}
}
@ -441,5 +446,18 @@ namespace MediaBrowser.Providers.MediaInfo
return items;
}
// MusicBrainz IDs are multi-value tags, so we need to split them
// However, our current provider can only have one single ID, which means we need to pick the first one
private string? GetFirstMusicBrainzId(string tag, bool useCustomTagDelimiters, char[] tagDelimiters, string[] whitelist)
{
var val = tag.Split(InternalValueSeparator).FirstOrDefault();
if (val is not null && useCustomTagDelimiters)
{
val = SplitWithCustomDelimiter(val, tagDelimiters, whitelist).FirstOrDefault();
}
return val;
}
}
}

View file

@ -198,7 +198,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
};
movie.SetProviderId(MetadataProvider.Tmdb, tmdbId);
movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
movie.TrySetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
if (movieResult.BelongsToCollection is not null)
{
movie.SetProviderId(MetadataProvider.TmdbCollection, movieResult.BelongsToCollection.Id.ToString(CultureInfo.InvariantCulture));

View file

@ -140,38 +140,39 @@ namespace MediaBrowser.Providers.TV
private void RemoveObsoleteEpisodes(Series series)
{
var episodes = series.GetEpisodes(null, new DtoOptions(), true).OfType<Episode>().ToList();
var numberOfEpisodes = episodes.Count;
// TODO: O(n^2), but can it be done faster without overcomplicating it?
for (var i = 0; i < numberOfEpisodes; i++)
var episodesBySeason = series.GetEpisodes(null, new DtoOptions(), true)
.OfType<Episode>()
.GroupBy(e => e.ParentIndexNumber)
.ToList();
foreach (var seasonEpisodes in episodesBySeason)
{
var currentEpisode = episodes[i];
// The outer loop only examines virtual episodes
if (!currentEpisode.IsVirtualItem)
List<Episode> nonPhysicalEpisodes = [];
List<Episode> physicalEpisodes = [];
foreach (var episode in seasonEpisodes)
{
continue;
if (episode.IsVirtualItem || episode.IsMissingEpisode)
{
nonPhysicalEpisodes.Add(episode);
continue;
}
physicalEpisodes.Add(episode);
}
// Virtual episodes without an episode number are practically orphaned and should be deleted
if (!currentEpisode.IndexNumber.HasValue)
// Only consider non-physical episodes
foreach (var episode in nonPhysicalEpisodes)
{
DeleteEpisode(currentEpisode);
continue;
}
// Episodes without an episode number are practically orphaned and should be deleted
// Episodes with a physical equivalent should be deleted (they are no longer missing)
var shouldKeep = episode.IndexNumber.HasValue && !physicalEpisodes.Any(e => e.ContainsEpisodeNumber(episode.IndexNumber.Value));
for (var j = i + 1; j < numberOfEpisodes; j++)
{
var comparisonEpisode = episodes[j];
// The inner loop is only for "physical" episodes
if (comparisonEpisode.IsVirtualItem
|| currentEpisode.ParentIndexNumber != comparisonEpisode.ParentIndexNumber
|| !comparisonEpisode.ContainsEpisodeNumber(currentEpisode.IndexNumber.Value))
if (shouldKeep)
{
continue;
}
DeleteEpisode(currentEpisode);
break;
DeleteEpisode(episode);
}
}
}

View file

@ -50,23 +50,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
case "id":
{
// get ids from attributes
// Get ids from attributes
item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
item.TrySetProviderId(MetadataProvider.Tvdb, reader.GetAttribute("TVDB"));
string? imdbId = reader.GetAttribute("IMDB");
string? tmdbId = reader.GetAttribute("TMDB");
// read id from content
// Read id from content
// Content can be arbitrary according to Kodi wiki, so only parse if we are sure it matches a provider-specific schema
var contentId = reader.ReadElementContentAsString();
if (contentId.Contains("tt", StringComparison.Ordinal) && string.IsNullOrEmpty(imdbId))
if (string.IsNullOrEmpty(imdbId) && contentId.StartsWith("tt", StringComparison.Ordinal))
{
imdbId = contentId;
}
else if (string.IsNullOrEmpty(tmdbId))
{
tmdbId = contentId;
}
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
break;
}

View file

@ -1,3 +1,4 @@
using System;
using System.Globalization;
using System.Xml;
using Emby.Naming.TV;
@ -48,16 +49,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
case "id":
{
item.TrySetProviderId(MetadataProvider.Imdb, reader.GetAttribute("IMDB"));
// Get ids from attributes
item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
item.TrySetProviderId(MetadataProvider.Tvdb, reader.GetAttribute("TVDB"));
string? imdbId = reader.GetAttribute("IMDB");
string? tvdbId = reader.GetAttribute("TVDB");
if (string.IsNullOrWhiteSpace(tvdbId))
// Read id from content
// Content can be arbitrary according to Kodi wiki, so only parse if we are sure it matches a provider-specific schema
var contentId = reader.ReadElementContentAsString();
if (string.IsNullOrEmpty(imdbId) && contentId.StartsWith("tt", StringComparison.Ordinal))
{
tvdbId = reader.ReadElementContentAsString();
imdbId = contentId;
}
item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
break;
}

View file

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

View file

@ -15,7 +15,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Extensions</PackageId>
<VersionPrefix>10.10.0</VersionPrefix>
<VersionPrefix>10.10.4</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View file

@ -71,24 +71,11 @@ namespace Jellyfin.Extensions.Json.Converters
writer.WriteStartArray();
if (value.Length > 0)
{
var toWrite = value.Length - 1;
foreach (var it in value)
{
var wrote = false;
if (it is not null)
{
writer.WriteStringValue(it.ToString());
wrote = true;
}
if (toWrite > 0)
{
if (wrote)
{
writer.WriteStringValue(Delimiter.ToString());
}
toWrite--;
}
}
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities.Libraries;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using Jellyfin.LiveTv.Configuration;
@ -39,6 +40,11 @@ public class GuideManager : IGuideManager
private readonly IRecordingsManager _recordingsManager;
private readonly LiveTvDtoService _tvDtoService;
/// <summary>
/// Amount of days images are pre-cached from external sources.
/// </summary>
public const int MaxCacheDays = 2;
/// <summary>
/// Initializes a new instance of the <see cref="GuideManager"/> class.
/// </summary>
@ -204,14 +210,14 @@ public class GuideManager : IGuideManager
progress.Report(15);
numComplete = 0;
var programs = new List<Guid>();
var programs = new List<LiveTvProgram>();
var channels = new List<Guid>();
var guideDays = GetGuideDays();
_logger.LogInformation("Refreshing guide with {0} days of guide data", guideDays);
_logger.LogInformation("Refreshing guide with {Days} days of guide data", guideDays);
var maxCacheDate = DateTime.UtcNow.AddDays(2);
var maxCacheDate = DateTime.UtcNow.AddDays(MaxCacheDays);
foreach (var currentChannel in list)
{
cancellationToken.ThrowIfCancellationRequested();
@ -237,22 +243,23 @@ public class GuideManager : IGuideManager
DtoOptions = new DtoOptions(true)
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
var newPrograms = new List<LiveTvProgram>();
var updatedPrograms = new List<BaseItem>();
var newPrograms = new List<Guid>();
var updatedPrograms = new List<Guid>();
foreach (var program in channelPrograms)
{
var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
var id = programItem.Id;
if (isNew)
{
newPrograms.Add(programItem);
newPrograms.Add(id);
}
else if (isUpdated)
{
updatedPrograms.Add(programItem);
updatedPrograms.Add(id);
}
programs.Add(programItem.Id);
programs.Add(programItem);
isMovie |= program.IsMovie;
isSeries |= program.IsSeries;
@ -261,24 +268,30 @@ public class GuideManager : IGuideManager
isKids |= program.IsKids;
}
_logger.LogDebug("Channel {0} has {1} new programs and {2} updated programs", currentChannel.Name, newPrograms.Count, updatedPrograms.Count);
_logger.LogDebug(
"Channel {Name} has {NewCount} new programs and {UpdatedCount} updated programs",
currentChannel.Name,
newPrograms.Count,
updatedPrograms.Count);
if (newPrograms.Count > 0)
{
_libraryManager.CreateItems(newPrograms, null, cancellationToken);
await PrecacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
var newProgramDtos = programs.Where(b => newPrograms.Contains(b.Id)).ToList();
_libraryManager.CreateItems(newProgramDtos, null, cancellationToken);
}
if (updatedPrograms.Count > 0)
{
var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList();
await _libraryManager.UpdateItemsAsync(
updatedPrograms,
updatedProgramDtos,
currentChannel,
ItemUpdateType.MetadataImport,
cancellationToken).ConfigureAwait(false);
await PrecacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
}
await PreCacheImages(programs, maxCacheDate).ConfigureAwait(false);
currentChannel.IsMovie = isMovie;
currentChannel.IsNews = isNews;
currentChannel.IsSports = isSports;
@ -313,7 +326,8 @@ public class GuideManager : IGuideManager
}
progress.Report(100);
return new Tuple<List<Guid>, List<Guid>>(channels, programs);
var programIds = programs.Select(p => p.Id).ToList();
return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
}
private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
@ -618,77 +632,17 @@ public class GuideManager : IGuideManager
item.IndexNumber = info.EpisodeNumber;
item.ParentIndexNumber = info.SeasonNumber;
if (!item.HasImage(ImageType.Primary))
{
if (!string.IsNullOrWhiteSpace(info.ImagePath))
{
item.SetImage(
new ItemImageInfo
{
Path = info.ImagePath,
Type = ImageType.Primary
},
0);
}
else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
{
item.SetImage(
new ItemImageInfo
{
Path = info.ImageUrl,
Type = ImageType.Primary
},
0);
}
}
forceUpdate = forceUpdate || UpdateImages(item, info);
if (!item.HasImage(ImageType.Thumb))
if (isNew)
{
if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
{
item.SetImage(
new ItemImageInfo
{
Path = info.ThumbImageUrl,
Type = ImageType.Thumb
},
0);
}
}
item.OnMetadataChanged();
if (!item.HasImage(ImageType.Logo))
{
if (!string.IsNullOrWhiteSpace(info.LogoImageUrl))
{
item.SetImage(
new ItemImageInfo
{
Path = info.LogoImageUrl,
Type = ImageType.Logo
},
0);
}
}
if (!item.HasImage(ImageType.Backdrop))
{
if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
{
item.SetImage(
new ItemImageInfo
{
Path = info.BackdropImageUrl,
Type = ImageType.Backdrop
},
0);
}
return (item, isNew, false);
}
var isUpdated = false;
if (isNew)
{
}
else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
{
isUpdated = true;
}
@ -703,7 +657,7 @@ public class GuideManager : IGuideManager
}
}
if (isNew || isUpdated)
if (isUpdated)
{
item.OnMetadataChanged();
}
@ -711,7 +665,80 @@ public class GuideManager : IGuideManager
return (item, isNew, isUpdated);
}
private async Task PrecacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
private static bool UpdateImages(BaseItem item, ProgramInfo info)
{
var updated = false;
// Primary
updated |= UpdateImage(ImageType.Primary, item, info);
// Thumbnail
updated |= UpdateImage(ImageType.Thumb, item, info);
// Logo
updated |= UpdateImage(ImageType.Logo, item, info);
// Backdrop
return updated || UpdateImage(ImageType.Backdrop, item, info);
}
private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info)
{
var image = item.GetImages(imageType).FirstOrDefault();
var currentImagePath = image?.Path;
var newImagePath = imageType switch
{
ImageType.Primary => info.ImagePath,
_ => string.Empty
};
var newImageUrl = imageType switch
{
ImageType.Backdrop => info.BackdropImageUrl,
ImageType.Logo => info.LogoImageUrl,
ImageType.Primary => info.ImageUrl,
ImageType.Thumb => info.ThumbImageUrl,
_ => string.Empty
};
var differentImage = newImageUrl?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false
|| newImagePath?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false;
if (!differentImage)
{
return false;
}
if (!string.IsNullOrWhiteSpace(newImagePath))
{
item.SetImage(
new ItemImageInfo
{
Path = newImagePath,
Type = imageType
},
0);
return true;
}
if (!string.IsNullOrWhiteSpace(newImageUrl))
{
item.SetImage(
new ItemImageInfo
{
Path = newImageUrl,
Type = imageType
},
0);
return true;
}
item.RemoveImage(image);
return false;
}
private async Task PreCacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
{
await Parallel.ForEachAsync(
programs
@ -741,7 +768,7 @@ public class GuideManager : IGuideManager
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Unable to precache {Url}", imageInfo.Path);
_logger.LogWarning(ex, "Unable to pre-cache {Url}", imageInfo.Path);
}
}
}

View file

@ -19,6 +19,7 @@ using System.Threading.Tasks;
using AsyncKeyedLock;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.LiveTv.Guide;
using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
@ -38,7 +39,7 @@ namespace Jellyfin.LiveTv.Listings
private readonly IHttpClientFactory _httpClientFactory;
private readonly AsyncNonKeyedLocker _tokenLock = new(1);
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private DateTime _lastErrorResponse;
private bool _disposed = false;
@ -86,7 +87,7 @@ namespace Jellyfin.LiveTv.Listings
{
_logger.LogWarning("SchedulesDirect token is empty, returning empty program list");
return Enumerable.Empty<ProgramInfo>();
return [];
}
var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
@ -94,7 +95,7 @@ namespace Jellyfin.LiveTv.Listings
_logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
var requestList = new List<RequestScheduleForChannelDto>()
{
new RequestScheduleForChannelDto()
new()
{
StationId = channelId,
Date = dates
@ -109,7 +110,7 @@ namespace Jellyfin.LiveTv.Listings
var dailySchedules = await Request<IReadOnlyList<DayDto>>(options, true, info, cancellationToken).ConfigureAwait(false);
if (dailySchedules is null)
{
return Array.Empty<ProgramInfo>();
return [];
}
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
@ -120,17 +121,17 @@ namespace Jellyfin.LiveTv.Listings
var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken)
.ConfigureAwait(false);
var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
if (programDetails is null)
{
return Array.Empty<ProgramInfo>();
return [];
}
var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
var programIdsWithImages = programDetails
.Where(p => p.HasImageArtwork).Select(p => p.ProgramId)
.Where(p => p.HasImageArtwork)
.Select(p => p.ProgramId)
.ToList();
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
@ -138,17 +139,15 @@ namespace Jellyfin.LiveTv.Listings
var programsInfo = new List<ProgramInfo>();
foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs))
{
// _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
// " which corresponds to channel " + channelNumber + " and program id " +
// schedule.ProgramId + " which says it has images? " +
// programDict[schedule.ProgramId].hasImageArtwork);
if (string.IsNullOrEmpty(schedule.ProgramId))
{
continue;
}
if (images is not null)
// Only add images which will be pre-cached until we can implement dynamic token fetching
var endDate = schedule.AirDateTime?.AddSeconds(schedule.Duration);
var willBeCached = endDate.HasValue && endDate.Value < DateTime.UtcNow.AddDays(GuideManager.MaxCacheDays);
if (willBeCached && images is not null)
{
var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
if (imageIndex > -1)
@ -456,7 +455,7 @@ namespace Jellyfin.LiveTv.Listings
if (programIds.Count == 0)
{
return Array.Empty<ShowImagesDto>();
return [];
}
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
@ -483,7 +482,7 @@ namespace Jellyfin.LiveTv.Listings
{
_logger.LogError(ex, "Error getting image info from schedules direct");
return Array.Empty<ShowImagesDto>();
return [];
}
}

View file

@ -702,7 +702,7 @@ public class NetworkManager : INetworkManager, IDisposable
return false;
}
}
else if (!_lanSubnets.Any(x => x.Contains(remoteIP)))
else if (!IsInLocalNetwork(remoteIP))
{
// Remote not enabled. So everyone should be LAN.
return false;

View file

@ -292,6 +292,9 @@ namespace Jellyfin.Providers.Tests.Manager
providerManager.Setup(pm => pm.SaveImage(item, It.IsAny<Stream>(), It.IsAny<string>(), imageType, null, It.IsAny<CancellationToken>()))
.Callback<BaseItem, Stream, string, ImageType, int?, CancellationToken>((callbackItem, _, _, callbackType, _, _) => callbackItem.SetImagePath(callbackType, 0, new FileSystemMetadata()))
.Returns(Task.CompletedTask);
providerManager.Setup(pm => pm.SaveImage(item, It.IsAny<string>(), It.IsAny<string>(), imageType, null, null, It.IsAny<CancellationToken>()))
.Callback<BaseItem, string, string, ImageType, int?, bool?, CancellationToken>((callbackItem, _, _, callbackType, _, _, _) => callbackItem.SetImagePath(callbackType, 0, new FileSystemMetadata()))
.Returns(Task.CompletedTask);
var itemImageProvider = GetItemImageProvider(providerManager.Object, null);
var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { dynamicProvider.Object }, refreshOptions, CancellationToken.None);