Compare commits

...

107 Commits

Author SHA1 Message Date
Jellyfin Release Bot
aefb9b2cff Bump version to 10.10.7 2025-04-05 15:14:56 -04:00
Joshua M. Boniface
abc51e89a3 Merge pull request #13845 from Shadowghost/upgrade-atl
Update dependency z440.atl.core to 6.20.0
2025-04-05 15:14:07 -04:00
renovate[bot]
c929229030 Update dependency z440.atl.core to 6.20.0 (#13811)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-05 21:05:17 +02:00
Joshua M. Boniface
f625665cb1 Merge commit from fork
Fix not trusting all sources for forward headers if none are configured
2025-04-05 14:44:42 -04:00
Joshua M. Boniface
79f3ce5325 Merge commit from fork
Fix validation of API parameters passed to FFmpeg
2025-04-05 14:44:19 -04:00
Tim Eisele
6938fac73e Update MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
Co-authored-by: Cody Robibero <cody@robibe.ro>
2025-04-03 20:42:52 +02:00
Shadowghost
cafa0a1e10 Add validation for level input 2025-04-03 20:18:04 +02:00
Shadowghost
24242a510d Rename ValidationRegex to ContainerValidationRegex 2025-04-03 20:17:57 +02:00
Shadowghost
2d95245405 Fix container parameter validation 2025-04-03 20:17:49 +02:00
JPVenson
784f6c5eff Fix trusting all sources for forward headers if none are configured 2025-04-03 19:24:46 +02:00
Joshua M. Boniface
3ee7194706 Merge pull request #13835 from nielsvanvelzen/10-10-api-backports
Backport 10.11 API enum changes
2025-04-03 13:14:23 -04:00
Niels van Velzen
a4ab5e5a14 Explicitly set default value for MediaStream.VideoRangeType enum 2025-04-03 18:18:06 +02:00
gnattu
1fff472569 Explicitly set default value for enums used in API models (#13821)
Enums in response model with no nullability or default value will make the API very fragile as each extension to the enum will break the API for some clients, but a lot of enums actually do have an unknown value which should be used as a default. This set all model properties that are non-nullable using an enum that has an Unknown member in 10.10, except MediaStream.VideoRangeType which is refactored in #13277

(cherry picked from commit 4a4fef830e)
2025-04-03 18:14:42 +02:00
Bond-009
91ca81eca7 Merge pull request #13720 from Lampan-git/fix_people_multiple
Fix regression where "Search for missing metadata" not handling cast having multiple roles
2025-03-20 10:48:44 +01:00
Lampan-git
78d9fa72e0 fix Missing metadata search where cast has multiple roles 2025-03-15 11:11:30 +01:00
Bond-009
9eb2044eae Merge pull request #13694 from gnattu/clone-fallback-tags
Clone fallback audio tags instead of use ATL.Track.set
2025-03-12 13:32:36 +01:00
gnattu
7ea09a8637 Clone fallback audio tags instead of use ATL.Track.set
The setter of the Track class is not intended for such use cases and will have unwanted side effects to change valid values. We should never use them use all fields from ATL.Track class read-only.
2025-03-11 05:02:39 +08:00
Bond-009
069b518ab0 Merge pull request #12907 from JPVenson/bugfix/10.10/SourceCountstackoverflow
Fix stackoverflow in MediaSourceCount
2025-03-10 14:34:50 +01:00
Bond-009
ae6a7acf14 Merge pull request #13659 from congerh/upgrade-lrcparser
Upgrade LrcParser to 2025.228.1
2025-03-10 10:55:40 +01:00
congerh
64e0f9099a Upgrade LrcParser to 2025.228.1 2025-03-05 18:51:45 +08:00
Bond-009
4f94d23011 Merge pull request #13639 from IDisposable/more-ratings
Support more rating formats
2025-03-02 17:22:12 +01:00
Bond-009
fcdef875a2 Merge pull request #13618 from Lampan-git/fix_people_meta_refresh
Include Role and SortOrder in MergePeople to fix "Search for missing metadata"
2025-03-01 14:55:27 +01:00
Marc Brooks
2da4a2d753 Fix overwrite of PremierDate with a year-only value (#13598)
* Preserve the more-specific PremierDate's month and day

Fixes #13596
2025-03-01 14:53:12 +01:00
Marc Brooks
9ae68057a7 Support more rating formats
Add support for `"Rated:"` and also trim blanks.
Updated unit tests.
2025-02-28 12:38:25 -06:00
Bond-009
767a5e6193 Merge pull request #13601 from Bond-009/deleteChild
Delete children from cache on parent delete
2025-02-25 21:28:04 +01:00
Bond-009
346f36bc09 Merge pull request #13593 from Bond-009/stupidwindows
Wait for ffmpeg to exit on Windows before we try deleting the concat file
2025-02-25 21:27:54 +01:00
Bond-009
1daf761aec Merge pull request #13611 from Shadowghost/remove-empty-parent-index-workaround
Remove empty ParentIndexNumber workaround
2025-02-25 21:25:34 +01:00
Lampan-git
a70a09fdb3 save person Role and SortOrder in metadata refresh 2025-02-25 20:10:19 +01:00
Shadowghost
888838adbc Fix providers 2025-02-24 22:10:59 +01:00
Shadowghost
72911501d3 Remove empty ParentIndexNumber workaround 2025-02-24 19:12:20 +01:00
Bond_009
1af7b6d348 Delete children from cache on parent delete
This seems like a simple and safe (small) win.
Automatically invalidating cache entries after a while would be even better
(or not having a cache at all) but such changes are too big for a point release IMO.
2025-02-21 17:24:19 +01:00
theguymadmax
317d7a9f4f Collapse Box Sets when filtering 4K (#13594) 2025-02-20 21:10:41 -07:00
Bond_009
9d9c6fe5eb Wait for ffmpeg to exit on Windows before we try deleting the concat file
Fixes #12145 without slowing down on other platform or for single files that don't need the concat file.
2025-02-20 11:52:58 +01:00
Jellyfin Release Bot
a5b771861f Bump version to 10.10.6 2025-02-16 16:55:41 -05:00
gnattu
3f539472f3 Fix image encoding concurrency limit (#13532)
* Fix image encoding concurrency limit

The current FFmpeg image extractor is configured to use a resource pool size that always equals 2 times the number of CPU cores, which is somewhat excessive. Make the default equal to the core count instead of twice, and respect the `ParallelImageEncodingLimit` option.

* Fix code stype

* Check null value for unit tests
2025-02-12 18:45:36 -07:00
Cody Robibero
7f43521b64 Merge pull request #13489 from gnattu/fix-subnet-detect
Fix subnet contains check
2025-02-12 07:55:53 -07:00
gnattu
99006c370f Fix more typo 2025-02-10 05:20:34 +08:00
Cody Robibero
e3f9f0a7f3 Update dependency z440.atl.core to 6.16.0 (#13526) 2025-02-09 18:41:41 +01:00
elfalem
d1fbdcee34 Skip allowed tags check for parents of an item (#12721) 2025-02-09 09:49:02 -07:00
Tim Eisele
21e398ba0c Fix SchedulesDirect image prefetching (#13469)
* Only prefetch images for new and updated programs

* Update src/Jellyfin.LiveTv/Guide/GuideManager.cs
2025-02-09 09:45:03 -07:00
Marc Brooks
8544e7fc72 Fix LiveTV Guide Backdrop image not updating (#13504)
If the guide data changes an any other image, the Backdrop would not be updated.

Fixes #13496
2025-02-09 09:44:47 -07:00
Joshua M. Boniface
117d2082aa Merge pull request #13490 from gnattu/fix-audio-date-number-fallback
Correctly handle audio number tag fallbacks
2025-02-09 00:15:13 -05:00
Joshua M. Boniface
03082e90f9 Merge pull request #13499 from crobibero/api-key-websocket
Allow api key to subscribe to admin websockets
2025-02-09 00:14:46 -05:00
Cody Robibero
88026518b1 Allow api key to subscribe to admin websockets 2025-02-04 17:33:56 -07:00
Bond-009
5f1fb26382 Fix rating mistake in us.csv (#13487) 2025-02-04 17:10:31 -07:00
gnattu
070d04c1b2 Typo
Co-authored-by: Cody Robibero <cody@robibe.ro>
2025-02-05 08:04:43 +08:00
Cody Robibero
8aa4e2e320 Merge pull request #13439 from Bond-009/fallbackmimetype
Fall back to calculating mime type from path when needed
2025-02-04 17:00:55 -07:00
JPVenson
49bb5a6442 Merge pull request #13459 from JPVenson/bugfix/13457_FixWebSocketControllerConcurrency
Fixed Websocket not locking state correctly
2025-02-04 14:59:06 +01:00
Bond_009
9e869b4541 Simplify logic in ProviderManager.SaveImage 2025-02-04 14:56:52 +01:00
gnattu
710e877762 Correctly handle audio number tag fallbacks
Although the number type is nullable from the type definition of ATL, the lib might still normalize all unknown values to 0 which makes doing null check only not enough. Fallback to ffprobe results when the number is 0 as well.
2025-02-04 07:21:18 +08:00
gnattu
f536e08e14 Fix subnet contains check
We are still using `Subnet.Contains` a lot but that does not handle IPv4 mapped to IPv6 addresses at all. It was partially fixed by #12094 in local network checking, but it may not always happen on LAN.

Also make all local network checking to use IsInLocalNetwork method instead of just performing `Subnet.Contains` which is not accurate.

Filter out all link-local addresses for external interface matching.
2025-02-04 07:07:21 +08:00
Bond_009
4eecfee29f Fall back to calculating mime type from path when needed
Should fix #12593
2025-02-03 17:45:48 +01:00
Bond-009
731874429c Merge pull request #13448 from Shadowghost/fix-interface-selection-again
Fix interface ordering again
2025-02-03 17:43:11 +01:00
Tim Eisele
e6c6441abf Take subnet size into account 2025-01-27 10:59:50 +01:00
Tim Eisele
4d89a095ed Fix interface ordering again 2025-01-27 10:55:30 +01:00
Jellyfin Release Bot
1136a36eed Bump version to 10.10.5 2025-01-25 14:14:26 -05:00
gnattu
e8514de33b Don't select audio stream and codec explicitly for copy when bitrate exceeds limit (#13423) 2025-01-25 11:23:51 -07:00
JPVenson
722cdcce5e Add check to prevent downgrade from future EFCore refactor (#13103) 2025-01-25 10:11:19 -07:00
Bond-009
bfe0fdbcdc Open files with FileShare.Read for BlurHash calculations (#13425) 2025-01-25 08:41:51 -07:00
Joshua M. Boniface
0b2a59e963 Merge pull request #13384 from alltilla/fix-parallel-subtitleeditparse
Fix parallel use of not thread-safe SubtitleFormat instance
2025-01-25 02:32:00 -05:00
Joshua M. Boniface
6329de4fc3 Merge pull request #13411 from gnattu/use-writethrough-imagesaver
Use WriteThrough for ImageSaver
2025-01-25 02:29:39 -05:00
gnattu
644df3585b Use WriteThrough for ImageSaver
When writing an image to the disk, we use the completion of the async task as a signal indicating the completion of a write operation. However, this approach may not be entirely accurate, as the operating system can optimize IO operations by writing data to an intermediate cache instead of directly to the disk before completing the operation. This optimization can lead to a data race for our scanner, as subsequent tasks such as blurhash computation may attempt to read a file that has not yet been flushed from the volatile cache. Consequently, the data within the file becomes invalid, causing the blurhash computation task to fail.

Use WriteThrough mode to ensure the data is actual on disk before return to resolve this issue.
2025-01-24 07:54:22 +08:00
Joshua M. Boniface
3766a88bea Merge pull request #13390 from gnattu/catch-ioexception
Catch IOExceptions for GetFileSystemMetadata
2025-01-22 16:36:26 -05:00
Joshua M. Boniface
f333ef74b3 Merge pull request #13092 from TheMelmacian/bugfix/xml_special_characters_in_set_elements
Fix: handling of <set> elements in NfoParser
2025-01-22 16:36:04 -05:00
Joshua M. Boniface
0394965753 Merge pull request #13382 from Shadowghost/fix-published-url-override
Fix interface selection
2025-01-22 16:34:11 -05:00
Joshua M. Boniface
53a45c6033 Merge pull request #13388 from Shadowghost/fix-ratings
Fix rating levels
2025-01-22 16:34:02 -05:00
Shadowghost
adfe52f55a Fix rating levels 2025-01-22 21:32:43 +01:00
Jellyfin Release Bot
cf78aefbb7 Bump version to 10.10.4 2025-01-21 21:20:10 -05:00
Attila Szakacs
c693da94ce Fix parallel use of not thread-safe SubtitleFormat instance
`SubtitleFormat`'s `LoadSubtitle()` function is
not thread-safe.

A `SubtitleEditParser` instance's `Parse()`
function can be called from multiple threads at
the same time.

`SubtitleFormat`s are cached in the constructor
of each `SubtitleEditParser`, and the same
instances are used for each possibly parallel
`Parse()` function call, which causes subtitle
parse problems.

This patch modifies the code, so we only cache
the extension -> `SubtitleFormat` type/class
mapping and create a new `SubtitleFormat`
instance in each `Parse()` call, so no
`SubtitleFormat` instance is accessed from
multiple threads.

Fixes #12113

Kudos for everyone investigating the issue there,
most notably @RenV123 for PoC-ing the solution.

Signed-off-by: Attila Szakacs <szakacs.attila96@gmail.com>
2025-01-18 21:16:35 +01:00
gnattu
1a7c2299c6 Catch IOExceptions for GetFileSystemMetadata
Our `GetFileSystemEntries` method will throw when enumerating the file system, but its callers might consider the unhandled exceptions as the whole path is not available. This would cause a single problematic file to fail the enumeration, and could lead to unexpected side effects.

HandleIOException gracefully by marking the files throwing as not exist to let the caller skip that file.
2025-01-19 00:40:13 +08:00
Shadowghost
9c7d735a96 Fix interface selection 2025-01-17 09:41:32 +01: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
TheMelmacian
5df03b9010 write Kodi conform set element to nfo files 2024-12-01 21:20:51 +01: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
TheMelmacian
ef13a18450 fix(MovieNfoParser): parsing of <set> elements 2024-11-23 11:18:27 +01:00
JPVenson
ba28e3041d Changed stack type 2024-10-29 14:28:02 +00:00
JPVenson
34f3ed0a4d Fixed Stackoverflow on MediaSourceCount 2024-10-29 14:10:58 +00:00
72 changed files with 830 additions and 485 deletions

View File

@@ -9,8 +9,8 @@
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.3" />
<PackageVersion Include="BlurHashSharp" Version="1.3.3" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
<PackageVersion Include="BlurHashSharp" Version="1.3.4" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Diacritics" Version="3.3.29" />
@@ -22,7 +22,7 @@
<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="LrcParser" Version="2025.228.1" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.11" />
@@ -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" />
@@ -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.8.0" />
<PackageVersion Include="z440.atl.core" Version="6.20.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.3</VersionPrefix>
<VersionPrefix>10.10.7</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -276,6 +276,13 @@ namespace Emby.Server.Implementations.IO
{
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
}
catch (IOException ex)
{
// IOException generally means the file is not accessible due to filesystem issues
// Catch this exception and mark the file as not exist to ignore it
_logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName);
result.Exists = false;
}
}
}
@@ -590,6 +597,9 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
{
// Note: any of unhandled exceptions thrown by this method may cause the caller to believe the whole path is not accessible.
// But what causing the exception may be a single file under that path. This could lead to unexpected behavior.
// For example, the scanner will remove everything in that path due to unhandled errors.
var directoryInfo = new DirectoryInfo(path);
var enumerationOptions = GetEnumerationOptions(recursive);

View File

@@ -454,6 +454,7 @@ namespace Emby.Server.Implementations.Library
foreach (var child in children)
{
_itemRepository.DeleteItem(child.Id);
_cache.TryRemove(child.Id, out _);
}
_cache.TryRemove(item.Id, out _);
@@ -2626,15 +2627,6 @@ namespace Emby.Server.Implementations.Library
{
episode.ParentIndexNumber = season.IndexNumber;
}
else
{
/*
Anime series don't generally have a season in their file name, however,
TVDb needs a season to correctly get the metadata.
Hence, a null season needs to be filled with something. */
// FIXME perhaps this would be better for TVDb parser to ask for season 1 if no season is specified
episode.ParentIndexNumber = 1;
}
if (episode.ParentIndexNumber.HasValue)
{

View File

@@ -286,8 +286,10 @@ namespace Emby.Server.Implementations.Localization
}
// Fairly common for some users to have "Rated R" in their rating field
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase);
rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase);
rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase)
.Replace("Rated:", string.Empty, StringComparison.OrdinalIgnoreCase)
.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase)
.Trim();
// Use rating system matching the language
if (!string.IsNullOrEmpty(countryCode))

View File

@@ -1,8 +1,14 @@
Livre,0
L,0
ER,9
AL,0
ER,10
10,10
A10,10
12,12
A12,12
14,14
A14,14
16,16
A16,16
18,18
A18,18
1 Livre 0
2 L 0
3 ER AL 9 0
4 ER 10
5 10 10
6 A10 10
7 12 12
8 A12 12
9 14 14
10 A14 14
11 16 16
12 A16 16
13 18 18
14 A18 18

View File

@@ -6,8 +6,6 @@ TV-Y7,7
TV-Y7-FV,7
PG,9
TV-PG,9
PG-13,13
13+,13
TV-14,14
14A,14
16+,16
1 E 0
6 TV-Y7-FV 7
7 PG 9
8 TV-PG 9
PG-13 13
13+ 13
9 TV-14 14
10 14A 14
11 16+ 16

View File

@@ -1,7 +1,7 @@
A,0
A/fig,0
A/i,0
A/fig/i,0
A/i/fig,0
APTA,0
ERI,0
TP,0
1 A 0
2 A/fig 0
3 A/i 0
4 A/fig/i A/i/fig 0
5 APTA 0
6 ERI 0
7 TP 0

View File

@@ -6,10 +6,11 @@ U,0
6+,6
7+,7
PG,8
9+,9
9,9
12,12
12+,12
12A,12
12PG,12
Teen,13
13+,13
14+,14
1 All 0
6 6+ 6
7 7+ 7
8 PG 8
9 9+ 9 9
10 12 12
11 12+ 12
12 12A 12
13 12PG 12
14 Teen 13
15 13+ 13
16 14+ 14

View File

@@ -4,6 +4,7 @@ PG,12
12A,12
12PG,12
15,15
15PG,15
15A,15
16,16
18,18
1 G 4
4 12A 12
5 12PG 12
6 15 15
7 15PG 15
8 15A 15
9 16 16
10 18 18

View File

@@ -6,4 +6,5 @@ A,0
12,12
15,15
18,18
C,18
Not approved,1001
1 A 0
6 12 12
7 15 15
8 18 18
9 C 18
10 Not approved 1001

View File

@@ -10,6 +10,7 @@ R16,16
RP16,16
GA,18
R18,18
RP18,18
MA,1000
R,1001
Objectionable,1001
1 Exempt 0
10 RP16 16
11 GA 18
12 R18 18
13 RP18 18
14 MA 1000
15 R 1001
16 Objectionable 1001

View File

@@ -5,23 +5,23 @@ TV-Y,0
TV-Y7,7
TV-Y7-FV,7
PG,10
TV-PG,10
TV-PG-D,10
TV-PG-L,10
TV-PG-S,10
TV-PG-V,10
TV-PG-DL,10
TV-PG-DS,10
TV-PG-DV,10
TV-PG-LS,10
TV-PG-LV,10
TV-PG-SV,10
TV-PG-DLS,10
TV-PG-DLV,10
TV-PG-DSV,10
TV-PG-LSV,10
TV-PG-DLSV,10
PG-13,13
TV-PG,13
TV-PG-D,13
TV-PG-L,13
TV-PG-S,13
TV-PG-V,13
TV-PG-DL,13
TV-PG-DS,13
TV-PG-DV,13
TV-PG-LS,13
TV-PG-LV,13
TV-PG-SV,13
TV-PG-DLS,13
TV-PG-DLV,13
TV-PG-DSV,13
TV-PG-LSV,13
TV-PG-DLSV,13
TV-14,14
TV-14-D,14
TV-14-L,14
@@ -48,3 +48,5 @@ TV-MA-LS,17
TV-MA-LV,17
TV-MA-SV,17
TV-MA-LSV,17
TV-X,18
TV-AO,18
1 Approved 0
5 TV-Y7 7
6 TV-Y7-FV 7
7 PG 10
8 TV-PG 10
9 TV-PG-D 10
10 TV-PG-L 10
11 TV-PG-S 10
12 TV-PG-V 10
13 TV-PG-DL 10
14 TV-PG-DS 10
15 TV-PG-DV 10
16 TV-PG-LS 10
17 TV-PG-LV 10
18 TV-PG-SV 10
19 TV-PG-DLS 10
20 TV-PG-DLV 10
21 TV-PG-DSV 10
22 TV-PG-LSV 10
23 TV-PG-DLSV 10
24 PG-13 13
TV-PG 13
TV-PG-D 13
TV-PG-L 13
TV-PG-S 13
TV-PG-V 13
TV-PG-DL 13
TV-PG-DS 13
TV-PG-DV 13
TV-PG-LS 13
TV-PG-LV 13
TV-PG-SV 13
TV-PG-DLS 13
TV-PG-DLV 13
TV-PG-DSV 13
TV-PG-LSV 13
TV-PG-DLSV 13
25 TV-14 14
26 TV-14-D 14
27 TV-14-L 14
48 TV-MA-LV 17
49 TV-MA-SV 17
50 TV-MA-LSV 17
51 TV-X 18
52 TV-AO 18

View File

@@ -116,6 +116,7 @@ public partial class AudioNormalizationTask : IScheduledTask
{
a.LUFS = await CalculateLUFSAsync(
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
cancellationToken).ConfigureAwait(false);
}
finally
@@ -142,7 +143,10 @@ public partial class AudioNormalizationTask : IScheduledTask
continue;
}
t.LUFS = await CalculateLUFSAsync(string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), cancellationToken).ConfigureAwait(false);
t.LUFS = await CalculateLUFSAsync(
string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
false,
cancellationToken).ConfigureAwait(false);
}
_itemRepository.SaveItems(tracks, cancellationToken);
@@ -162,7 +166,7 @@ public partial class AudioNormalizationTask : IScheduledTask
];
}
private async Task<float?> CalculateLUFSAsync(string inputArgs, CancellationToken cancellationToken)
private async Task<float?> CalculateLUFSAsync(string inputArgs, bool waitForExit, CancellationToken cancellationToken)
{
var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";
@@ -189,18 +193,28 @@ public partial class AudioNormalizationTask : IScheduledTask
}
using var reader = process.StandardError;
float? lufs = null;
await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))
{
Match match = LUFSRegex().Match(line);
if (match.Success)
{
return float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
break;
}
}
_logger.LogError("Failed to find LUFS value in output");
return null;
if (lufs is null)
{
_logger.LogError("Failed to find LUFS value in output");
}
if (waitForExit)
{
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
}
return lufs;
}
}
}

View File

@@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.Session
private readonly SessionInfo _session;
private readonly List<IWebSocketConnection> _sockets;
private readonly ReaderWriterLockSlim _socketsLock;
private bool _disposed = false;
public WebSocketController(
@@ -31,10 +32,26 @@ namespace Emby.Server.Implementations.Session
_logger = logger;
_session = session;
_sessionManager = sessionManager;
_sockets = new List<IWebSocketConnection>();
_sockets = new();
_socketsLock = new();
}
private bool HasOpenSockets => GetActiveSockets().Any();
private bool HasOpenSockets
{
get
{
ObjectDisposedException.ThrowIf(_disposed, this);
try
{
_socketsLock.EnterReadLock();
return _sockets.Any(i => i.State == WebSocketState.Open);
}
finally
{
_socketsLock.ExitReadLock();
}
}
}
/// <inheritdoc />
public bool SupportsMediaControl => HasOpenSockets;
@@ -42,23 +59,38 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public bool IsSessionActive => HasOpenSockets;
private IEnumerable<IWebSocketConnection> GetActiveSockets()
=> _sockets.Where(i => i.State == WebSocketState.Open);
public void AddWebSocket(IWebSocketConnection connection)
{
_logger.LogDebug("Adding websocket to session {Session}", _session.Id);
_sockets.Add(connection);
connection.Closed += OnConnectionClosed;
ObjectDisposedException.ThrowIf(_disposed, this);
try
{
_socketsLock.EnterWriteLock();
_sockets.Add(connection);
connection.Closed += OnConnectionClosed;
}
finally
{
_socketsLock.ExitWriteLock();
}
}
private async void OnConnectionClosed(object? sender, EventArgs e)
{
var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
_sockets.Remove(connection);
connection.Closed -= OnConnectionClosed;
ObjectDisposedException.ThrowIf(_disposed, this);
try
{
_socketsLock.EnterWriteLock();
_sockets.Remove(connection);
connection.Closed -= OnConnectionClosed;
}
finally
{
_socketsLock.ExitWriteLock();
}
await _sessionManager.CloseIfNeededAsync(_session).ConfigureAwait(false);
}
@@ -69,7 +101,17 @@ namespace Emby.Server.Implementations.Session
T data,
CancellationToken cancellationToken)
{
var socket = GetActiveSockets().MaxBy(i => i.LastActivityDate);
ObjectDisposedException.ThrowIf(_disposed, this);
IWebSocketConnection? socket;
try
{
_socketsLock.EnterReadLock();
socket = _sockets.Where(i => i.State == WebSocketState.Open).MaxBy(i => i.LastActivityDate);
}
finally
{
_socketsLock.ExitReadLock();
}
if (socket is null)
{
@@ -94,12 +136,23 @@ namespace Emby.Server.Implementations.Session
return;
}
foreach (var socket in _sockets)
try
{
socket.Closed -= OnConnectionClosed;
socket.Dispose();
_socketsLock.EnterWriteLock();
foreach (var socket in _sockets)
{
socket.Closed -= OnConnectionClosed;
socket.Dispose();
}
_sockets.Clear();
}
finally
{
_socketsLock.ExitWriteLock();
}
_socketsLock.Dispose();
_disposed = true;
}
@@ -110,12 +163,23 @@ namespace Emby.Server.Implementations.Session
return;
}
foreach (var socket in _sockets)
try
{
socket.Closed -= OnConnectionClosed;
await socket.DisposeAsync().ConfigureAwait(false);
_socketsLock.EnterWriteLock();
foreach (var socket in _sockets)
{
socket.Closed -= OnConnectionClosed;
await socket.DisposeAsync().ConfigureAwait(false);
}
_sockets.Clear();
}
finally
{
_socketsLock.ExitWriteLock();
}
_socketsLock.Dispose();
_disposed = true;
}
}

View File

@@ -92,18 +92,18 @@ public class AudioController : BaseJellyfinApiController
[ProducesAudioFile]
public async Task<ActionResult> GetAudioStream(
[FromRoute, Required] Guid itemId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -114,7 +114,7 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -133,8 +133,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -259,18 +259,18 @@ public class AudioController : BaseJellyfinApiController
[ProducesAudioFile]
public async Task<ActionResult> GetAudioStreamByContainer(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string container,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -281,7 +281,7 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -300,8 +300,8 @@ public class AudioController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,

View File

@@ -166,18 +166,18 @@ public class DynamicHlsController : BaseJellyfinApiController
[ProducesPlaylistFile]
public async Task<ActionResult> GetLiveHlsStream(
[FromRoute, Required] Guid itemId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -188,7 +188,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -207,8 +207,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -415,12 +415,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -431,7 +431,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -452,8 +452,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -591,12 +591,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery, Required] string mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -608,7 +608,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -627,8 +627,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -761,12 +761,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -777,7 +777,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -798,8 +798,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -933,12 +933,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -950,7 +950,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -969,8 +969,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -1106,7 +1106,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
[FromRoute, Required] string container,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromQuery, Required] long runtimeTicks,
[FromQuery, Required] long actualSegmentLengthTicks,
[FromQuery] bool? @static,
@@ -1114,12 +1114,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -1130,7 +1130,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -1151,8 +1151,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -1291,7 +1291,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] int segmentId,
[FromRoute, Required] string container,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromQuery, Required] long runtimeTicks,
[FromQuery, Required] long actualSegmentLengthTicks,
[FromQuery] bool? @static,
@@ -1299,12 +1299,12 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -1316,7 +1316,7 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -1335,8 +1335,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -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

@@ -1186,7 +1186,9 @@ public class LiveTvController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesVideoFile]
public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container)
public ActionResult GetLiveStreamFile(
[FromRoute, Required] string streamId,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container)
{
var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId);
if (liveStreamInfo is null)

View File

@@ -102,13 +102,13 @@ public class UniversalAudioController : BaseJellyfinApiController
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] Guid? userId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] int? maxAudioChannels,
[FromQuery] int? transcodingAudioChannels,
[FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] long? startTimeTicks,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? transcodingContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? transcodingContainer,
[FromQuery] MediaStreamProtocol? transcodingProtocol,
[FromQuery] int? maxAudioSampleRate,
[FromQuery] int? maxAudioBitDepth,

View File

@@ -315,18 +315,18 @@ public class VideosController : BaseJellyfinApiController
[ProducesVideoFile]
public async Task<ActionResult> GetVideoStream(
[FromRoute, Required] Guid itemId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery, ParameterObsolete] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -337,7 +337,7 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -358,8 +358,8 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,
@@ -556,18 +556,18 @@ public class VideosController : BaseJellyfinApiController
[ProducesVideoFile]
public Task<ActionResult> GetVideoStreamByContainer(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string container,
[FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container,
[FromQuery] bool? @static,
[FromQuery] string? @params,
[FromQuery] string? tag,
[FromQuery] string? deviceProfileId,
[FromQuery] string? playSessionId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer,
[FromQuery] int? segmentLength,
[FromQuery] int? minSegments,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
[FromQuery] bool? enableAutoStreamCopy,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
@@ -578,7 +578,7 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
[FromQuery] string? profile,
[FromQuery] string? level,
[FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level,
[FromQuery] float? framerate,
[FromQuery] float? maxFramerate,
[FromQuery] bool? copyTimestamps,
@@ -599,8 +599,8 @@ public class VideosController : BaseJellyfinApiController
[FromQuery] int? cpuCoreLimit,
[FromQuery] string? liveStreamId,
[FromQuery] bool? enableMpegtsM2TsMode,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec,
[FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec,
[FromQuery] string? transcodeReasons,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? videoStreamIndex,

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

@@ -70,7 +70,7 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
/// <param name="message">The message.</param>
protected override void Start(WebSocketMessageInfo message)
{
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) && !message.Connection.AuthorizationInfo.IsApiKey)
{
throw new AuthenticationException("Only admin users can retrieve the activity log.");
}

View File

@@ -79,7 +79,7 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
/// <param name="message">The message.</param>
protected override void Start(WebSocketMessageInfo message)
{
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) && !message.Connection.AuthorizationInfo.IsApiKey)
{
throw new AuthenticationException("Only admin users can subscribe to session information.");
}

View File

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

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);

View File

@@ -118,15 +118,15 @@ namespace Jellyfin.Server.Extensions
// https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs
// Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues.
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
if (config.KnownProxies.Length == 0)
{
options.ForwardedHeaders = ForwardedHeaders.None;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
}
else
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
AddProxyAddresses(config, config.KnownProxies, options);
}

View File

@@ -51,6 +51,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.RemoveDuplicatePlaylistChildren)
};
private static readonly Guid _downgradeCheckMigration = Guid.Parse("36445464-849f-429f-9ad0-bb130efa0664");
/// <summary>
/// Run all needed migrations.
/// </summary>
@@ -88,6 +90,12 @@ namespace Jellyfin.Server.Migrations
? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
: new MigrationOptions();
// 10.10 specific EFCore migration check.
if (migrationOptions.Applied.Any(f => f.Id.Equals(_downgradeCheckMigration)))
{
throw new InvalidOperationException("You cannot downgrade your jellyfin install from the library.db migration.");
}
// We have to deserialize it manually since the configuration manager may overwrite it
var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)!

View File

@@ -30,7 +30,7 @@ namespace Jellyfin.Server.Migrations.Routines
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}");
public Guid Id => Guid.Parse("{D34BFC33-5D2E-4790-8085-069EF6EECB4E}");
/// <inheritdoc/>
public string Name => "MigrateRatingLevels";

View File

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

View File

@@ -22,7 +22,7 @@ namespace MediaBrowser.Controller.Channels
[JsonIgnore]
public override SourceType SourceType => SourceType.Channel;
public override bool IsVisible(User user)
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels);
if (blockedChannelsPreference.Length != 0)
@@ -41,7 +41,7 @@ namespace MediaBrowser.Controller.Channels
}
}
return base.IsVisible(user);
return base.IsVisible(user, skipAllowedTagsCheck);
}
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)

View File

@@ -1299,7 +1299,7 @@ namespace MediaBrowser.Controller.Entities
return false;
}
if (GetParents().Any(i => !i.IsVisible(user)))
if (GetParents().Any(i => !i.IsVisible(user, true)))
{
return false;
}
@@ -1521,13 +1521,14 @@ namespace MediaBrowser.Controller.Entities
/// Determines if a given user has access to this item.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
/// <returns><c>true</c> if [is parental allowed] [the specified user]; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException">If user is null.</exception>
public bool IsParentalAllowed(User user)
public bool IsParentalAllowed(User user, bool skipAllowedTagsCheck)
{
ArgumentNullException.ThrowIfNull(user);
if (!IsVisibleViaTags(user))
if (!IsVisibleViaTags(user, skipAllowedTagsCheck))
{
return false;
}
@@ -1599,7 +1600,7 @@ namespace MediaBrowser.Controller.Entities
return list.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
}
private bool IsVisibleViaTags(User user)
private bool IsVisibleViaTags(User user, bool skipAllowedTagsCheck)
{
var allTags = GetInheritedTags();
if (user.GetPreference(PreferenceKind.BlockedTags).Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
@@ -1614,7 +1615,7 @@ namespace MediaBrowser.Controller.Entities
}
var allowedTagsPreference = user.GetPreference(PreferenceKind.AllowedTags);
if (allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
if (!skipAllowedTagsCheck && allowedTagsPreference.Length != 0 && !allowedTagsPreference.Any(i => allTags.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
@@ -1654,13 +1655,14 @@ namespace MediaBrowser.Controller.Entities
/// Default is just parental allowed. Can be overridden for more functionality.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="skipAllowedTagsCheck">Don't check for allowed tags.</param>
/// <returns><c>true</c> if the specified user is visible; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="user" /> is <c>null</c>.</exception>
public virtual bool IsVisible(User user)
public virtual bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
ArgumentNullException.ThrowIfNull(user);
return IsParentalAllowed(user);
return IsParentalAllowed(user, skipAllowedTagsCheck);
}
public virtual bool IsVisibleStandalone(User user)

View File

@@ -96,11 +96,11 @@ namespace MediaBrowser.Controller.Entities
return GetLibraryOptions(Path);
}
public override bool IsVisible(User user)
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
if (GetLibraryOptions().Enabled)
{
return base.IsVisible(user);
return base.IsVisible(user, skipAllowedTagsCheck);
}
return false;

View File

@@ -217,7 +217,7 @@ namespace MediaBrowser.Controller.Entities
LibraryManager.CreateItem(item, this);
}
public override bool IsVisible(User user)
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
if (this is ICollectionFolder && this is not BasePluginFolder)
{
@@ -239,7 +239,7 @@ namespace MediaBrowser.Controller.Entities
}
}
return base.IsVisible(user);
return base.IsVisible(user, skipAllowedTagsCheck);
}
/// <summary>
@@ -1200,6 +1200,11 @@ namespace MediaBrowser.Controller.Entities
return false;
}
if (request.Is4K.HasValue)
{
return false;
}
if (request.IsHD.HasValue)
{
return false;

View File

@@ -144,14 +144,14 @@ namespace MediaBrowser.Controller.Entities.Movies
return GetItemLookupInfo<BoxSetInfo>();
}
public override bool IsVisible(User user)
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
if (IsLegacyBoxSet)
{
return base.IsVisible(user);
return base.IsVisible(user, skipAllowedTagsCheck);
}
if (base.IsVisible(user))
if (base.IsVisible(user, skipAllowedTagsCheck))
{
if (LinkedChildren.Length == 0)
{

View File

@@ -152,16 +152,7 @@ namespace MediaBrowser.Controller.Entities
{
get
{
if (!string.IsNullOrEmpty(PrimaryVersionId))
{
var item = LibraryManager.GetItemById(PrimaryVersionId);
if (item is Video video)
{
return video.MediaSourceCount;
}
}
return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
return GetMediaSourceCount(new HashSet<Guid>());
}
}
@@ -550,5 +541,25 @@ namespace MediaBrowser.Controller.Entities
return list;
}
private int GetMediaSourceCount(HashSet<Guid> callstack)
{
if (!string.IsNullOrEmpty(PrimaryVersionId))
{
var item = LibraryManager.GetItemById(PrimaryVersionId);
if (item is Video video)
{
if (callstack.Contains(video.Id))
{
return video.LinkedAlternateVersions.Length + video.LocalAlternateVersions.Length + 1;
}
callstack.Add(video.Id);
return video.GetMediaSourceCount(callstack);
}
}
return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
}
}
}

View File

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

View File

@@ -35,7 +35,13 @@ namespace MediaBrowser.Controller.MediaEncoding
/// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters.
/// This should matches all common valid codecs.
/// </summary>
public const string ValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$";
/// <summary>
/// The level validation regex.
/// This regular expression matches strings representing a double.
/// </summary>
public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?";
private const string _defaultMjpegEncoder = "mjpeg";
@@ -75,7 +81,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1);
private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0);
private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled);
private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled);
private static readonly string[] _videoProfilesH264 =
[
@@ -309,7 +315,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"))
{
@@ -451,7 +456,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return GetMjpegEncoder(state, encodingOptions);
}
if (_validationRegex.IsMatch(codec))
if (_containerValidationRegex.IsMatch(codec))
{
return codec.ToLowerInvariant();
}
@@ -492,7 +497,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public static string GetInputFormat(string container)
{
if (string.IsNullOrEmpty(container) || !_validationRegex.IsMatch(container))
if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container))
{
return null;
}
@@ -710,7 +715,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var codec = state.OutputAudioCodec;
if (!_validationRegex.IsMatch(codec))
if (!_containerValidationRegex.IsMatch(codec))
{
codec = "aac";
}
@@ -2061,7 +2066,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)
@@ -5690,7 +5701,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);
}
@@ -7064,7 +7079,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

@@ -227,11 +227,11 @@ namespace MediaBrowser.Controller.Playlists
return [item];
}
public override bool IsVisible(User user)
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
{
if (!IsSharedItem)
{
return base.IsVisible(user);
return base.IsVisible(user, skipAllowedTagsCheck);
}
if (OpenAccess)

View File

@@ -122,7 +122,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
_jsonSerializerOptions = new JsonSerializerOptions(JsonDefaults.Options);
_jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
var semaphoreCount = 2 * Environment.ProcessorCount;
// Although the type is not nullable, this might still be null during unit tests
var semaphoreCount = serverConfig.Configuration?.ParallelImageEncodingLimit ?? 0;
if (semaphoreCount < 1)
{
semaphoreCount = Environment.ProcessorCount;
}
_thumbnailResourcePool = new(semaphoreCount);
}

View File

@@ -17,7 +17,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
public class SubtitleEditParser : ISubtitleParser
{
private readonly ILogger<SubtitleEditParser> _logger;
private readonly Dictionary<string, SubtitleFormat[]> _subtitleFormats;
private readonly Dictionary<string, List<Type>> _subtitleFormatTypes;
/// <summary>
/// Initializes a new instance of the <see cref="SubtitleEditParser"/> class.
@@ -26,10 +26,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
public SubtitleEditParser(ILogger<SubtitleEditParser> logger)
{
_logger = logger;
_subtitleFormats = GetSubtitleFormats()
.Where(subtitleFormat => !string.IsNullOrEmpty(subtitleFormat.Extension))
.GroupBy(subtitleFormat => subtitleFormat.Extension.TrimStart('.'), StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase);
_subtitleFormatTypes = GetSubtitleFormatTypes();
}
/// <inheritdoc />
@@ -38,13 +35,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var subtitle = new Subtitle();
var lines = stream.ReadAllLines().ToList();
if (!_subtitleFormats.TryGetValue(fileExtension, out var subtitleFormats))
if (!_subtitleFormatTypes.TryGetValue(fileExtension, out var subtitleFormatTypesForExtension))
{
throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension));
}
foreach (var subtitleFormat in subtitleFormats)
foreach (var subtitleFormatType in subtitleFormatTypesForExtension)
{
var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(subtitleFormatType, true)!;
_logger.LogDebug(
"Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
fileExtension,
@@ -97,11 +95,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// <inheritdoc />
public bool SupportsFileExtension(string fileExtension)
=> _subtitleFormats.ContainsKey(fileExtension);
=> _subtitleFormatTypes.ContainsKey(fileExtension);
private List<SubtitleFormat> GetSubtitleFormats()
private Dictionary<string, List<Type>> GetSubtitleFormatTypes()
{
var subtitleFormats = new List<SubtitleFormat>();
var subtitleFormatTypes = new Dictionary<string, List<Type>>(StringComparer.OrdinalIgnoreCase);
var assembly = typeof(SubtitleFormat).Assembly;
foreach (var type in assembly.GetTypes())
@@ -113,9 +111,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles
try
{
// It shouldn't be null, but the exception is caught if it is
var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(type, true)!;
subtitleFormats.Add(subtitleFormat);
var tempInstance = (SubtitleFormat)Activator.CreateInstance(type, true)!;
var extension = tempInstance.Extension.TrimStart('.');
if (!string.IsNullOrEmpty(extension))
{
// Store only the type, we will instantiate from it later
if (!subtitleFormatTypes.TryGetValue(extension, out var subtitleFormatTypesForExtension))
{
subtitleFormatTypes[extension] = [type];
}
else
{
subtitleFormatTypesForExtension.Add(type);
}
}
}
catch (Exception ex)
{
@@ -123,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
}
return subtitleFormats;
return subtitleFormatTypes;
}
}
}

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.
@@ -862,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;
}
}
}
@@ -963,9 +982,28 @@ 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);
directAudioStreamSatisfied = directAudioStreamSatisfied && !playlistItem.TranscodeReasons.HasFlag(TranscodeReason.ContainerBitrateExceedsLimit);
var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : null;
if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
{
@@ -2213,7 +2251,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))
@@ -2221,6 +2259,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))
@@ -2235,19 +2287,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

@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Drawing;
@@ -586,6 +587,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the type of the media.
/// </summary>
/// <value>The type of the media.</value>
[DefaultValue(MediaType.Unknown)]
public MediaType MediaType { get; set; }
/// <summary>

View File

@@ -1,6 +1,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
@@ -34,6 +35,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the type.
/// </summary>
/// <value>The type.</value>
[DefaultValue(PersonKind.Unknown)]
public PersonKind Type { get; set; }
/// <summary>

View File

@@ -157,6 +157,7 @@ namespace MediaBrowser.Model.Entities
/// Gets the video range.
/// </summary>
/// <value>The video range.</value>
[DefaultValue(VideoRange.Unknown)]
public VideoRange VideoRange
{
get
@@ -171,6 +172,7 @@ namespace MediaBrowser.Model.Entities
/// Gets the video range type.
/// </summary>
/// <value>The video range type.</value>
[DefaultValue(VideoRangeType.Unknown)]
public VideoRangeType VideoRangeType
{
get

View File

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

View File

@@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.MediaSegments;
@@ -21,6 +22,7 @@ public class MediaSegmentDto
/// <summary>
/// Gets or sets the type of content this segment defines.
/// </summary>
[DefaultValue(MediaSegmentType.Unknown)]
public MediaSegmentType Type { get; set; }
/// <summary>

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Search
@@ -115,6 +116,7 @@ namespace MediaBrowser.Model.Search
/// Gets or sets the type of the media.
/// </summary>
/// <value>The type of the media.</value>
[DefaultValue(MediaType.Unknown)]
public MediaType MediaType { get; set; }
/// <summary>

View File

@@ -291,6 +291,7 @@ namespace MediaBrowser.Providers.Manager
var fileStreamOptions = AsyncFile.WriteOptions;
fileStreamOptions.Mode = FileMode.Create;
fileStreamOptions.Options = FileOptions.WriteThrough;
if (source.CanSeek)
{
fileStreamOptions.PreallocationSize = source.Length;

View File

@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
@@ -551,10 +552,16 @@ namespace MediaBrowser.Providers.Manager
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
var mimetype = response.Content.Headers.ContentType?.MediaType;
if (mimetype is null || mimetype.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
{
mimetype = MimeTypes.GetMimeType(response.RequestMessage.RequestUri.GetLeftPart(UriPartial.Path));
}
await _providerManager.SaveImage(
item,
stream,
response.Content.Headers.ContentType?.MediaType,
mimetype,
type,
null,
cancellationToken).ConfigureAwait(false);
@@ -677,10 +684,16 @@ namespace MediaBrowser.Providers.Manager
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
var mimetype = response.Content.Headers.ContentType?.MediaType;
if (mimetype is null || mimetype.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
{
mimetype = MimeTypes.GetMimeType(response.RequestMessage.RequestUri.GetLeftPart(UriPartial.Path));
}
await _providerManager.SaveImage(
item,
stream,
response.Content.Headers.ContentType?.MediaType,
mimetype,
imageType,
null,
cancellationToken).ConfigureAwait(false);

View File

@@ -1010,7 +1010,7 @@ namespace MediaBrowser.Providers.Manager
}
}
if (replaceData || !target.PremiereDate.HasValue)
if (replaceData || !target.PremiereDate.HasValue || (IsYearOnlyDate(target.PremiereDate.Value) && source.PremiereDate.HasValue))
{
target.PremiereDate = source.PremiereDate;
}
@@ -1142,6 +1142,8 @@ namespace MediaBrowser.Providers.Manager
}
}
private static bool IsYearOnlyDate(DateTime date) => date.Month == 1 && date.Day == 1;
private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target)
{
if (target is null)
@@ -1149,13 +1151,24 @@ namespace MediaBrowser.Providers.Manager
target = new List<PersonInfo>();
}
foreach (var person in target)
{
var normalizedName = person.Name.RemoveDiacritics();
var personInSource = source.FirstOrDefault(i => string.Equals(i.Name.RemoveDiacritics(), normalizedName, StringComparison.OrdinalIgnoreCase));
var sourceByName = source.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
var targetByName = target.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
if (personInSource is not null)
foreach (var name in targetByName.Select(g => g.Key))
{
var targetPeople = targetByName[name].ToArray();
var sourcePeople = sourceByName[name].ToArray();
if (sourcePeople.Length == 0)
{
continue;
}
for (int i = 0; i < targetPeople.Length; i++)
{
var person = targetPeople[i];
var personInSource = i < sourcePeople.Length ? sourcePeople[i] : sourcePeople[0];
foreach (var providerId in personInSource.ProviderIds)
{
person.ProviderIds.TryAdd(providerId.Key, providerId.Value);
@@ -1165,6 +1178,16 @@ namespace MediaBrowser.Providers.Manager
{
person.ImageUrl = personInSource.ImageUrl;
}
if (!string.IsNullOrWhiteSpace(personInSource.Role) && string.IsNullOrWhiteSpace(person.Role))
{
person.Role = personInSource.Role;
}
if (personInSource.SortOrder.HasValue && !person.SortOrder.HasValue)
{
person.SortOrder = personInSource.SortOrder;
}
}
}
}

View File

@@ -204,20 +204,10 @@ namespace MediaBrowser.Providers.Manager
{
contentType = MediaTypeNames.Image.Png;
}
else
{
throw new HttpRequestException("Invalid image received: contentType not set.", null, response.StatusCode);
}
}
// TVDb will sometimes serve a rubbish 404 html page with a 200 OK code, because reasons...
if (contentType.Equals(MediaTypeNames.Text.Html, StringComparison.OrdinalIgnoreCase))
{
throw new HttpRequestException("Invalid image received.", null, HttpStatusCode.NotFound);
}
// some iptv/epg providers don't correctly report media type, extract from url if no extension found
if (string.IsNullOrWhiteSpace(MimeTypes.ToExtension(contentType)))
// some providers don't correctly report media type, extract from url if no extension found
if (contentType is null || contentType.Equals(MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
{
// Strip query parameters from url to get actual path.
contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path));
@@ -225,7 +215,7 @@ namespace MediaBrowser.Providers.Manager
if (!contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
{
throw new HttpRequestException($"Request returned {contentType} instead of an image type", null, HttpStatusCode.NotFound);
throw new HttpRequestException($"Request returned '{contentType}' instead of an image type", null, HttpStatusCode.NotFound);
}
var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
@@ -262,7 +252,7 @@ namespace MediaBrowser.Providers.Manager
try
{
var fileStream = AsyncFile.OpenRead(source);
await new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
await new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken).ConfigureAwait(false);
}
finally
{

View File

@@ -170,16 +170,20 @@ namespace MediaBrowser.Providers.MediaInfo
_logger.LogWarning("File {File} only has ID3v1 tags, some fields may be truncated", audio.Path);
}
track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
track.Year ??= mediaInfo.ProductionYear;
track.TrackNumber ??= mediaInfo.IndexNumber;
track.DiscNumber ??= mediaInfo.ParentIndexNumber;
// We should never use the property setter of the ATL.Track class.
// That setter is meant for its own tag parser and external editor usage and will have unwanted side effects
// For example, setting the Year property will also set the Date property, which is not what we want here.
// To properly handle fallback values, we make a clone of those fields when valid.
var trackTitle = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
var trackAlbum = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
var trackYear = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year;
var trackTrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber;
var trackDiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber;
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 +214,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)
@@ -271,22 +275,22 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(track.Title))
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(trackTitle))
{
audio.Name = track.Title;
audio.Name = trackTitle;
}
if (options.ReplaceAllMetadata)
{
audio.Album = track.Album;
audio.IndexNumber = track.TrackNumber;
audio.ParentIndexNumber = track.DiscNumber;
audio.Album = trackAlbum;
audio.IndexNumber = trackTrackNumber;
audio.ParentIndexNumber = trackDiscNumber;
}
else
{
audio.Album ??= track.Album;
audio.IndexNumber ??= track.TrackNumber;
audio.ParentIndexNumber ??= track.DiscNumber;
audio.Album ??= trackAlbum;
audio.IndexNumber ??= trackTrackNumber;
audio.ParentIndexNumber ??= trackDiscNumber;
}
if (track.Date.HasValue)
@@ -294,11 +298,12 @@ namespace MediaBrowser.Providers.MediaInfo
audio.PremiereDate = track.Date;
}
if (track.Year.HasValue)
if (trackYear.HasValue)
{
var year = track.Year.Value;
var year = trackYear.Value;
audio.ProductionYear = year;
// ATL library handles such fallback this with its own internal logic, but we also need to handle it here for the ffprobe fallbacks.
if (!audio.PremiereDate.HasValue)
{
try
@@ -307,14 +312,14 @@ namespace MediaBrowser.Providers.MediaInfo
}
catch (ArgumentOutOfRangeException ex)
{
_logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, track.Year);
_logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, trackYear);
}
}
}
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)
{

View File

@@ -55,13 +55,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb
if (info.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string? seriesImdbId)
&& !string.IsNullOrEmpty(seriesImdbId)
&& info.IndexNumber.HasValue
&& info.ParentIndexNumber.HasValue)
&& info.IndexNumber.HasValue)
{
result.HasMetadata = await _omdbProvider.FetchEpisodeData(
result,
info.IndexNumber.Value,
info.ParentIndexNumber.Value,
info.ParentIndexNumber ?? 1,
info.GetProviderId(MetadataProvider.Imdb),
seriesImdbId,
info.MetadataLanguage,

View File

@@ -63,10 +63,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return Enumerable.Empty<RemoteImageInfo>();
}
var seasonNumber = episode.ParentIndexNumber;
var seasonNumber = episode.ParentIndexNumber ?? 1;
var episodeNumber = episode.IndexNumber;
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
if (!episodeNumber.HasValue)
{
return Enumerable.Empty<RemoteImageInfo>();
}
@@ -75,7 +75,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
var episodeResult = await _tmdbClientManager
.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, series.DisplayOrder, null, null, cancellationToken)
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, series.DisplayOrder, null, null, cancellationToken)
.ConfigureAwait(false);
var stills = episodeResult?.Images?.Stills;

View File

@@ -47,7 +47,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
// The search query must either provide an episode number or date
if (!searchInfo.IndexNumber.HasValue || !searchInfo.ParentIndexNumber.HasValue)
if (!searchInfo.IndexNumber.HasValue)
{
return Enumerable.Empty<RemoteSearchResult>();
}
@@ -96,10 +96,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return metadataResult;
}
var seasonNumber = info.ParentIndexNumber;
var seasonNumber = info.ParentIndexNumber ?? 1;
var episodeNumber = info.IndexNumber;
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
if (!episodeNumber.HasValue)
{
return metadataResult;
}
@@ -112,7 +112,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
List<TvEpisode>? result = null;
for (int? episode = startindex; episode <= endindex; episode++)
{
var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false);
var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false);
if (episodeInfo is not null)
{
(result ??= new List<TvEpisode>()).Add(episodeInfo);
@@ -156,7 +156,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
else
{
episodeResult = await _tmdbClientManager
.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
.GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
.ConfigureAwait(false);
}

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;
}
@@ -82,21 +79,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (!string.IsNullOrWhiteSpace(val) && movie is not null)
{
// TODO Handle this better later
if (!val.Contains('<', StringComparison.Ordinal))
try
{
movie.CollectionName = val;
ParseSetXml(val, movie);
}
else
catch (Exception ex)
{
try
{
ParseSetXml(val, movie);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error parsing set node");
}
Logger.LogError(ex, "Error parsing set node");
}
}
@@ -139,7 +128,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
if (reader.NodeType == XmlNodeType.Element)
if (reader.NodeType == XmlNodeType.Text && reader.Depth == 1)
{
movie.CollectionName = reader.Value;
break;
}
else if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{

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

@@ -115,7 +115,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
{
if (!string.IsNullOrEmpty(movie.CollectionName))
{
writer.WriteElementString("set", movie.CollectionName);
writer.WriteStartElement("set");
writer.WriteElementString("name", movie.CollectionName);
writer.WriteEndElement();
}
}
}

View File

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

View File

@@ -195,8 +195,10 @@ public class SkiaEncoder : IImageEncoder
return string.Empty;
}
// Use FileStream with FileShare.Read instead of having Skia open the file to allow concurrent read access
using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
// Any larger than 128x128 is too slow and there's no visually discernible difference
return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
return BlurHashEncoder.Encode(xComp, yComp, fileStream, 128, 128);
}
private bool RequiresSpecialCharacterHack(string path)

View File

@@ -66,7 +66,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
var semaphoreCount = config.Configuration.ParallelImageEncodingLimit;
if (semaphoreCount < 1)
{
semaphoreCount = 2 * Environment.ProcessorCount;
semaphoreCount = Environment.ProcessorCount;
}
_parallelEncodingLimit = new(semaphoreCount);

View File

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

View File

@@ -39,6 +39,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 +209,14 @@ public class GuideManager : IGuideManager
progress.Report(15);
numComplete = 0;
var programs = new List<Guid>();
var programIds = new List<Guid>();
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();
@@ -238,11 +243,12 @@ public class GuideManager : IGuideManager
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
var newPrograms = new List<LiveTvProgram>();
var updatedPrograms = new List<BaseItem>();
var updatedPrograms = new List<LiveTvProgram>();
foreach (var program in channelPrograms)
{
var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
var id = programItem.Id;
if (isNew)
{
newPrograms.Add(programItem);
@@ -252,7 +258,7 @@ public class GuideManager : IGuideManager
updatedPrograms.Add(programItem);
}
programs.Add(programItem.Id);
programIds.Add(programItem.Id);
isMovie |= program.IsMovie;
isSeries |= program.IsSeries;
@@ -261,12 +267,17 @@ 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);
_libraryManager.CreateItems(newPrograms, currentChannel, cancellationToken);
await PreCacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
}
if (updatedPrograms.Count > 0)
@@ -276,7 +287,8 @@ public class GuideManager : IGuideManager
currentChannel,
ItemUpdateType.MetadataImport,
cancellationToken).ConfigureAwait(false);
await PrecacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
await PreCacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
}
currentChannel.IsMovie = isMovie;
@@ -313,7 +325,7 @@ public class GuideManager : IGuideManager
}
progress.Report(100);
return new Tuple<List<Guid>, List<Guid>>(channels, programs);
return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
}
private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
@@ -488,35 +500,27 @@ public class GuideManager : IGuideManager
forceUpdate = true;
}
var seriesId = info.SeriesId;
if (!item.ParentId.Equals(channel.Id))
var channelId = channel.Id;
if (!item.ParentId.Equals(channelId))
{
item.ParentId = channel.Id;
forceUpdate = true;
}
item.ParentId = channel.Id;
item.Audio = info.Audio;
item.ChannelId = channel.Id;
item.CommunityRating ??= info.CommunityRating;
if ((item.CommunityRating ?? 0).Equals(0))
{
item.CommunityRating = null;
}
item.ChannelId = channelId;
item.CommunityRating = info.CommunityRating;
item.EpisodeTitle = info.EpisodeTitle;
item.ExternalId = info.Id;
if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal))
var seriesId = info.SeriesId;
if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.OrdinalIgnoreCase))
{
item.ExternalSeriesId = seriesId;
forceUpdate = true;
}
item.ExternalSeriesId = seriesId;
var isSeries = info.IsSeries || !string.IsNullOrEmpty(info.EpisodeTitle);
if (isSeries || !string.IsNullOrEmpty(info.EpisodeTitle))
{
item.SeriesName = info.Name;
@@ -564,7 +568,6 @@ public class GuideManager : IGuideManager
}
item.Tags = tags.ToArray();
item.Genres = info.Genres.ToArray();
if (info.IsHD ?? false)
@@ -575,41 +578,35 @@ public class GuideManager : IGuideManager
item.IsMovie = info.IsMovie;
item.IsRepeat = info.IsRepeat;
if (item.IsSeries != isSeries)
{
item.IsSeries = isSeries;
forceUpdate = true;
}
item.IsSeries = isSeries;
item.Name = info.Name;
item.OfficialRating ??= info.OfficialRating;
item.Overview ??= info.Overview;
item.OfficialRating = info.OfficialRating;
item.Overview = info.Overview;
item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks;
item.ProviderIds = info.ProviderIds;
foreach (var providerId in info.SeriesProviderIds)
{
info.ProviderIds["Series" + providerId.Key] = providerId.Value;
}
item.ProviderIds = info.ProviderIds;
if (item.StartDate != info.StartDate)
{
item.StartDate = info.StartDate;
forceUpdate = true;
}
item.StartDate = info.StartDate;
if (item.EndDate != info.EndDate)
{
item.EndDate = info.EndDate;
forceUpdate = true;
}
item.EndDate = info.EndDate;
item.ProductionYear = info.ProductionYear;
if (!isSeries || info.IsRepeat)
{
item.PremiereDate = info.OriginalAirDate;
@@ -618,100 +615,113 @@ 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 |= UpdateImages(item, info);
if (!item.HasImage(ImageType.Thumb))
{
if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
{
item.SetImage(
new ItemImageInfo
{
Path = info.ThumbImageUrl,
Type = ImageType.Thumb
},
0);
}
}
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);
}
}
var isUpdated = false;
if (isNew)
{
item.OnMetadataChanged();
return (item, true, false);
}
else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
var isUpdated = forceUpdate;
var etag = info.Etag;
if (string.IsNullOrWhiteSpace(etag))
{
isUpdated = true;
}
else
else if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
{
var etag = info.Etag;
if (!string.Equals(etag, item.GetProviderId(EtagKey), StringComparison.OrdinalIgnoreCase))
{
item.SetProviderId(EtagKey, etag);
isUpdated = true;
}
item.SetProviderId(EtagKey, etag);
isUpdated = true;
}
if (isNew || isUpdated)
if (isUpdated)
{
item.OnMetadataChanged();
return (item, false, true);
}
return (item, isNew, isUpdated);
return (item, false, false);
}
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
updated |= UpdateImage(ImageType.Backdrop, item, info);
return updated;
}
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,
_ => null
};
var newImageUrl = imageType switch
{
ImageType.Backdrop => info.BackdropImageUrl,
ImageType.Logo => info.LogoImageUrl,
ImageType.Primary => info.ImageUrl,
ImageType.Thumb => info.ThumbImageUrl,
_ => null
};
var sameImage = (currentImagePath?.Equals(newImageUrl, StringComparison.OrdinalIgnoreCase) ?? false)
|| (currentImagePath?.Equals(newImagePath, StringComparison.OrdinalIgnoreCase) ?? false);
if (sameImage)
{
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
@@ -730,6 +740,7 @@ public class GuideManager : IGuideManager
var imageInfo = program.ImageInfos[i];
if (!imageInfo.IsLocalFile)
{
_logger.LogDebug("Caching image locally: {Url}", imageInfo.Path);
try
{
program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal(
@@ -741,7 +752,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

@@ -689,10 +689,10 @@ public class NetworkManager : INetworkManager, IDisposable
{
// Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
// If left blank, all remote addresses will be allowed.
if (_remoteAddressFilter.Any() && !_lanSubnets.Any(x => x.Contains(remoteIP)))
if (_remoteAddressFilter.Any() && !IsInLocalNetwork(remoteIP))
{
// remoteAddressFilter is a whitelist or blacklist.
var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIP));
var matches = _remoteAddressFilter.Count(remoteNetwork => SubnetContainsAddress(remoteNetwork, remoteIP));
if ((!config.IsRemoteIPFilterBlacklist && matches > 0)
|| (config.IsRemoteIPFilterBlacklist && matches == 0))
{
@@ -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;
@@ -816,7 +816,7 @@ public class NetworkManager : INetworkManager, IDisposable
_logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
}
bool isExternal = !_lanSubnets.Any(network => network.Contains(source));
bool isExternal = !IsInLocalNetwork(source);
_logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal);
if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result))
@@ -863,7 +863,7 @@ public class NetworkManager : INetworkManager, IDisposable
// (For systems with multiple internal network cards, and multiple subnets)
foreach (var intf in availableInterfaces)
{
if (intf.Subnet.Contains(source))
if (SubnetContainsAddress(intf.Subnet, source))
{
result = NetworkUtils.FormatIPString(intf.Address);
_logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result);
@@ -891,21 +891,11 @@ public class NetworkManager : INetworkManager, IDisposable
{
if (NetworkUtils.TryParseToSubnet(address, out var subnet))
{
return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix)));
return IsInLocalNetwork(subnet.Prefix);
}
if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
{
foreach (var ept in addresses)
{
if (IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept))))
{
return true;
}
}
}
return false;
return NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)
&& addresses.Any(IsInLocalNetwork);
}
/// <summary>
@@ -919,6 +909,19 @@ public class NetworkManager : INetworkManager, IDisposable
return NetworkConstants.IPv4RFC3927LinkLocal.Contains(address) || address.IsIPv6LinkLocal;
}
private static bool SubnetContainsAddress(IPNetwork network, IPAddress address)
{
ArgumentNullException.ThrowIfNull(address);
ArgumentNullException.ThrowIfNull(network);
if (address.IsIPv4MappedToIPv6)
{
address = address.MapToIPv4();
}
return network.Contains(address);
}
/// <inheritdoc/>
public bool IsInLocalNetwork(IPAddress address)
{
@@ -940,6 +943,11 @@ public class NetworkManager : INetworkManager, IDisposable
return CheckIfLanAndNotExcluded(address);
}
/// <summary>
/// Check if the address is in the LAN and not excluded.
/// </summary>
/// <param name="address">The IP address to check. The caller should make sure this is not an IPv4MappedToIPv6 address.</param>
/// <returns>Boolean indicates whether the address is in LAN.</returns>
private bool CheckIfLanAndNotExcluded(IPAddress address)
{
foreach (var lanSubnet in _lanSubnets)
@@ -979,7 +987,7 @@ public class NetworkManager : INetworkManager, IDisposable
{
// Only use matching internal subnets
// Prefer more specific (bigger subnet prefix) overrides
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && SubnetContainsAddress(x.Data.Subnet, source))
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
.ToList();
}
@@ -987,7 +995,7 @@ public class NetworkManager : INetworkManager, IDisposable
{
// Only use matching external subnets
// Prefer more specific (bigger subnet prefix) overrides
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && SubnetContainsAddress(x.Data.Subnet, source))
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
.ToList();
}
@@ -995,9 +1003,11 @@ public class NetworkManager : INetworkManager, IDisposable
foreach (var data in validPublishedServerUrls)
{
// Get interface matching override subnet
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => SubnetContainsAddress(data.Data.Subnet, x.Address));
if (intf?.Address is not null)
if (intf?.Address is not null
|| (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any))
|| (data.Data.AddressFamily == AddressFamily.InterNetworkV6 && data.Data.Address.Equals(IPAddress.IPv6Any)))
{
// If matching interface is found, use override
bindPreference = data.OverrideUri;
@@ -1025,6 +1035,7 @@ public class NetworkManager : INetworkManager, IDisposable
}
_logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
return true;
}
@@ -1055,6 +1066,7 @@ public class NetworkManager : INetworkManager, IDisposable
if (isInExternalSubnet)
{
var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address))
.Where(x => !IsLinkLocalAddress(x.Address))
.OrderBy(x => x.Index)
.ToList();
if (externalInterfaces.Count > 0)
@@ -1062,7 +1074,8 @@ public class NetworkManager : INetworkManager, IDisposable
// Check to see if any of the external bind interfaces are in the same subnet as the source.
// If none exists, this will select the first external interface if there is one.
bindAddress = externalInterfaces
.OrderByDescending(x => x.Subnet.Contains(source))
.OrderByDescending(x => SubnetContainsAddress(x.Subnet, source))
.ThenByDescending(x => x.Subnet.PrefixLength)
.ThenBy(x => x.Index)
.Select(x => x.Address)
.First();
@@ -1079,7 +1092,8 @@ public class NetworkManager : INetworkManager, IDisposable
// Check to see if any of the internal bind interfaces are in the same subnet as the source.
// If none exists, this will select the first internal interface if there is one.
bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
.OrderByDescending(x => x.Subnet.Contains(source))
.OrderByDescending(x => SubnetContainsAddress(x.Subnet, source))
.ThenByDescending(x => x.Subnet.PrefixLength)
.ThenBy(x => x.Index)
.Select(x => x.Address)
.FirstOrDefault();
@@ -1122,7 +1136,7 @@ public class NetworkManager : INetworkManager, IDisposable
// (For systems with multiple network cards and/or multiple subnets)
foreach (var intf in extResult)
{
if (intf.Subnet.Contains(source))
if (SubnetContainsAddress(intf.Subnet, source))
{
result = NetworkUtils.FormatIPString(intf.Address);
_logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result);

View File

@@ -141,8 +141,10 @@ namespace Jellyfin.Providers.Tests.Manager
{ "ProductionYear", 1, 2 },
{ "CommunityRating", 1.0f, 2.0f },
{ "CriticRating", 1.0f, 2.0f },
{ "EndDate", DateTime.UnixEpoch, DateTime.Now },
{ "PremiereDate", DateTime.UnixEpoch, DateTime.Now },
{ "EndDate", DateTime.UnixEpoch, DateTime.UtcNow },
{ "PremiereDate", DateTime.UnixEpoch, DateTime.UtcNow },
{ "PremiereDate", new DateTime(1999, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTime.UtcNow },
{ "PremiereDate", new DateTime(2025, 2, 21, 0, 0, 0, DateTimeKind.Utc), DateTime.UtcNow },
{ "Video3DFormat", Video3DFormat.HalfSideBySide, Video3DFormat.FullSideBySide }
};
@@ -151,7 +153,15 @@ namespace Jellyfin.Providers.Tests.Manager
public void MergeBaseItemData_SimpleField_ReplacesAppropriately(string propName, object oldValue, object newValue)
{
// Use type Movie to allow testing of Video3DFormat
Assert.False(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, false, out _));
if (propName.Equals("PremiereDate", StringComparison.Ordinal) && oldValue is DateTime oldDateTime)
{
bool expectReplaced = oldDateTime.Month == 1 && oldDateTime.Day == 1;
Assert.Equal(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, false, out _), expectReplaced);
}
else
{
Assert.False(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, false, out _));
}
Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, true, out _));
Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, null, newValue, null, false, out _));

View File

@@ -1,6 +1,5 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Emby.Server.Implementations.Localization;
using MediaBrowser.Controller.Configuration;
@@ -84,7 +83,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
await localizationManager.LoadAll();
var ratings = localizationManager.GetParentalRatings().ToList();
Assert.Equal(54, ratings.Count);
Assert.Equal(56, ratings.Count);
var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
Assert.NotNull(tvma);
@@ -116,6 +115,10 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
[InlineData("TV-MA", "US", 17)]
[InlineData("XXX", "asdf", 1000)]
[InlineData("Germany: FSK-18", "DE", 18)]
[InlineData("Rated : R", "US", 17)]
[InlineData("Rated: R", "US", 17)]
[InlineData("Rated R", "US", 17)]
[InlineData(" PG-13 ", "US", 13)]
public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel)
{
var localizationManager = Setup(new ServerConfiguration()

View File

@@ -257,5 +257,23 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
}
[Fact]
public void Parsing_Fields_With_Escaped_Xml_Special_Characters_Success()
{
var result = new MetadataResult<Video>()
{
Item = new Movie()
};
_parser.Fetch(result, "Test Data/Lilo & Stitch.nfo", CancellationToken.None);
var item = (Movie)result.Item;
Assert.Equal("Lilo & Stitch", item.Name);
Assert.Equal("Lilo & Stitch", item.OriginalTitle);
Assert.Equal("Lilo & Stitch Collection", item.CollectionName);
Assert.StartsWith(">>", item.Overview, StringComparison.InvariantCulture);
Assert.EndsWith("<<", item.Overview, StringComparison.InvariantCulture);
}
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<movie>
<title>Lilo &amp; Stitch</title>
<originaltitle>Lilo &amp; Stitch</originaltitle>
<set>Lilo &amp; Stitch Collection</set>
<plot>&gt;&gt;As Stitch, a runaway genetic experiment from a faraway planet, wreaks havoc on the Hawaiian Islands, he becomes the mischievous adopted alien "puppy" of an independent little girl named Lilo and learns about loyalty, friendship, and ʻohana, the Hawaiian tradition of family.&lt;&lt;</plot>
</movie>